moxzi
Docs / Browser / The client API

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#

ExportFromWhat it is
Moxzimoxzi-webThe runtime handle; Moxzi.start(...) is the entry point
ActorErrormoxzi-webWhat a canister answered with when it rejected, trapped, or overran its deadline. Carries .canister (hex id) and .method
idFromName(name)moxzi-webA stable ten-byte principal derived from a name (FNV-1a, IC opaque-id suffix)
fetchMopsClosure(base, entry, token?)moxzi-webA Motoko entry file's full dependency closure from a moxzid --project, as { entry, pkgSource, unresolved, files: [{ vfs, bytes }] }
indexedDbStore(name?, storeName?)moxzi-web/storageA {get,set,delete,keys} store backed by IndexedDB
memoryStore()moxzi-web/storageThe same interface over a Map, for tests
attachDevtools(moxzi, { open })moxzi-web/devtoolsInjects the πŸ”Ž badge and panel; returns { detach, refresh, snapshot }

Starting#

OptionDefaultMeaning
workerfalseHost 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
workerUrlpackaged workerOverride the worker URL (the raw-asset arrangement)
httpfalseHTTPS outcalls. Worker only; needs cross-origin isolation
deadlineMs0Kill any message exceeding this wall-clock budget. Worker only
timerMs0Poll due timers on this interval, instead of calling tick() yourself
storenullA store for persistence
autosavefalseSnapshot and write after every successful update

The calls#

CallSignatureReturnsPage vs worker
Moxzi.startstart(options)Promise<Moxzi>; sets the actors' clock from Date.now()The only call whose options differ
installinstall({ name | id, wasm, arg, initTypes, idlFactory })An actor. wasm may be bytes or a URLSame
upgradeupgrade({ name | id, wasm, arg, initTypes, idlFactory })An actor running the new code, state keptSame
actoractor(idOrName, idlFactory?) β€” synchronousA callable actor for something already installed or restoredSame
logslogs(idOrName)The actor's prints, taken and clearedSame
historyhistory(idOrName)The bounded per-actor ring: seq, time_ns, kind, method, caller, arg_len, arg_preview, ok, error, reply_lenSame
candidOfcandidOf(idOrName)The candid:service text of the module the runtime is executing, or nullSame
savesave(idOrName)One actor's snapshot bytes, in the native host's layoutSame
restorerestore(idOrName, state)β€”Same
persistpersist(id?)β€” . Throws without a store; with no argument, everything installedSame
forgetforget(idOrName)β€” . Drops the stored copy so the next install starts freshSame
saveAllsaveAll()The whole runtime as one blob, spawned actors includedSame
loadAllloadAll(blob?)How many canisters came back. Lifecycle hooks are not re-runSame
ticktick(nowNs?)How many timers firedSame
withDeadlinewithDeadline(ms, fn)Whatever fn returnsWorker only in effect
recoverrecover()β€” . Kills the worker, re-boots it, reinstalls and restores every actorWorker only β€” the page transport has no kill
canPreemptgetterfalse in page mode, true in a workerThe difference itself
stopstop()β€” . Clears the timer interval and terminates the workerSame

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#

On this pageModule exportsStartingThe callsOne program, two transportsWhat only a worker can doPersistence and spawned actorsDevtoolsNext