Files
2026-05-27 23:47:53 +01:00

2.7 KiB

WorkerEngine

(no submodules)

Purpose

WorkerEngine uses real node:worker_threads for true OS-thread isolation. Each execute call spawns a fresh Worker constructed from an inline source string (eval: true), posts the code + context + modules over the structured-clone message channel, and awaits the structured reply. Intrinsics are NOT shared with the host realm — the worker has its own V8 isolate, its own heap, and its own event loop. Timeouts are enforced by calling worker.terminate(), which is a hard stop the running script cannot intercept.

Use WorkerEngine when the code being sandboxed may be adversarial: a true OS-thread boundary defeats the prototype-chain escapes that compromise vm-engine. The cost is per-call worker startup, which is non-trivial; pair with SandboxPool when latency matters.

Features

  • Real node:worker_threads execution — OS-thread isolation.
  • Worker spawned per call, terminated after every result.
  • worker.terminate() enforces timeout — script cannot intercept.
  • Structured-clone passes context + modules.
  • Output-size cap applied inside the worker before postMessage.
  • OTel recordExecution + recordDuration per call.

Usage: Basic

import { WorkerEngine } from "@suaveplan/sandbox/worker-engine";

const e = new WorkerEngine({
    timeout: 5000,
    maxOutputSize: 1024,
    memoryLimit: 50 * 1024 * 1024,
});
const r = await e.execute("2 + 2");
e.destroy();

Usage: Advanced

Wrap the engine in a pool to amortise startup:

import { SandboxPool } from "@suaveplan/sandbox/pool";
import { WorkerEngine } from "@suaveplan/sandbox/worker-engine";

const pool = new SandboxPool({
    minSize: 2,
    maxSize: 8,
    engineConfig: { timeout: 5000, maxOutputSize: 1024, memoryLimit: 5e7 },
    factory: (cfg) => new WorkerEngine(cfg),
});

API Reference

  • new WorkerEngine(config: SandboxEngineConfig).
  • execute(code, context?, options?): Promise<SandboxResult>.
  • registerModule(mod): void.
  • unregisterModule(name): boolean.
  • reset(): void.
  • destroy(): void.

Implementation Notes

The inline worker source is a single string literal. The worker uses a runtime Reflect.construct(Function, [...keys, source]) to wrap the user code in an isolated function scope — this is acceptable inside the worker because the worker IS the security boundary. Outside the worker, in the host, no dynamic code construction happens. The post-message channel uses structured clone, which copies primitives + plain objects + typed arrays but not functions or symbols — module exports must therefore be data, not host references; for host bridges use spawn-engine plus an IPC channel.