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

Tip

The example uses bun as a build tool. Run bun install to set up the dependencies

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

Info

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