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.

Tip

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

Info

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