werkbank
    Preparing search index...

    Getting Started with Werkbank RPC

    Werkbank RPC is a type-safe library for seamless communication between the main thread and Web Workers. It allows you to invoke worker functions as if they were local asynchronous methods, handling all the complexity of postMessage, serialization, and error propagation for you.

    • Bundler Support: You must use a bundler that supports Web Workers (e.g., Parcel, Vite, Webpack 5+).
    • Environment: Modern browsers supporting ES Modules and Web Workers.

    JavaScript runs on a single thread. Heavy computations (like image processing, large data sorting, or complex parsing) can block the UI, causing the page to freeze. Web Workers allow you to run this code in a separate background thread, keeping your application responsive.

    The native postMessage API for workers is low-level and event-based. You send a message and listen for a response, which makes it hard to correlate requests with replies, handle errors, or maintain type safety.

    Remote Procedure Call (RPC) abstracts this away. It lets you define an API on the worker and call it from the client like this:

    // Instead of postMessage(...)
    let result = await client.heavyComputation(data);

    Note: Even though your API might define synchronous return types (like number), the RPC client will always return a Promise<number> because worker communication is asynchronous.

    Ensure werkbank and rxjs are installed in your project (rxjs is required for streaming support):

    npm install werkbank rxjs
    

    We will create a simple worker that performs calculations and exposes them to the main thread. This approach works with any frontend framework (React, Vue, Svelte, etc.) or vanilla JavaScript.

    Implement the API and expose it using expose. This function sets up the necessary onmessage listeners to handle incoming RPC requests from the main thread.

    Note: Workers support standard ES Module imports, so you can import other files or libraries as needed.

    worker.ts

    import { expose } from "werkbank/rpc/worker";
    import { Observable } from "rxjs";

    const api = {
    add: (a, b) => a + b,

    count: (to) => {
    return new Observable((subscriber) => {
    let i = 0;
    let interval = setInterval(() => {
    subscriber.next(i++);
    if (i > to) {
    clearInterval(interval);
    subscriber.complete();
    }
    }, 1000);

    // Teardown logic: This function is called when the client unsubscribes
    // or the Observable completes. Use it to clean up resources.
    return () => clearInterval(interval);
    });
    },

    processImage: (buffer) => {
    // Simulate processing
    return buffer;
    },

    log: (msg) => {
    console.log("Worker received:", msg);
    },

    fetchData: async (url) => {
    // You can use async/await inside the worker
    const response = await fetch(url);
    return response.json();
    }
    };

    export type WorkerApi = typeof api;

    // 'mod' stands for module or implementation
    expose(api);

    In your main application file, initialize the worker and create the RPC client.

    index.ts

    import { createClient } from "werkbank/rpc/client";
    import type { WorkerApi } from "./worker";

    async function main() {
    // Initialize the worker using standard ESM syntax (supported by Vite, Parcel, Webpack 5)
    const worker = new Worker(new URL("./worker.ts", import.meta.url), {
    type: "module",
    });

    // Create the typed client
    const client = createClient<WorkerApi>(worker);

    // 1. Basic Method Call (Returns Promise)
    const sum = await client.add(5, 10);
    console.log("Sum:", sum); // 15

    // 2. Void Method Call (Returns Promise<void>)
    await client.log("Hello from client");

    // 3. Async Method Call
    const data = await client.fetchData("https://api.example.com/data");

    // 4. Streaming (Observables)
    console.log("Starting count...");
    const subscription = client.count(5).subscribe({
    next: (val) => console.log("Count:", val),
    complete: () => console.log("Done!"),
    });

    // To cancel the stream, simply unsubscribe:
    // subscription.unsubscribe();
    }

    main();

    If an error occurs in the worker, it is propagated to the client.

    try {
    await client.add(1, 2);
    } catch (err) {
    console.error("Worker failed:", err);
    }

    You can use console.log inside your worker. The output will appear in the browser's developer console, usually marked with the worker file name.

    Web Workers run in a separate thread and have some limitations:

    • No DOM Access: You cannot access document, window, or manipulate HTML elements directly.
    • No localStorage: You cannot access local storage (use IndexedDB instead).
    • Serialization: All data passed to/from the worker must be serializable (JSON-compatible) or Transferable. You cannot pass functions or DOM elements.

    You can create multiple workers by instantiating new Worker objects and creating a client for each.

    const worker1 = new Worker(...);
    const client1 = createClient<Api1>(worker1);

    const worker2 = new Worker(...);
    const client2 = createClient<Api2>(worker2);

    To send messages from the worker to the client (like progress updates or notifications), use Observables (as shown in the count example). This allows the worker to emit multiple values over time.

    When passing large data (like ArrayBuffer, ImageBitmap) between threads, standard cloning is slow. Transferables allow you to move ownership of the memory instantly, with zero copy overhead.

    Supported Transferables: ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas.

    Note: If you do not use Transferables, the data will be cloned, which can be slow for large files.

    Werkbank RPC handles transferables in two ways:

    The library automatically detects Transferable objects (like ArrayBuffer) if they are passed as top-level arguments.

    // Client
    const largeBuffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB
    await client.processImage(largeBuffer);
    // 'largeBuffer' is now unusable in the client (detached)

    If you have Transferables nested inside objects, or want to be explicit, pass an array of Transferables as the last argument.

    const data = { id: 1, buffer: myBuffer };

    // Pass [myBuffer] as the last argument to mark it for transfer
    await client.saveData(data, [myBuffer]);