Browser and Node.js
An ES module that uses @vertep/web, the package a browser loads. The actor system runs in a worker, a worker thread in Node.js, with the core compiled to WebAssembly.
Files: examples/browser-js/main.mjs, examples/browser-js/package.json, examples/browser-js/expected-output.txt. All commands run from the repository root.
The program
It spawns a cart actor from SCXML text with in-memory storage, subscribes to snapshots, sends events as { type, ...payload }, reads the journal with the transitions each entry took, and reads the state as of an earlier entry.
// Browser JS: a Vertep actor system from JavaScript, run here with Node.js.
//
// `@vertep/web` starts the actor system in a worker (a Web Worker in a browser, a worker
// thread in Node.js): the Vertep core compiled to WebAssembly, a shell on the JavaScript event
// loop, and a journal. This main thread only holds a thin typed client; every call returns a
// Promise. The same file runs in a browser page unchanged, with `storage: "opfs"` for a durable
// journal instead of `"memory"`.
//
// Illustrates the wasm-bindings capability (Worker-Hosted Actor System, Actor Creation From
// Chart Sources, Event Sending From JavaScript, Snapshot And Transition Subscriptions, Journal
// Inspection And Time Travel, Node.js Support).
//
// Run with `npm install && npm start` in this directory, after building the JavaScript packages
// (see examples/README.md).
import { createActorSystem } from "@vertep/web";
// A cart on the CEL datamodel: `add` carries a quantity, `checkout` needs a non-empty cart.
const CART = `<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
datamodel="cel" name="cart" initial="empty">
<datamodel><data id="items" expr="0"/></datamodel>
<state id="empty">
<transition event="add" target="filled"><assign location="items" expr="ctx.items + ev.data.qty"/></transition>
</state>
<state id="filled">
<transition event="add"><assign location="items" expr="ctx.items + ev.data.qty"/></transition>
<transition event="checkout" cond="ctx.items > 0" target="ordered"/>
</state>
<state id="ordered"/>
</scxml>`;
const system = await createActorSystem({ storage: "memory" });
try {
// The chart is validated and content-addressed exactly as natively. Registered under a type
// IRI and spawned with a key, the actor's address is `<type>/<key>`.
const cart = await system.spawn(CART, { type: "https://example.org/charts/cart", key: "alice" });
console.log("actor", await cart.address);
// One snapshot per macrostep that changes the configuration or the context.
const seen = [];
const stop = cart.subscribe((s) => {
seen.push(s.position);
console.log(`snapshot @${s.position}: ${JSON.stringify(s.configuration)} items=${s.context.items}`);
});
// An event is its name as `type` plus its payload's fields.
await cart.send({ type: "add", qty: 2 });
await cart.send({ type: "add", qty: 1 });
await cart.send({ type: "pay" }); // no transition takes it: journaled, no new snapshot
await cart.send({ type: "checkout" });
await until(() => seen.includes(4));
stop();
const now = await cart.getSnapshot();
console.log("now:", JSON.stringify(now.configuration), "matches('ordered') =", now.matches("ordered"));
// Every accepted event is a journal entry, with the transitions it took. Transitions are
// IRIs inside the chart version: `<version>#s.<state>:t<index>`.
console.log("journal:");
for (const e of await cart.journal()) {
const taken = (e.transitions ?? []).map((t) => t.split("#")[1]);
console.log(` ${e.position} ${e.event.name.padEnd(8)} ${JSON.stringify(e.event.data).padEnd(9)} -> ${JSON.stringify(taken)}`);
}
// Time travel: the state after entry 1, rebuilt from the journal.
const then = await cart.asOf({ position: 1 });
console.log("as of entry 1:", JSON.stringify(then.configuration), "items =", then.context.items);
} finally {
await system.terminate();
}
/** Waits until `ready()` holds (subscriptions are delivered asynchronously). */
async function until(ready, timeoutMs = 5000) {
const deadline = Date.now() + timeoutMs;
while (!ready()) {
if (Date.now() > deadline) throw new Error("timed out waiting for snapshots");
await new Promise((r) => setTimeout(r, 10));
}
}
Build once, then run
In your own project, npm install @vertep/web is all you need. This example runs against the checkout instead: its package.json links @vertep/web to the local build, so build the packages once first.
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli --version 0.2.128
cd bindings/js && npm install && npm run build
cd ../../examples/browser-js
npm install # links the local @vertep/web; no download
npm start
actor https://example.org/charts/cart/alice
snapshot @1: ["filled"] items=2
snapshot @2: ["filled"] items=3
snapshot @4: ["ordered"] items=3
now: ["ordered"] matches('ordered') = true
journal:
1 add {"qty":2} -> ["s.empty:t0"]
2 add {"qty":1} -> ["s.filled:t0"]
3 pay null -> []
4 checkout null -> ["s.filled:t1"]
as of entry 1: ["filled"] items = 2
- An event no transition takes, here
pay, is still journaled but produces no snapshot. subscribedoes not deliver the current snapshot, and notifications arrive asynchronously.- In a browser, pass
storage: "opfs"for a durable journal in the origin private file system.