Walkthrough: Standalone Example
See this example on GitHub, where you can find the instructions to run it yourself.
Setup
This example is a web application that talks to a web worker.
The web application:
- Create and starts the worker
- Makes sure the worker is ready before doing anything
- Worker will send a message when ready
- Calls a function on the worker to do some work
The web worker:
- Does some initialization
- Signals the web application that it's ready
- Handles the function call from the web application
Run Workex
The interface can be defined as
// src/msg/proto.ts
import { WorkexPromise as Promise } from "./workex";
// in this case, the app doesn't have an API the worker
// can call. So we only need one interface (one direction).
/**
* Functions the app can call on the worker
*
* @workex:send app
* @workex:recv worker
*/
export interface MyAwesomeLib {
/** Do some work and return the result as string */
doWork(): Promise<string>;
}
Now run workex to generate the interfaces and workex library
workex --protocol app src/msg/proto.ts
Run inside the examples/standalone
directory.
If you don't have workex
installed yet, use cargo run --
instead.
The Worker Side
See the comments in the code that walks through the implementation
// src/worker.ts
// import utilities from workex
import { hostFromDelegate, type Delegate } from "./msg/workex";
// import generated types from `sides`
import { bindMyAwesomeLibHost } from "./msg/sides/worker.ts";
// import input types from the input file `proto.ts`
import type { MyAwesomeLib } from "./msg/proto.ts";
// helper function to help us distinguish between app and worker logs
function print(msg: any) {
console.log("worker: " + msg);
}
// Do whatever initializations needed in the worker
// here we just log that the worker has started
print("started");
// function to simulate some work that takes some time
// this is synchronous, so we can demonstrate workers are on separate threads
function someExpensiveWork(): string {
// do some expensive work
let now = Date.now();
while (Date.now() - now < 2000) {
// do nothing
}
return "Hello from worker!";
}
// Create the handler to handle the messages sent by app
//
// Using the `Delegate` type, each function here returns a regular
// Promise instead of WorkexPromise. Then later we use `hostFromDelegate`
// to wrap the result of each function as WorkexPromise
// Note that making a class and `new`-ing it will not work
// because how hostFromDelegate is implemented
const handler = {
async doWork(): Promise<string> {
print("received doWork request from app");
const result = someExpensiveWork();
print("work done!");
return result;
},
} satisfies Delegate<MyAwesomeLib>;
const options = { worker: self };
// Now we bind the handler to the worker
// at this point, messages from the app can be handled
const handshake = bindMyAwesomeLibHost(hostFromDelegate(handler), options);
// we use the returned handshake object to notify the other side
// that we are ready.
handshake.initiate();
// initiate() returns a Promise that you can await on.
// If the calls can go both directions, you need to await for that
// before you can start calling the other side (which is what the app does
// in this example)
The Web App Side
See the comments in the code that walks through the implementation
// src/app.ts
// import utilities from workex
import type { WorkexResult } from "./msg/workex";
// import generated types from `sides`
import { MyAwesomeLibClient } from "./msg/sides/app.ts";
// helper function to help us distinguish between app and worker logs
function print(msg: any) {
console.log("app: " + msg);
}
export async function createWorker(): Promise<MyAwesomeLibClient> {
print("creating worker");
// /dist/worker.js is where the build tool puts the worker file
const worker = new Worker("/dist/worker.js");
const options = { worker };
const client = new MyAwesomeLibClient(options);
// the worker will initiate the handshake, we need to wait for
// it to be established
print("waiting for handshake to be established");
await client.handshake().established();
print("worker ready");
// at this point, we have completed the handshake, and both sides are ready
// to communicate
return client;
}
async function main() {
print("starting");
const worker = await createWorker();
setTimeout(() => {
// to prove workers are on separate threads
// log a message while the worker is synchronously
// doing some work
print(
"if this message is before `work done!`, then worker is on a separate thread",
);
}, 1000);
// the type is inferred. Just putting it here to be clear for you
const result: WorkexResult<string> = await worker.doWork();
if (result.val) {
print("worker returned:" + result.val);
} else {
console.error(result.err);
}
// cleanup
print("terminating worker");
worker.terminate();
}
main();
Run the Example
First let's do a type check with tsc
bunx tsc
Then, build the project
mkdir -p dist
bun build src/app.ts --outfile dist/app.js --minify
bun build src/worker.ts --outfile dist/worker.js --minify
Finally, serve the project
bunx serve .
Open the served page in the browser and open the console. You should see the message exchange working as expected!
app: starting
app: creating worker
app: waiting for handshake to be established
worker: started
app: worker ready
worker: received doWork request from app
app: if this message is before `work done!`, then worker is on a separate thread
worker: work done!
app: worker returned:Hello from worker!
app: terminating worker