The client APIalpha
What are the calls, and how do page mode and worker mode differ?
moxzi/web/lib/moxzi.js is the layer between a page and the runtime, and its whole surface is async β even where the work is synchronous. That is deliberate: actors can be hosted in the page's thread or in a worker, and moving between the two must be a change to one option rather than a rewrite of every call site.
Module exports#
| Export | From | What it is |
|---|---|---|
Moxzi | moxzi-web | The runtime handle; Moxzi.start(...) is the entry point |
ActorError | moxzi-web | What a canister answered with when it rejected, trapped, or overran its deadline. Carries .canister (hex id) and .method |
idFromName(name) | moxzi-web | A stable ten-byte principal derived from a name (FNV-1a, IC opaque-id suffix) |
fetchMopsClosure(base, entry, token?) | moxzi-web | A Motoko entry file's full dependency closure from a moxzid --project, as { entry, pkgSource, unresolved, files: [{ vfs, bytes }] } |
indexedDbStore(name?, storeName?) | moxzi-web/storage | A {get,set,delete,keys} store backed by IndexedDB |
memoryStore() | moxzi-web/storage | The same interface over a Map, for tests |
attachDevtools(moxzi, { open }) | moxzi-web/devtools | Injects the π badge and panel; returns { detach, refresh, snapshot } |
Starting#
| Option | Default | Meaning |
|---|---|---|
worker | false | Host the runtime in a module worker instead of this thread |
glue | β | The wasm-bindgen module (import * as glue from 'moxzi-web/runtime'). Required in page mode, refused in worker mode |
workerUrl | packaged worker | Override the worker URL (the raw-asset arrangement) |
http | false | HTTPS outcalls. Worker only; needs cross-origin isolation |
deadlineMs | 0 | Kill any message exceeding this wall-clock budget. Worker only |
timerMs | 0 | Poll due timers on this interval, instead of calling tick() yourself |
store | null | A store for persistence |
autosave | false | Snapshot and write after every successful update |
The calls#
| Call | Signature | Returns | Page vs worker |
|---|---|---|---|
Moxzi.start | start(options) | Promise<Moxzi>; sets the actors' clock from Date.now() | The only call whose options differ |
install | install({ name | id, wasm, arg, initTypes, idlFactory }) | An actor. wasm may be bytes or a URL | Same |
upgrade | upgrade({ name | id, wasm, arg, initTypes, idlFactory }) | An actor running the new code, state kept | Same |
actor | actor(idOrName, idlFactory?) β synchronous | A callable actor for something already installed or restored | Same |
logs | logs(idOrName) | The actor's prints, taken and cleared | Same |
history | history(idOrName) | The bounded per-actor ring: seq, time_ns, kind, method, caller, arg_len, arg_preview, ok, error, reply_len | Same |
candidOf | candidOf(idOrName) | The candid:service text of the module the runtime is executing, or null | Same |
save | save(idOrName) | One actor's snapshot bytes, in the native host's layout | Same |
restore | restore(idOrName, state) | β | Same |
persist | persist(id?) | β . Throws without a store; with no argument, everything installed | Same |
forget | forget(idOrName) | β . Drops the stored copy so the next install starts fresh | Same |
saveAll | saveAll() | The whole runtime as one blob, spawned actors included | Same |
loadAll | loadAll(blob?) | How many canisters came back. Lifecycle hooks are not re-run | Same |
tick | tick(nowNs?) | How many timers fired | Same |
withDeadline | withDeadline(ms, fn) | Whatever fn returns | Worker only in effect |
recover | recover() | β . Kills the worker, re-boots it, reinstalls and restores every actor | Worker only β the page transport has no kill |
canPreempt | getter | false in page mode, true in a worker | The difference itself |
stop | stop() | β . Clears the timer interval and terminates the worker | Same |
An actor object carries the interface methods from its idlFactory, plus four things that are always there: id (the ten-byte Uint8Array), callRaw(method, argBytes), queryRaw(method, argBytes) and logs(). Without an idlFactory you get only those β see /docs/web/candid/.
One program, two transports#
This is the shape scripts/web_lib_gate.sh runs twice β once in the page, once in a worker β and asserts gives the same answers:
async function exercise(moxzi, tag) {
const greeter = await moxzi.install({ name: 'greeter', wasm: './greet.wasm', idlFactory });
const hello = await greeter.greet('world');
await greeter.greet('ada');
const n = await greeter.visitors();
return { hello, n: String(n) };
}
const page = await Moxzi.start({ glue }); // canPreempt false
const worker = await Moxzi.start({ worker: true, deadlineMs: 2000 }); // canPreempt true
What only a worker can do#
const worker = await Moxzi.start({ worker: true, deadlineMs: 2000 });
const spinner = await worker.install({ name: 'spinner', wasm: './spin.wasm', idlFactory: spinIdl });
try { await spinner.spin(); }
catch (e) { e instanceof ActorError; } // "spin exceeded its 2000ms deadline and was stopped"
The deadline is a Promise.race against the call; when it wins, recover() runs, the worker is terminated and rebuilt, and every actor is reinstalled and restored from its last committed snapshot β including actors that had nothing to do with the runaway. Recovery is single-flight: several calls in flight when one overruns all land in the same rebuild rather than each starting their own.
withDeadline scopes a different budget to the calls a callback makes β the knights demo uses it because an LLM outcall legitimately takes tens of seconds inside a runtime whose game ticks should die in three:
const r = await moxzi.withDeadline(180_000, () =>
world.improveKnight(BigInt(kid), api.base, api.key, api.model, prefix));
It is scoped, not concurrent: calls made elsewhere while fn is in flight see the temporary deadline too, so keep the callback's work sequential.
Persistence and spawned actors#
persist() covers what the page installed. Actors created by actors go through the management canister, so their code never crossed into the page and only the runtime knows they exist β that is what saveAll/loadAll are for. From the knights demo and the library gate:
const blob = await moxzi.saveAll(); // every canister, spawned ones included
// β¦ a reload, or a fresh runtime β¦
const n = await reborn.loadAll(blob); // how many came back
const back = reborn.actor('nursery', nurseryIdl);
install is also the reload path: with a store, an actor whose state is already saved is restored instead of initialised. Installing a different module over stored state is refused by module hash, naming upgrade() as the call to make instead.
Devtools#
import { attachDevtools } from 'moxzi-web/devtools';
window.__devtools = attachDevtools(moxzi); // π badge, bottom right
The panel lists every canister in the runtime, spawned ones included, renders each interface from candidOf(id) as callable forms, and takes logs on click. detach() removes it.
Next#
- /docs/web/candid/ β argument encoding, and why an untyped reply shows numbers instead of field names.
- /docs/web/install/ β package exports, the worker/bundler rule, COOP/COEP.
- /docs/web/limits/ β where this stops matching the IC.