SDK
Web Worker
createNanoWorker() hosts the VM in a Web Worker, so long runs never block the page. The client mirrors Nano with async methods, works with Shell and ServeBridge, and proxies callbacks back to the main thread.
javascript
import { createNanoWorker } from "@userland-run/nano-sdk";
const client = await createNanoWorker(
{ image: { wasm: "/nano/console.wasm" }, ramMB: 512 },
() => new Worker(new URL("@userland-run/nano-sdk/worker", import.meta.url), { type: "module" }),
{ transferBuffers: false },
);| Option | Type | Default | Description |
|---|---|---|---|
config | NanoConfig | — | The same configuration as createNano(). |
workerFactory | () => Worker | the SDK's `./worker/worker.js` | Create the worker yourself. With a bundler, pass a factory using new URL("@userland-run/nano-sdk/worker", import.meta.url); the default only resolves where the bundler rewrites the SDK's own new URL("./worker/worker.js", import.meta.url) (webpack does, which is what this site uses). |
opts.transferBuffers | boolean | false | Transfer the config's ArrayBuffers to the worker instead of copying them. Saves memory, but detaches your buffers. |
Commands and files over the worker
Every call is a message round-trip; onData chunks are proxied back as they arrive. client.fs has the Vfs methods, all async.
javascript
const client = await sdk.createNanoWorker({ image: { wasm: "/nano/console.wasm" }, ramMB: 256 });
const r = await client.shExec("echo hi from the worker; uname -m", { onData: (c) => print("[data]", c.trim()) });
print("exit", r.exitCode);
await client.fs.writeFile("/work/a.txt", "abc");
await client.fs.mkdir("/work/d/e");
await client.fs.copy("/work/a.txt", "/work/d/e/b.txt");
print(await client.fs.readText("/work/a.txt"), JSON.stringify(await client.fs.walk("/work")));
const sh = client.shell({ cwd: "/work" });
await sh.run("cd d/e");
print("shell cwd:", sh.cwd, (await sh.run("ls")).stdout.trim());
client.destroy();Expected output
[data] hi from the worker [data] riscv64 [data] exit 0 abc ["/work/a.txt","/work/d/e/b.txt"] shell cwd: /work/d/e b.txt
transferBuffers and destroy()
javascript
const wasm = await (await fetch("/nano/console.wasm")).arrayBuffer();
print("before:", wasm.byteLength > 0);
const client = await sdk.createNanoWorker({ image: { wasm }, ramMB: 256, warmup: false }, undefined, { transferBuffers: true });
print("after:", wasm.byteLength, "(detached)");
print((await client.run("echo transferred")).stdout.trim());
client.destroy();
print(await client.run("echo x").catch((e) => e.message));Expected output
before: true after: 0 (detached) transferred nano-sdk: worker client destroyed
The client surface
Area
Members
Commands
run, exec, shExec, node, readText, cancel, destroystdin
Files
client.fs: writeFile, readText, readFile, list, exists, walk, mkdir, remove, move, copy, loadTarGzShell
client.shell(opts) returns a regular ShellNode
client.nodeRuntime() with async warmup, run, resetScripting
client.scripting(opts), client.script(src); registerFunction callbacks run on the main threadServe
the client is a
ConnectionInjector, so pass it to ServeBridge.register({ injector: client })Catalog (0.2.7+)
installApp, installBundle (with onProgress proxied), execWasmApp, resolveNodeEngineSnapshots
Scripting in the worker
javascript
const client = await sdk.createNanoWorker({ image: { wasm: "/nano/console.wasm" }, ramMB: 256, scripting: { wasm: "/nano/boa.wasm" } });
const engine = await client.scripting({ expose: { run: true }, onStdout: (d) => print("[script]", d.trim()) });
engine.registerFunction("double", (x) => x * 2); // runs on the main thread
print(await engine.eval('(async () => { console.log("in the worker"); const r = await nano.run("uname -m"); return r.stdout.trim() + " " + (await double(21)); })()'));
engine.dispose();
client.destroy();Expected output
[script] in the worker riscv64 42
Typed catalog wrappers
Since SDK 0.2.7 the worker client has installApp, installBundle, execWasmApp and resolveNodeEngine. The installer runs in the worker next to the VM; onProgress events are proxied to the main thread.
installApp and execWasmApp over the worker
javascript
const client = await sdk.createNanoWorker({ image: { wasm: "/nano/console.wasm" }, ramMB: 256 });
const phases = new Set();
const m = await client.installApp("hexyl", { onProgress: (e) => phases.add(e.phase) });
print("installed", m.name, m.version, "| phases:", [...phases].join(" → "));
print((await client.shExec("echo hi | hexyl --border none --color never")).stdout.trim());
print("node engine for app.js:", await client.resolveNodeEngine(["app.js"]));
client.destroy();Expected output
installed hexyl 0.14.0 | phases: index → manifest → chunk → write → done 00000000 68 69 0a hi_ node engine for app.js: vm
When to use the worker
- Interactive UIs: long runs, Node boots and servers keep the page responsive.
- Interactive stdin: a blocking
readwould otherwise freeze the page. - Large images:
transferBuffersavoids a second copy of the wasm bytes.
The worker client works with provision(), which always uses it. The react-studio example runs a dev server this way.