Skip to content

A lightweight, type-safe library for communicating with Web Workers via proxied function calls.

License

Notifications You must be signed in to change notification settings

WhimsicalCode/uniworker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

UniWorker

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.

Features

  • 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

Install

npm install uniworker

Quick Start

1. Define worker code (my-worker.ts)

import { 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;
    },
  };
});

2. Use from the main thread (main.ts)

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); // 5

API

expose(initializer)

Call 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);
    },
  };
});

init<T>(options, ...args): Promise<WorkerProxy<ReturnType<T>>>

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
);

WorkerProxy<T>

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.

Transferring data

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);

createMockProxy<T>(obj): WorkerProxy<T>

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!"

How It Works

  1. expose() registers the initializer and listens for messages in the worker.
  2. init() sends the init arguments to the worker, which runs the initializer and returns a mapping of method names to internal IDs.
  3. The main-thread proxy translates method calls into postMessage calls using these IDs.
  4. For await calls, 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.

License

MIT © Whimsical, Inc.

About

A lightweight, type-safe library for communicating with Web Workers via proxied function calls.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors