Run in a worker#
Keep the page or the server responsive while large solve, level, evaluate or suggestAssignments calls run.
Every raw export is synchronous and blocks the thread that calls it. Load the module inside a worker, keep the JsonProject handle there, and pass JSON strings between the threads. A handle belongs to the module instance that created it, so it can't be sent to another thread. Background: Module and handles.
Prerequisites#
- The browser build in
pkg/, or the Node.js build inpkg-node/for a worker thread. See Module and handles. - For the browser: module workers (
new Worker(url, { type: "module" })), sincepkg/plen_wasm.jsis an ES module.
Steps#
-
Create the worker script. It loads the module, creates one project and answers each message with the envelope text, unparsed:
// scheduler-worker.js const = new; // Set the handler right away and wait for the module inside it, // so messages sent during loading aren't missed. const = .; ;The
OPSlist keepsfreeandcloneout of reach of the main thread.init()fetchesplen_wasm_bg.wasmfrom the folder that holdsplen_wasm.js. To load it from another URL, see init. -
Start the worker on the main thread and turn each request into a promise. A request id matches every reply to its call:
// scheduler-client.jsArguments are the same JSON strings the export takes.
callresolves with the parsed envelope, so checkokas you would after a direct call. See Error handling. If the main thread only forwards results, resolve withtextand skip the parse. -
Load the data, then send the heavy calls through
call. The main thread stays free while the worker runs them:// Two 2-day tasks, both booked full time on dev1 from Monday, August 10, 2026. const = ; const = ; const = await ; throw new; ; const = await ; throw new;Pass
nullas the data argument for asolveoranalyzewithout data:scheduler.call("solve", null, "{}"). -
Close the worker when the project is no longer needed. The
closemessage frees the handle before the worker stops:;worker.terminate()also releases everything, since the module instance goes away with the worker. It doesn't wait for a running call to finish.
In a Node.js worker thread#
Use pkg-node/ and node:worker_threads. The Node build is ready as soon as require returns, so the worker needs no init():
// scheduler-worker.js
const = ;
const = ;
const = new;
const = new;
;
On the main thread, the client from step 2 changes in two lines:
const = ;
const = ;
const = new;
;
Several workers from one download#
A module instance is single-threaded, so one worker runs one call at a time. For parallel work, start several workers. Each one holds its own instance and its own projects. To fetch and compile the .wasm file only once, compile it on the main thread and send the compiled module to each worker:
// main thread
const = new;
const = await ;
const = .;
// pool-worker.js
let ;
;
Messages to one worker arrive in order, so the module message is handled before any request.
evaluate doesn't split its candidates across workers by itself. To score them in parallel, give each worker a share of the candidates and its own solved copy of the project.
If the worker hits an engine failure#
An internal engine failure doesn't come back as an error envelope. The call throws instead, and the client above rejects with trap: true.
An internal engine failure surfaces as a WebAssembly.RuntimeError; after one, create the project again (or reload the module) before continuing.
In a worker, the cleanest way to reload is to terminate() it, start a new one, and send the full data again with solve.
Result#
Open the page with the browser console. It prints:
loaded 2 tasks
B: 2026-08-12 08:00 .. 2026-08-14 08:00, delay 2
The page stays interactive while the worker runs. The handle lives only in the worker, and the main thread sees nothing but JSON.