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
The example references the TS SDK in the PNPM workspace, which requires
you to use pnpm
as the package manager. Additionally, it uses
bun
as a bundler to build the TS files.
If you don't have pnpm, you can either install it,
or replace the version of @pistonite/workex
in package.json
with the latest version published on npmjs.org, and then
install the node modules with your favorite package manager
Run Workex
The interface can be defined as
// src/msg/proto.ts
import { WorkexPromise as Promise } from "@pistonite/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 example (packages/example-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 "@pistonite/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 "@pistonite/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