Span: A Web Worker Bridging Module

deno module Unit Test CI

Allows for class definitions to be translated to awaitable function definitions which run in the same Web Worker Each function defined within a given class definition is passed an SharedArrayBuffer which can be used for caching execution state for later use. The goal of this project is to lower the barrier to use web workers within applications and provide an intuitive abstraction for managing execution state and shared memory.

Each function within a class definition extending WorkerDefinition is given its own shared buffer instance which is accessible from the generated bridge the bridge exports all wrapper functions for the given definition.

Running Web Assembly modules

It's also possible to declare a Web Assembly file which can be interfaced with with in the worker context.

Currently supports

  • Golang
  • Rust

compiled WASM. Support will be added for WASM compiled and instated through module exports shall be added.

note when compiling rust through wasm bidgen only --target web is known to be supported.

Under development, still largely a work in progress Should not be used in production.

Example JS

class Example extends WorkerDefinition {
  public constructor() {
    super();
  }

  public addOne(
    buffer: SharedArrayBuffer,
    args: Record<string, any>,
  ): SharedArrayBuffer {
    console.log("param name value: ", args.name);
    const arr = new Int8Array(buffer);
    arr[0] += 1;
    return buffer;
  }

  public fib(
    buffer: SharedArrayBuffer,
    module: Record<string, any>,
  ): SharedArrayBuffer {
    let i;
    const arr = new Uint8Array(buffer);
    arr[0] = 0;
    arr[1] = 1;

    for (i = 2; i <= module.count; i++) {
      arr[i] = arr[i - 2] + arr[i - 1];
      console.log(arr[i]);
    }
    
    return buffer;
  }
}

const example: Example = new Example();

const wrapper: InstanceWrapper<Example> = new InstanceWrapper<Example>(
  example,
  {} as InstanceConfiguration,
);

wrapper.start();

await example.execute("addOne", {name: 'foo'}).then((buf: SharedArrayBuffer) => {
  console.log("add one result: ", new Int32Array(buf));
});
await example.execute("addOne", { name: "foo" }).then(
  (buf: SharedArrayBuffer) => {
    console.log("add one result ", new Int32Array(buf)[0]);
  },
);

await example.execute("fib", {count: 10}).then((buffer: SharedArrayBuffer) => {
  console.log('fib result ', new Uint8Array(buffer));
  console.log('last fib number', new Uint8Array(buffer)[10]);
});

Usage of generated code

class Example extends WorkerDefinition {
  public constructor() {
    super();
  }

  public addOne(
    buffer: SharedArrayBuffer,
    args: Record<string, any>,
  ): SharedArrayBuffer {
    console.log("param name value: ", args.name);
    const arr = new Int8Array(buffer);
    arr[0] += 1;
    return buffer;
  }

  public fib(
    buffer: SharedArrayBuffer,
    module: Record<string, any>,
  ): SharedArrayBuffer {
    let i;
    const arr = new Uint8Array(buffer);
    arr[0] = 0;
    arr[1] = 1;

    for (i = 2; i <= module.count; i++) {
      arr[i] = arr[i - 2] + arr[i - 1];
      console.log(arr[i]);
    }
    
    return buffer;
  }
}
const example: Example = new Example();

const wrapper: InstanceWrapper<Example> = new InstanceWrapper<Example>(
  example,
  {
    outputPath: "/path/to/gen/output"
  } as InstanceConfiguration,
);

wrapper.create();
import { foo } from "<path/to/bridge.js>";
await foo().then(() => {
  console.log("bar");
});

Usage in process (uses example from above)

const example: WorkerDefinition = new Example();

const wrapper: InstanceWrapper<Example> = new InstanceWrapper<Example>(example, {
    outputPath: 'output'
});

wrapper.start();
await example.execute('fib' {count: 10}).then((buffer: SharedArrayBuffer) => {
  console.log("final fib number", new Uint8Array(buffer)[10]);
});

example.terminateWorker();

Example JS With WASM

The below example uses a WASM module compiled from Golang using tiny-go. Below we provide the go WASM runtime as an addon and give a callback for loading the module at the given file path.

import { WasmInstanceWrapper, WasmWorkerDefinition } from "./../../src/mod.ts";
class Example extends WasmWorkerDefinition {

    public constructor(modulePath: string) {
        super(modulePath);
    }

    public test(buffer: SharedArrayBuffer, module: any) {
        let arr = new Int8Array(buffer);
        arr[0] += 1
        //@ts-ignore
        self.primeGenerator()
        return arr.buffer
    }
}

const example: Example = new Example("./examples/wasm/primes-2.wasm");

const wrapper: WasmInstanceWrapper<Example> = new WasmInstanceWrapper<Example>(example, {
     addons: [
      "./lib/wasm_exec_tiny.js",
    ],
    addonLoader: (path: string) => {
      return Deno.readTextFileSync(path);
    },
    moduleLoader: (path: string) => {
      const fd = Deno.openSync(path);
      return Deno.readAllSync(fd);
    },
});

wrapper.start();
//@ts-ignore
await example.execute("test").then((buf: SharedArrayBuffer) => {
    console.log("buffer returned ", new Int32Array(buf))
});

example.terminateWorker();