A lightweight, type-safe library for communicating with Web Workers via proxied function calls.
UniWorker lets you call functions inside a Web Worker as if they were local, abstracting away postMessage / addEventListener boilerplate entirely. It's inspired by Comlink, while being simpler, more limited in scope, and significantly faster.
- Fully typed — proxy methods preserve the original function signatures
- Three calling modes — fire-and-forget, awaited, and proxy-creating
- Transferable support — transfer
ArrayBuffer,OffscreenCanvas,ImageBitmap, etc. - Mock proxy — run the same code synchronously on the main thread for testing or fallback
- Zero dependencies
- ~1 KB minified + gzipped
npm install uniworkerimport { expose } from "uniworker";
expose((name: string) => {
// The initializer function runs once when init() is called.
// It returns an object whose methods become available on the proxy.
return {
greet(greeting: string) {
return `${greeting}, ${name}!`;
},
add(a: number, b: number) {
return a + b;
},
};
});import { init, WorkerProxy } from "uniworker";
// Type describing the initializer function (matches the expose() argument)
type MyWorkerInit = (name: string) => {
greet(greeting: string): string;
add(a: number, b: number): number;
};
const worker = new Worker(new URL("./my-worker.ts", import.meta.url), {
type: "module",
});
const proxy = await init<MyWorkerInit>({ worker }, "World");
// Fire-and-forget (returns void, does not wait)
proxy.greet("Hello");
// Await the return value
const message = await proxy.await.greet("Hello");
console.log(message); // "Hello, World!"
const sum = await proxy.await.add(2, 3);
console.log(sum); // 5Call inside a Web Worker script. initializer is a function that receives arguments passed from init() and returns an object whose methods are exposed to the main thread.
expose((canvas: OffscreenCanvas) => {
const ctx = canvas.getContext("2d")!;
return {
drawRect(x: number, y: number, w: number, h: number) {
ctx.fillRect(x, y, w, h);
},
};
});Call on the main thread to initialize the worker and get a typed proxy.
Options:
| Option | Type | Description |
|---|---|---|
worker |
Worker |
The Web Worker instance |
transfer |
Transfer |
Optional array of transferable objects to send with the init call |
const canvas = document.createElement("canvas");
const offscreen = canvas.transferControlToOffscreen();
const proxy = await init<MyWorkerInit>(
{ worker, transfer: [offscreen] },
offscreen
);The proxy object returned by init(). It provides three ways to call each exposed method:
| Mode | Syntax | Returns | Description |
|---|---|---|---|
| Fire-and-forget | proxy.method(args) |
void |
Fastest. Sends message, doesn't wait for a result. |
| Awaited | proxy.await.method(args) |
Promise<T> |
Sends message and resolves with the return value. |
| Create proxy | proxy.createProxy.method(args) |
Promise<WorkerProxy<T>> |
Like await, but the return value is itself wrapped in a new proxy. Useful for factory functions that return objects with their own methods. |
Use .transfer() before calling a method to transfer ownership of objects:
proxy.transfer([imageBitmap]).drawImage(imageBitmap);
// Also works with await
const result = await proxy.transfer([buffer]).await.process(buffer);Creates a synchronous mock proxy that wraps a plain object with the same interface as WorkerProxy. Useful for:
- Testing without spinning up real workers
- Fallback when Web Workers are unavailable
- Isomorphic code that should work with or without a worker
import { createMockProxy, WorkerProxy } from "uniworker";
const impl = {
greet(name: string) {
return `Hello, ${name}!`;
},
};
const proxy = createMockProxy(impl);
// Same interface as worker proxy
proxy.greet("World"); // fire-and-forget (sync)
const msg = await proxy.await.greet("World"); // "Hello, World!"expose()registers the initializer and listens for messages in the worker.init()sends the init arguments to the worker, which runs the initializer and returns a mapping of method names to internal IDs.- The main-thread proxy translates method calls into
postMessagecalls using these IDs. - For
awaitcalls, a return handler is registered and resolved when the worker posts back the result.
The protocol is a simple positional array format ([fnID, returnID, proxy, ...args]), avoiding serialization overhead of structured objects.
MIT © Whimsical, Inc.