userland.rundocs70Try the demo
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 },
);
OptionTypeDefaultDescription
configNanoConfig—The same configuration as createNano().
workerFactory() => Workerthe 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.transferBuffersbooleanfalseTransfer the config's ArrayBuffers to the worker instead of copying them. Saves memory, but detaches your buffers.
SDK · JavaScript

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();
loading…
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
SDK · JavaScript

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));
loading…
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, destroy
stdin
setInteractiveStdin, writeStdin, closeStdin (see interactive stdin)
Files
client.fs: writeFile, readText, readFile, list, exists, walk, mkdir, remove, move, copy, loadTarGz
Shell
client.shell(opts) returns a regular Shell
Node
client.nodeRuntime() with async warmup, run, reset
Scripting
client.scripting(opts), client.script(src); registerFunction callbacks run on the main thread
Serve
the client is a ConnectionInjector, so pass it to ServeBridge.register({ injector: client })
Catalog (0.2.7+)
installApp, installBundle (with onProgress proxied), execWasmApp, resolveNodeEngine
Snapshots
setWarmup, warmup, loadSnapshot, restoreRun, restoreServe (used by provision())
SDK · JavaScript

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();
loading…
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.

SDK · JavaScript

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();
loading…
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 read would otherwise freeze the page.
  • Large images: transferBuffers avoids 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.