Skip to content

Worker contract and implementation choice

Worker protocol v1 uses plan format v1 and numerical ABI v6. laneops/worker exports createVectorExecutor and its public types. Node uses worker_threads; browser/default conditions use module Workers and Wasm. An explicit browser entry is available at laneops/worker/browser. See usage and bundler examples.

Scope and ownership

The executor supports the pipeline's eight dtypes, ten operations, 68 signatures, up to 64 nodes and eight extra inputs. submit accepts a definition or pipeline.plan; the main thread normalizes and freezes it, then the Worker compiles and validates it again. It accepts no compiled objects, arena handles, functions or caller-owned out. It is not a Promise wrapper for all 97 operations.

Before returning a task, submit synchronously snapshots every input view's actual range. Subviews copy only their elements; repeated/overlapping inputs are copied independently. It also reads the plan and operand list at submission. Later input mutation/detachment, plan changes or disposal of an unrelated synchronous engine do not alter an accepted task. Shared/resizable/detached memory, foreign-realm backing buffers and wrong dtype/length fail before admission. Intrinsic brand, buffer, offset and length metadata prevents shadowed properties bypassing budgets.

Queued snapshots belong to the executor. Dispatch transfers them into the Worker without another structured clone of the large arrays. Workers allocate independent outputs and transfer them back. Delivered results belong to the caller and leave the executor's byte budget. The executor never detaches caller inputs, and there is no public transfer/shared-memory mode. Snapshot cost is synchronous and proportional to input bytes.

States and errors

TransitionTrigger
submission → queuedValidation, budget reservation and snapshots succeed
queued → runningDispatch to a Worker
running → completedValidate and deliver the result
queued/running → cancelledcancel or AbortSignal
queued/running → disposedexecutor.dispose
running → failedComputation error, exit, timeout or malformed response
queued → failedReplacement Worker cannot initialize

Running means dispatched, possibly still in the Worker's message queue. Terminal states settle once; terminal cancel() returns false. Invalid inputs, stale plans, pre-aborted signals, unavailable executors, full queues and insufficient budgets throw synchronously before acceptance. After acceptance, failures reject task.result; applications must observe that Promise.

Error nameMeaning
TypeError / RangeErrorInvalid argument, plan, dtype or length
AbortErrorPre-aborted signal or task cancellation
QueueFullErrorSlots are busy/starting/retiring and the waiting queue is full
BudgetExceededErrorPer-task or total byte budget exceeded
TimeoutErrorWorker startup or dispatched task exceeded its deadline
ExecutorUnavailableErrorSubmission to a failed or disposed executor
ExecutorDisposedErrorDisposal cancelled an unsettled task

Worker computation errors preserve name/message, not the original prototype, stack or cause. Startup checks protocol, registry fingerprint and backend info; the engine also validates native/Wasm ABI. Stale responses cannot revive cancelled tasks. Wrong task IDs or output dtype/length retire and replace the Worker.

Cancellation and disposal

Queued cancellation releases its snapshot and reservation immediately. Running cancellation rejects immediately, terminates the Worker and ignores late results. Its reservation remains until termination completes. A synchronous native call may need to return before Node can finish terminating the thread. Browser terminate returns immediately; garbage-collection timing is not an API guarantee.

The current strategy terminates and replaces Workers instead of splitting kernels into cooperative cancellation blocks. Frequent cancellation has restart costs. Task deadlines begin at dispatch, excluding queue time, and callbacks depend on main-thread scheduling; they are not real-time guarantees.

A successfully initialized slot is replaced after an exit, running cancellation or protocol failure even if the queue is empty or maxQueue is zero. Failed replacement puts the executor in failed state, rejects remaining tasks and stops other slots. Ordinary computation errors reject only that task and retain the slot.

dispose() synchronously marks unfinished tasks disposed, prevents replacements and returns a Promise waiting for all Workers to terminate. Repeated calls return the same Promise. Delivered arrays and terminal tasks remain valid. Idle Node Workers keep the process alive, so dispose unused executors.

Queue and byte budgets

OptionDefaultConstraint
concurrency11–16 Workers, one active task each
maxQueue32Nonnegative waiting-task count, excluding active/retiring work
maxTaskBytes64 MiBPositive safe integer
maxBytes256 MiBSum of accepted, unreclaimed task reservations
startupTimeoutMs10,0001 through 2³¹−1 milliseconds
taskTimeoutMs30,000Same range, starting at dispatch

For N input views (1–9) and B bytes per view:

text
reservation = 2 × (N + 1) × B + 4096

One copy covers snapshots/output; the second conservatively covers Wasm scratch, and 4096 covers bounded instructions/pointers. Native uses the same formula. Check admission before allocating snapshots. Empty inputs still reserve 4096 bytes. Completion, queued cancellation or completed termination releases reservations. getStats() exposes state/queued/running/retiring/reservedBytes/highWaterBytes; info contains the latest successful backend initialization for each slot.

The budget bounds admitted payload, not RSS. Caller inputs/results, Worker/engine baselines, plan caches, Wasm page granularity/fragmentation and pending GC are outside it. Wasm retains grown capacity; disposal/replacement releases ownership. Memory measurements distinguish reservations from process RSS.

Why Node uses Workers

worker_threads reuse the full synchronous engine, native/Wasm fallback, browser protocol and slot recovery. The optional AsyncTask prototype supports a smaller API and omits production queue, budget and lifecycle costs. Its lower-level latency does not represent a replacement public executor.

See the benchmark comparison for reproduction and the Worker archive for recorded measurements.

References: Node Workers, napi-rs AsyncTask, TypedArray ownership, browser termination.

Browser assets

Static ESM deployments retain adjacent Worker/Wasm assets. Bundlers must resolve both Worker and Wasm URLs. With Vite, use laneops/worker/runtime?worker&url, not an opaque ?url import, so the Worker's dependency graph is bundled. URL hrefs are snapshotted during initialization. No Node dependency, shared memory or cross-origin isolation is required in the browser entry. Host CSP, Worker origin and Wasm loading rules still apply; missing assets fail startup.

Released under the MIT License.