Module and Handles#
This guide covers the runtime side of the raw WebAssembly surface: which build to load in which host, how to get the module ready, how long a project lives in wasm memory and how to release it, and what each call costs the calling thread. It's for hosts that call JsonProject and schedule directly instead of going through the JavaScript glue.
How the module works#
The package builds two artifacts from one crate. Both expose the same two exports, and both speak the same JSON:
JsonProject, a class. Each instance is a handle: a small JS object that points at a project stored in the module's linear memory. The project keeps its accepted baseline, its pending edits and its cached result between calls.schedule, a function. It builds a default project, solves the snapshot you pass, returns the result and drops the project. There's nothing to free.
Every call is a plain synchronous function call: JSON text goes in, JSON envelope text comes out. Only one step is asynchronous, and only in the browser build: fetching and instantiating the .wasm file.
The JS garbage collector can't see wasm memory. A handle object is tiny on the JS side, but the project behind it can be large, so the GC has no reason to hurry. That's why handles are freed explicitly. wasm-bindgen registers each handle with a FinalizationRegistry that frees leaked handles eventually, but treat it as a safety net.
A handle belongs to the module instance that created it. If you load the module in two workers, each worker has its own instance and its own memory. Handles can't move between them; send the JSON instead.
Envelopes and payload conventions are on Wire format. Error kinds are on Error handling.
Build targets#
| Target | Folder | Module format | Loading |
|---|---|---|---|
| Browser, bundlers, any ES module host | pkg/ |
ES module | async init() or sync initSync() |
| Node.js | pkg-node/ |
CommonJS | instantiated synchronously on require |
npm run build in crates/plen-wasm builds both. From the workspace root, task build:wasm builds pkg/ and task build:wasm:node builds pkg-node/.
The package's exports map exposes only the glue ("." and "./glue"). The pkg/ and pkg-node/ files ship in the package, but you load them by path:
// Browser / ES module host
// Node.js
const = ;
Instantiation#
Loading the module in the browser#
Call the default export init() once and await it before the first new JsonProject or schedule call. Without an argument, it fetches plen_wasm_bg.wasm from the same folder as plen_wasm.js.
await ;
const = ;
const = ;
init() rejects when the file can't be fetched or instantiated. A second call returns the instance that's already loaded. Calling an export before init() resolves fails.
Loading the .wasm file from another URL#
Pass the location as { module_or_path }. It accepts a URL, a Request, a Response, the bytes, a compiled WebAssembly.Module, or a promise of any of these.
await ;
Passing the value directly, without the { module_or_path } wrapper, still works but is deprecated by wasm-bindgen and logs a warning.
Instantiating synchronously from bytes#
When you already hold the bytes or a compiled module, for example one compiled once on the main thread and sent to a worker, initSync instantiates without a promise:
// `wasmModule` is a WebAssembly.Module received from the main thread
;
const = new;
Loading in Node.js#
The Node build reads plen_wasm_bg.wasm from its own folder and instantiates it while require runs. There's no init step:
const = ;
const = ;
const = ;
;
Full signatures: init.
Creating a project#
Creating a handle with default options#
The constructor takes ProjectOptions as JSON text. Send "{}" for strict validation:
const = new;
Creating a handle that repairs messy input#
const = new;
With "repair", the project fixes duplicate ids and missing or inconsistent dates, and reports each fix as a repaired note. See Accept messy input.
Catching constructor errors#
The constructor is the only export that throws on bad input. What it throws is a string, not an Error: the JSON text of an error envelope. Parse it like any other envelope:
let ;
Invalid JSON, an unknown field and a validation value other than "strict" or "repair" all end up here. Every other export reports input errors in the returned envelope. Details: JsonProject, Error handling.
Choosing between a handle and schedule#
Use schedule when every request carries the whole snapshot: batch jobs, request handlers, a loop that reschedules everything on each gesture. Use a JsonProject when the host keeps a project open and sends small edits (apply, preview), or when it needs the baseline-based calls (level, loadProfile, evaluate, slots, suggestAssignments).
const = new;
const = ;
// ... later edits go through apply / preview on the same handle
;
Freeing handles#
Releasing a project you no longer need#
Call free() once per handle. It releases the project's wasm memory right away:
const = new;
After free(), the handle is dead. Any method call on it throws. Free each handle exactly once and drop your reference to it.
Releasing with using#
[Symbol.dispose]() is the same function as free(), so hosts that support explicit resource management can let the scope free the handle:
Freeing clones#
clone() returns a new handle with its own copy of the accepted state, cached result and options. Freeing the original doesn't free the clone, and the other way round:
const = ;
Relying on the finalizer#
If a handle becomes unreachable without free(), the FinalizationRegistry frees its memory at some later point the GC chooses. Don't count on it for long-lived editors or for loops that create many projects; memory can grow well before the finalizer runs.
Threads and blocking#
Keeping the UI responsive#
Every export runs to completion on the calling thread. While solve, level, evaluate or suggestAssignments runs on a large project, the event loop waits. Move heavy work to a Web Worker (browser) or a worker thread (Node): load the module inside the worker, keep the handle there, and exchange JSON strings with the main thread. See Run in a worker.
// worker.js
await ;
const = new;
;
One instance, one thread#
A module instance is single-threaded. Calls on its handles run one after another, never at the same time. To run work in parallel, start several workers. Each one loads its own instance and holds its own projects.
Evaluating candidates#
evaluate scores each candidate on a disposable copy of the baseline, one candidate after another.
Recovering from an engine failure#
An internal engine failure surfaces as a wasm trap, not as an error envelope.
An internal engine failure surfaces as a WebAssembly.RuntimeError; after one, create the project again (or reload the module) before continuing.
Cost per call#
What a call does besides scheduling#
Each call encodes its JSON arguments to UTF-8, copies them into wasm memory, runs the engine, then copies the result string back out and decodes it. On top of that comes your own JSON.stringify before the call and JSON.parse after it. For a large project, this serialization is a visible part of every call.
Sending less JSON#
A JsonProject keeps the project between calls, so after the first solve with data you send only change sets:
const = new;
const = ;
const = ;
const = ;
const = ;
Results are sparse too: updates lists only tasks that changed. Ask for includeUnchanged: true only when you need the full date list, such as on the first load.
Reusing a cached result#
solve without data and with no edits since the last solve returns the cached result when the options are equal to the previous call's. level starts with the same solve, so it reuses that cache as well.
Skipping the parse#
Hosts that only forward results can pass the envelope text straight through without parsing it. See Forward JSON from a server.
Cloning cheaply#
clone() shares the project's immutable state with the original until one of them is edited. A clone for a short-lived proposal costs little until you change it, but it's still a handle to free.