@nanobpm/engine-wasm
v0.8.1
Published
The nanobpmn engine (engine-core) compiled to WebAssembly for in-browser BPMN execution: deploy a diagram, start instances, activate/complete jobs, and read live snapshots/traces. Published as two subpath entrypoints — a lean baseline engine (default) and
Readme
@nanobpm/engine-wasm
The nanobpmn engine (engine-core) compiled to WebAssembly for in-browser
BPMN execution. Deploy a diagram, start instances, activate/complete jobs, drive
the virtual clock, and read live snapshots/traces — all client-side, with no
gateway. It is the substrate for the Bojtos
demo framework (ADR 0043) and the console test-run panel.
The package ships two independent engines from one install, selectable by import path:
| Import | Engine | Wire size (brotli) | Use it for |
| --- | --- | ---: | --- |
| @nanobpm/engine-wasm | lean — primary state only | ~252 KB | demos, the modeler, debuggers, anything that reads via snapshot() / events() |
| @nanobpm/engine-wasm/readmodel | read-model — lean surface + the gateway's C8-style REST read methods | ~561 KB | full-surface app testing in CI that wants gateway read parity |
| @nanobpm/engine-wasm/readmodel-types | types only — the DTOs the read-model search* / get*ByKey methods return | 0 KB (erased) | typing read-model query results in a consumer (e.g. @nanobpm/bojtos-kit) without hand-mirroring the shapes |
Why two binaries (not a runtime toggle)
Each variant is its own wasm-pack --target web JS glue + _bg.wasm. A bundler
follows the static module graph, so it only emits the wasm for the subpath you
actually import — wasm cannot be tree-shaken out of a single fat build. So:
- Lean-only pages pay zero read-model weight (the +309 KB brotli never lands).
- You can
await import("@nanobpm/engine-wasm/readmodel")to code-split the heavy engine behind a runtime decision. - You can even instantiate both on one page.
The read-model variant is the lean engine plus an in-memory wasm SQLite read model (the same projection the gateway serves from), exposing the REST read surface the lean engine lacks.
Install
npm install @nanobpm/engine-wasmUsage — lean (default)
import init, { TestEngine } from "@nanobpm/engine-wasm";
await init(); // instantiate the wasm module
const engine = new TestEngine();
engine.deploy(bpmnXml); // deploy a BPMN diagram
const handle = engine.createInstance("my-process", "{}");
const snap = JSON.parse(engine.snapshot()); // live primary state
const trace = JSON.parse(engine.events()); // the event log
engine.reset(); // back to a clean engineThe lean surface covers primary state and execution: deploy, createInstance,
snapshot, events, job ops (activateJobs, completeJob, failJob,
updateRetries, throwError), user-task ops (completeUserTask,
assignUserTask, …), messaging/signals, the virtual clock (advanceTime,
tickNow), and reset. It links no SQLite / read-model code.
Usage — read-model
import init, { TestEngine } from "@nanobpm/engine-wasm/readmodel";
await init();
const engine = new TestEngine();
engine.deploy(bpmnXml);
engine.createInstance("my-process", "{}");
// Same lean surface as above, PLUS the gateway's REST read channel:
const form = JSON.parse(engine.getFormByKey("2251799813685250"));
const open = JSON.parse(engine.searchUserTasks(JSON.stringify({ state: "CREATED" })));
const insts = JSON.parse(engine.searchProcessInstances("{}"));
const res = JSON.parse(engine.getResourceByKey("2251799813685251"));
const vars = JSON.parse(engine.searchVariables("{}"));Read methods (read-model subpath only)
Each returns the same JSON shapes the Camunda v2 REST surface returns; each
delegates to the in-memory read model, kept current after every command and
cleared by reset().
getFormByKey(formKey)→ the latest deployed form forformKey({ tenantId, formId, schema, version, formKey }), ornull. MirrorsGET /forms/{formKey}.searchUserTasks(filterJson)→{ items, page }. Honours an optional{ state? }filter (e.g."CREATED") through the read model. MirrorsPOST /user-tasks/search.searchProcessInstances(filterJson)→{ items, page }. Body is shape-validated; filter/sort/page fields are not yet honoured (returns every instance). MirrorsPOST /process-instances/search.getResourceByKey(resourceKey)→ the generic resource, ornull. MirrorsGET /resources/{resourceKey}.searchVariables(filterJson)→{ items, page }. Body is shape-validated; long values are truncated withisTruncated: true. MirrorsPOST /variables/search.
Typing the results — @nanobpm/engine-wasm/readmodel-types
The methods above hand off opaque JSON strings (the wasm boundary is strings), so
the subpath @nanobpm/engine-wasm/readmodel-types ships the DTO types for
those results — UserTaskSearchQueryResult, ProcessInstanceSearchQueryResult,
VariableSearchQueryResult, FormResult, ResourceResult — so a consumer can
type them without hand-mirroring the shapes:
import type { UserTaskSearchQueryResult } from "@nanobpm/engine-wasm/readmodel-types";
const open = JSON.parse(
engine.searchUserTasks(JSON.stringify({ state: "CREATED" })),
) as UserTaskSearchQueryResult;These are derived from the single source of truth — the Camunda-parity REST
OpenAPI in spec/ — by engine-wasm/readmodel-types (@hey-api/openapi-ts); a
CI drift guard regenerates and fails on any stale artifact. The subpath is
types-only (its runtime module is empty), so importing it adds zero wire weight.
Choosing a subpath
- Reach for lean for demos, the modeler, debuggers/adapters, and anything
that only inspects
snapshot()/events(). Keeping these paths lean is the whole point of the split — the +309 KB brotli must never ship in a demo page. - Reach for
/readmodelwhen you need gateway read parity — full-surface app testing in CI — instead of hand-rolling shadow read stores in JS/TS.
References
- Epic #796 — the read channel via a wasm SQLite read model (size table, toolchain note, downstream consumers).
- ADR 0043 — Bojtos (the demo framework this engine backs).
