moxzi
Docs / Browser / Getting started with moxzi-web

Getting started with moxzi-webalpha

Add moxzi-web to a Vite app, run an actor in the page, then in a worker, and save its state.

This tutorial runs the counter actor from Your first actor in a web page. You need moxzi, Node.js 18 or later, and didc (from the Candid releases) to generate JavaScript types.

1. Build the actor#

Put counter.mo in a new directory, build it into public/, and generate its Candid bindings:

counter.mo
actor {
  var count : Nat = 0;

  public func inc() : async Nat {
    count += 1;
    count
  };

  public query func get() : async Nat {
    count
  };
};
moxzi build counter.mo -o public/counter.wasm
didc bind -t js public/counter.did > counter.did.js
counter.mo -> public/counter.wasm + public/counter.did (245600 bytes, 1 file(s), 50 steps, 1.08s)
compiled by ~/.local/lib/moxzi/compiler.wasm (sha256 9294817c6e9069cbd0e712d0e9dde0ec1b6893d0e7b42d798fa3dd4c8cd772cb), moxzi 0.1.0-alpha.10

counter.did.js exports an idlFactory describing the actor's methods. If you use dfx, dfx generate writes the same file.

2. Create the app#

package.json
{
  "name": "web-counter",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "dependencies": {
    "@dfinity/candid": "^2.4.1",
    "@dfinity/principal": "^2.4.1",
    "moxzi-web": "https://moxzi.ai/releases/v0.1.0-alpha.7/moxzi-web-0.1.0-alpha.7.tgz"
  },
  "devDependencies": {
    "vite": "^6.0.0"
  }
}

moxzi-web is installed from moxzi.ai. The version on npm is older than this release. @dfinity/candid and @dfinity/principal encode and decode arguments.

vite.config.js
export default {
  build: {
    target: 'esnext',
    rollupOptions: { input: { main: 'index.html', worker: 'worker.html', persist: 'persist.html', candid: 'candid.html' } },
  },
};

moxzi-web uses top-level await and module workers, so the build target must be esnext. input lists the three pages this tutorial builds; with a single index.html you can leave it out.

index.html
<!doctype html>
<html>
  <body>
    <p id="out">loading…</p>
    <script type="module" src="/main.js"></script>
  </body>
</html>
main.js
import * as glue from 'moxzi-web/runtime';
import { Moxzi } from 'moxzi-web';
import { idlFactory } from './counter.did.js';

const out = document.querySelector('#out');

// Start the runtime in this page, and install the actor.
const moxzi = await Moxzi.start({ glue });
const counter = await moxzi.install({ name: 'counter', wasm: '/counter.wasm', idlFactory });

// Methods come from the Candid interface. Nat values are BigInts.
await counter.inc();
await counter.inc();
out.textContent = `count = ${await counter.get()}`;

3. Run it#

npm install
npx vite build --logLevel silent
npx vite preview    # then open the URL it prints

The page shows:

count = 2

The actor's methods are called like JavaScript functions. Candid nat values come back as BigInts.

4. Move the actor to a worker#

In page mode a long-running message blocks the page. In worker mode the actors run in a Web Worker, and deadlineMs stops any message that runs too long. This page also saves the whole runtime and loads it into a new one:

worker.html
<!doctype html>
<html>
  <body>
    <p id="out">loading…</p>
    <script type="module" src="/worker-main.js"></script>
  </body>
</html>
worker-main.js
import { Moxzi } from 'moxzi-web';
import { idlFactory } from './counter.did.js';

const out = document.querySelector('#out');

// Run the actors in a worker, and stop any message that takes longer than 5 seconds.
const first = await Moxzi.start({ worker: true, deadlineMs: 5000 });
const counter = await first.install({ name: 'counter', wasm: '/counter.wasm', idlFactory });
await counter.inc();
await counter.inc();
await counter.inc();

// Save the whole runtime, then load it into a new one.
const saved = await first.saveAll();
first.stop();

const second = await Moxzi.start({ worker: true });
await second.loadAll(saved);
const restored = second.actor('counter', idlFactory);
out.textContent = `restored count = ${await restored.get()}`;

Opening worker.html shows:

restored count = 3

Worker mode doesn't take glue; the worker loads its own copy of the runtime.

5. Keep state across reloads#

Pass a store to keep actors in IndexedDB. With autosave, state is saved after every update, and install restores a saved actor instead of starting it fresh:

persist.html
<!doctype html>
<html>
  <body>
    <p id="out">loading…</p>
    <script type="module" src="/persist-main.js"></script>
  </body>
</html>
persist-main.js
import { Moxzi } from 'moxzi-web';
import { indexedDbStore } from 'moxzi-web/storage';
import { idlFactory } from './counter.did.js';

const moxzi = await Moxzi.start({
  worker: true,
  store: await indexedDbStore('counter-demo'),
  autosave: true,
});

// On the first visit this installs the actor. After that it restores the saved one.
const counter = await moxzi.install({ name: 'counter', wasm: '/counter.wasm', idlFactory });
const count = await counter.inc();
document.querySelector('#out').textContent = `visits = ${count}`;

Opening persist.html and then reloading it shows:

visits = 1
visits = 2

Installing a different module over saved state is refused. Use moxzi.upgrade(…) to change an actor's code and keep its state.

Bundlers#

Vite and webpack detect the worker from this exact expression inside moxzi-web:

new Worker(new URL('./moxzi-worker.js', import.meta.url), { type: 'module' })

If your build tool doesn't handle it, worker mode hangs without an error. In that case, copy node_modules/moxzi-web/moxzi-worker.js, moxzi_web.js and moxzi_web_bg.wasm into your static files, and pass the worker's URL:

const moxzi = await Moxzi.start({ worker: true, workerUrl: '/moxzi/moxzi-worker.js' });

HTTPS outcalls#

For actors that make HTTPS requests (including ic-agent), start with { worker: true, http: true } and serve the page with these headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Without them Moxzi.start fails with a message naming the headers. require-corp also applies to every other resource the page loads.

The worker reads response streams incrementally and cancels a response as soon as it crosses the smaller of the actor's max_response_bytes value and the 4 MiB shared-channel capacity. Because Fetch decodes supported content encodings, the browser limit measures decoded bytes. The native runtime leaves content encoding opaque and counts the encoded bytes it returns. Each worker generation owns a fresh shared channel and aborts its fetches before recovery or stop, so a late response cannot answer a later actor call. httpTimeoutMs is a host transport timeout for connection and streamed-body work; it defaults to 30 seconds and must be a positive integer.

Next steps#

On this page1. Build the actor2. Create the app3. Run it4. Move the actor to a worker5. Keep state across reloadsBundlersHTTPS outcallsNext steps