deno-director
v0.9.30
Published
High-performance Node.js <-> Deno bridge with native worker isolation, fast binary channels, and rich type-safe cross-runtime messaging.
Maintainers
Readme
🦕 Deno Director 🎬
Run Deno inside Node.js like a boss.
Deno Director is a native Rust/Neon bridge that lets your Node process spin up isolated Deno runtimes, execute TS/JSX directly, stream bytes between them, call functions both ways, and keep strict permission boundaries.
You get one process, two ecosystems, and clean runtime separation.
🔥 Why Deno Director?
- Native bridge, not toy IPC: move
Map,Set, typed arrays,Date,RegExp,Error, recursive objects, and functions across runtimes with type fidelity intact. - Host function bridging: inject Node functions into Deno and call them from sandboxed code, sync or async.
- Real sandbox controls: enforce
read,write,net,env,run,ffi,sys,import,hrtime, andwasmpermissions per worker. - TS/JSX first-class: run TypeScript and TSX through
eval,evalSync, and module APIs with built-in transpilation. - Node.js compatibility: enjoy Node-like support across module resolution, runtime behavior, and CommonJS interop.
- Fast stream transport: push bytes through
worker.stream.connect(...)when message-style APIs are not enough. - Handles and global ops: mutate and inspect runtime object graphs through
worker.handle.*andworker.global.*APIs. - Fleet controls: orchestrate large runtime pools with
DenoDirector. - Serverless-style execution: spin up short-lived, permission-scoped Deno runtimes per request when isolation matters most.
- Telemetry: read heap stats and execution timing without duct tape.
💡 How it works
- One Node process hosts many Deno runtimes.
- Each
DenoWorkeris an isolated V8 boundary. - Values cross through a native bridge with wire hydration + function bridging.
- You set runtime policy per worker: permissions, import interception, console routing, timeouts, memory limits, startup hooks.
- If needed, scale to many workers with
DenoDirector.
🚀 Quick Start
npm install deno-director🚀 The Basics: TS Evaluation + Host Callback Bridge
import { DenoWorker } from "deno-director";
// 1) Boot a locked-down isolate
const worker = new DenoWorker({
// Sandbox first. Deno sees only what you allow.
permissions: { net: false, read: false, env: false }
});
// 2) Expose a Node async function to Deno
await worker.global.set("hostFetchData", async (userId: string) => {
console.log(`[Node.js] Deno asked for data for ${userId}. Fetching securely...`);
// Simulate an async database or API call on the Node side
await new Promise(resolve => setTimeout(resolve, 500));
return { id: userId, secret: "super_classified_payload" };
});
// 3) Evaluate an ES module in Deno that calls back into Node
const sandbox = await worker.module.eval(`
export async function processUser(userId) {
console.log(\`[Deno] Initiating secure processing for \${userId}...\`);
// Call the async Node.js function from inside the isolated Deno sandbox
const rawData: { // TypeScript is OK!
id: string,
secret: string
} = await hostFetchData(userId);
// Return the processed data back to Node
return {
status: "SECURED",
originalId: rawData.id,
fingerprint: btoa(rawData.secret).substring(0, 12)
};
}
`, {srcLoader: "ts"}); // enable TS compiler
// 4) Call the Deno export from Node
console.log("[Node.js] Triggering Deno sandbox...");
const result = await sandbox.processUser("user_999");
console.log("[Node.js] Final Result from Deno:", result);
// Result: { status: 'SECURED', originalId: 'user_999', fingerprint: 'c3VwZXJfY2xh' }
await worker.close();
🧭 Fleet Orchestration: Multi-Tenant Without Drama
Use DenoDirector when one runtime turns into fifty and then into five hundred.
import { DenoDirector } from "deno-director";
const director = new DenoDirector({
template: {
// Base configuration for all workers
workerOptions: { limits: { maxMemoryBytes: 128 * 1024 * 1024 } }, // 128MB limit
bootstrapScripts: ["globalThis.APP_RUNTIME = 'DenoDirector';"]
}
});
// Start a runtime for a specific tenant
const tenantA = await director.start({
label: "tenant-a",
tags: ["premium-tier", "us-east"],
globals: { TENANT_ID: "A" }
});
await tenantA.eval(`console.log("Hello from", TENANT_ID)`);
// Query and manage your fleet
const premiumRuntimes = director.list({ tag: "premium-tier" });
console.log(`Active premium runtimes: ${premiumRuntimes.length}`);
// Remove a tenant runtime cleanly
await director.stopByLabel("tenant-a");
☁️ Serverless-Style Execution with Deno
If you want function-style isolation with low overhead, keep a warm runtime pool and dispatch requests onto it.
import { DenoDirector } from "deno-director";
const director = new DenoDirector({
template: {
workerOptions: {
permissions: { net: false, read: false, write: false, env: false },
limits: { maxEvalMs: 1500, maxMemoryBytes: 256 * 1024 * 1024 },
},
},
});
export async function handleRequest(payload: unknown) {
// Prestart runtimes at boot and reuse them.
const rt = await getLeastBusyWarmRuntime();
try {
return await rt.eval<{ ok: boolean; data: unknown }>(
`(input) => ({ ok: true, data: input })`,
{ args: [payload] },
);
} finally {
markRuntimeIdle(rt);
}
}This gives you serverless-style isolation boundaries with much lower latency than cold-starting a runtime per request.
🧠 Major Capabilities
🌉 The Transdimensional Bridge
When you pass data between Node and Deno using eval, evalSync, global.set or handles, Deno Director doesn't just JSON.stringify. It uses a complex custom codec backed by V8 serialization.
NaN,Infinity,-0? Preserved.Uint8Array,DataView,SharedArrayBuffer? Passed instantly via underlying memory views.- Promises? Automatically chained and awaited across the boundary.
📦 ES Module Proxying
Don't just evaluate strings—import entire ES modules and use them as if they were native Node.js objects.
// Deno dynamically imports the code, and Node gets a fully typed proxy namespace!
const mod = await worker.module.eval(`
export const version = "1.0.0";
export function encrypt(data) { return btoa(data); }
`);
console.log(mod.version); // "1.0.0"
console.log(await mod.encrypt("secret")); // "c2VjcmV0"
If you already have a module specifier, use worker.module.import(...):
const worker = new DenoWorker({
modules: { // declare available startup modules
"app:math": {src: "export const add = (a: number, b: number): number => a + b;", srcLoader: "ts"},
},
imports: false, // block non-registered imports; only `modules` entries are resolvable
});
const math = await worker.module.import("app:math");
console.log(math.add(2, 3)); // 5
// throws:
const another_module = await worker.module.import("another_module");
await worker.close();modules also supports loader-aware entries:
const worker = new DenoWorker({
sourceLoaders: [
({ srcLoader, src }) => {
if (srcLoader !== "app-ts") return;
// Map custom loader name to built-in TS transpilation.
return { src, srcLoader: "ts" };
},
],
modules: {
"app:config": {
src: `export const env: string = "prod";`,
srcLoader: "app-ts",
},
},
});When you need dynamic resolution (instead of a fixed allowlist in modules), use the imports interceptor.
🪄 Magic Module Resolution: The imports Interceptor
By default, Deno resolves modules from the disk or network. But with Deno Director, you can completely hijack the ES Module graph. Every time the Deno sandbox encounters an import statement, it pauses and asks your Node.js host exactly what to do.
You can use the imports callback to rewrite specifiers, block malicious network requests, or even serve virtual modules directly from memory. Because the callback can be async, you have the full power of Node.js at your fingertips to fetch, compile, and cache code on the fly.
What it looks like inside the Deno Sandbox: From Deno's perspective, everything is just standard ECMAScript. It has no idea you are pulling the strings behind the scenes.
// Inside Deno:
import { sum } from "./math.ts"; // Normal relative import
import { db } from "app:database"; // Custom URL scheme!
const Secret = await import("untrusted-dynamic"); // Dynamic import
What it looks like on the Node.js Host:
Let's build an interceptor that blocks dynamic imports for security, uses a custom in-memory cache, and compiles a proprietary module scheme (app:) on the fly.
Return shape note for imports callbacks:
{ src: string, srcLoader?: string }srcLoaderdefaults to"js"when omitted.- Final runtime source loader must resolve to one of
"js" | "ts" | "tsx" | "jsx". - When
sourceLoaders: falseis set on the worker, only"js"is allowed.
import { DenoWorker } from "deno-director";
// A simple in-memory cache on the Node side
const moduleCache = new Map<string, string>();
const worker = new DenoWorker({
sourceLoaders: [
async ({ src, srcLoader }) => {
if (srcLoader !== "app-ts") return;
// Rewrite custom loader names to built-in runtime loaders.
return { src, srcLoader: "ts" };
},
],
// Import interceptor
imports: async (specifier, referrer, isDynamicImport) => {
console.log(`[Deno] requesting: ${specifier} (from ${referrer})`);
// 1. Security: Block all dynamic imports (const y = await import("..")) to prevent code-injection attacks
if (isDynamicImport) {
console.warn(`Blocked dynamic import of ${specifier}`);
return false; // false = block module with error
}
// 2. Custom Scheme: Intercept "app:*" imports and compile them on the fly
if (specifier.startsWith("app:")) {
const moduleName = specifier.replace("app:", "");
// Check our Node-side cache first!
if (moduleCache.has(moduleName)) {
return { src: moduleCache.get(moduleName)!, srcLoader: "app-ts" }; // Serve from memory
}
// Simulate fetching or compiling custom code (e.g., from a DB or remote API)
const compiledTsCode = `export const ${moduleName} = "Super Secret Data for ${moduleName}";`;
// Save to cache
moduleCache.set(moduleName, compiledTsCode);
// Feed it source code directly into Deno's memory as a TypeScript module!
return { src: compiledTsCode, srcLoader: "app-ts" };
}
// 3. Fallback: Allow normal resolution for everything else
return true;
}
});
// Run it! Deno will hit our interceptor for "app:database"
const result = await worker.module.eval(`
import { database } from "app:database";
export function getData() {
return database;
}
`);
console.log(await result.getData()); // "Super Secret Data for database"
The resulting superpower: You can seamlessly integrate tools like Webpack, SWC, or esbuild on the Node.js side, transpile custom DSLs, and feed the resulting raw code directly into the isolated Deno runtime without ever touching the disk.
🎛️ Runtime Handles
Handles let you keep a live reference to a runtime value and operate on it without re-evaluating lookup code each time. This is useful for complex object graphs, long-lived instances, and high-frequency operations.
Entry points:
worker.handle.get(source, options?)-> bind to an existing runtime value (throws if object does not exist)worker.handle.tryGet(source, options?)-> same asgetbut returnsundefinedwhen missingworker.handle.eval(source, options?)-> evaluate source and bind the result as a handle root
Once you have a handle, you can call methods like: get, set, has, delete, keys, entries, call, construct, await, and many more!
import { DenoWorker } from "deno-director";
const worker = new DenoWorker();
await worker.eval(`
globalThis.counter = {
value: 0,
inc(n = 1) { this.value += n; return this.value; }
};
`);
const h = await worker.handle.get("counter");
await h.call("inc", [2]); // 2
await h.set("value", 10);
console.log(await h.get("value")); // 10
console.log(await h.getType()); // { type: "object", ... }
console.log(h.rootType); // cached root type snapshot
await h.dispose();
await worker.close();🥷 Smuggling Node.js Modules into Deno
Because Deno Director utilizes a recursive V8 serializer and native function bridging, you can literally inject entire Node.js core modules (or any complex object with methods) directly into the Deno sandbox.
Deno Director will automatically walk the object's enumerable properties, wrapping all functions into blazing-fast native bridges.
import fs from "node:fs";
import crypto from "node:crypto";
import { DenoWorker } from "deno-director";
const worker = new DenoWorker({
// Deno's native file reading is blocked...
permissions: { read: false }
});
// ...but we can inject Node's `fs` module in anyway!
await worker.global.set("nodeFs", fs);
await worker.global.set("nodeCrypto", crypto);
const result = await worker.eval(`
// Calling Node.js fs functions from inside Deno!
const cwdEntries = nodeFs.readdirSync(".");
const payload = new TextEncoder().encode(String(cwdEntries.length));
// Calling Node's crypto module from Deno
// This function runs in the Node context.
const hash = nodeCrypto.createHash("sha256");
hash.update(payload);
hash.digest("hex");
`);
console.log(`File hash: ${result}`);
await worker.close();
🤫 Hijack the Console
Untrusted code loves to spam console.log. Deno Director gives you absolute authority over standard output. You can silence the sandbox completely, pipe it natively to Node.js, or route specific log levels to your own telemetry tools.
// 0. Default behavior: the sandbox console is routed to stdout/stderr/etc
const defaultWorker = new DenoWorker();
// 1. Total Silence: no console output
const silentWorker = new DenoWorker({ console: false });
// 2. Native Passthrough: Pipe Deno's console directly to Node's console
const noisyWorker = new DenoWorker({ console: console });
// 3. Surgical Routing: Hook specific methods to custom host functions
const customWorker = new DenoWorker({
console: {
log: (...args) => myDatadogLogger.info("Deno says:", ...args),
// async functions are supported!
error: async (...args) => await PagerDuty.alert("Deno crashed:", ...args),
warn: false, // Drop warnings into the void
debug: undefined // Fallback to default Deno behavior
}
});
🧮 Function Calls with args
Use options.args to call a function value produced by eval/evalSync.
import { DenoWorker } from "deno-director";
const worker = new DenoWorker();
// Call a global function in the Deno runtime...
// passing in args from Node
const json_val = await worker.eval("JSON.parse", { args: ['{"key": "value"}'] });
console.log(json_val); // {key: "value"}
// Same pattern with inline functions and expressions.
// promises automatically resolve across the call boundary
const product = await worker.eval("async (a, b) => a * b", { args: [3, 4] });
console.log(product); // 12
// evalSync also supports args
// even without await, promises resolve across the call boundary
const out = worker.evalSync("async (name) => `hi ${name}`", { args: ["director"] });
console.log(out); // "hi director"
await worker.close();Notes:
- If
argsare provided (even empty[]) and the evaluated value is callable, the function is invoked with those args. - If
argsare omitted, function values are not auto-called. - If
argsare provided but the evaluated value is not callable, the value is returned as-is.
🧩 Node.js Compatibility
Use nodeJs as the centralized API for Node-style module/runtime compatibility.
import { DenoWorker } from "deno-director";
// Example 1: module resolution only
const modulesWorker = new DenoWorker({
imports: true,
nodeJs: { modules: true },
});
const modulesOut = await modulesWorker.module.eval(`
import path from "path";
export const out = path.basename("/tmp/demo.txt");
`);
console.log(modulesOut.out); // "demo.txt"
await modulesWorker.close();
// Example 2: runtime + modules
const runtimeWorker = new DenoWorker({
imports: true,
nodeJs: { runtime: true, modules: true },
});
const runtimeOut = await runtimeWorker.module.eval(`
import path from "path";
export const out = path.join("a", "b");
`);
console.log(runtimeOut.out); // "a/b" (platform separators may vary)
await runtimeWorker.close();
// Example 3: shorthand (enable all Node modes)
const allNodeWorker = new DenoWorker({
imports: true,
nodeJs: true, // same as { modules: true, runtime: true, cjsInterop: true }
});
await allNodeWorker.close();📦 CommonJS Interop (cjsInterop)
Enable nodeJs.cjsInterop: true to run CJS packages with Node-style wrapper semantics (exports, require, module, __filename, __dirname) and expose an ESM facade.
import { DenoWorker } from "deno-director";
const worker = new DenoWorker({
imports: true,
nodeJs: {
modules: true,
runtime: true,
cjsInterop: true,
},
});
const out = await worker.module.eval(`
import cjsPkg, { named } from "some-cjs-package";
// Node-style behavior:
// - default import is module.exports
// - named imports map to properties on module.exports
export const result = [typeof cjsPkg, typeof named];
`);
console.log(out.result);
await worker.close();Interop semantics:
nodeJs.cjsInteropis boolean-only (true= enabled,false/omitted = disabled).nodeJs: trueis shorthand for enablingmodules,runtime, andcjsInterop.- CJS detection is parser-backed and best effort, not guaranteed full Node parity for every package shape.
- Default import follows Node CJS interop semantics (
defaultis the fullmodule.exportsvalue). exports.default(when present in transpiled CJS) remains nested atdefault.default.
Force overrides (nodeJs.cjsForcePaths):
- Use this when detection is ambiguous and you need deterministic CJS treatment.
- Supports literal paths, glob patterns, and regex.
- Relative path/glob entries resolve from worker
cwd.
const worker = new DenoWorker({
cwd: process.cwd(),
imports: true,
nodeJs: {
modules: true,
runtime: true,
cjsInterop: true,
cjsForcePaths: [
"node_modules/some_package/index.js", // literal
"node_modules/some_package/*", // glob
/node_modules\/legacy-.*\/dist\/.+\.js$/, // regex
],
},
});Compatibility scope:
- For module loading and package usage, this targets broad Node-style behavior (
nodeJs.modules+nodeJs.runtime+nodeJs.cjsInterop). cjsInteropis best effort; usenodeJs.cjsForcePathswhen deterministic CJS classification is required.- Deno Director is still a Deno runtime in-process, so non-module Node internals may differ from a full Node runtime.
🧪 Custom Loaders in 20 Seconds
srcLoader defaults to "js".
Built-in runtime loaders are "js", "ts", "tsx", and "jsx".
If final source loader is "ts", "tsx", or "jsx", the built-in transpiler runs.
Custom loaders let the Node host intercept source before runtime execution, so you can adapt code to your own pipeline. Use them to alias loader names, precompile custom formats to JS, enforce tenant-specific policy, or hard-disable loader modes for stricter runtime behavior.
import { DenoWorker } from "deno-director";
const worker = new DenoWorker({
sourceLoaders: [
async ({ src, srcLoader }) => {
if (srcLoader !== "custom-ts") return;
// Rewrite custom loader name to built-in TS loader.
return { src, srcLoader: "ts" };
},
],
});
const out = await worker.eval<number>(
"const n: number = 41; n + 1;",
{ srcLoader: "custom-ts" },
);
console.log(out); // 42
await worker.close();🌍 Environment Variables: The Secure Way
By default, Deno Director does not copy host process env into the worker.
Use env: process.env for host-env passthrough, envFile: true to load <cwd>/.env at startup
(and emit a startup warning when missing), or env: true to enable runtime env access without seeding values.
import { DenoWorker } from "deno-director";
const worker = new DenoWorker({
// 1. Explicitly pass a map of variables
env: {
DB_PASS: "super_secret",
NODE_ENV: "production"
},
// OR 2. Pass host env explicitly
// env: process.env,
// OR 3. Enable runtime env usage with no startup data
// env: true,
// OR 4. Auto-load <cwd>/.env at startup; warns if file is missing
// envFile: true,
// OR 5. provide exact file path (resolved from cwd); errors if missing
// envFile: ".my.env"
});
// Inside Deno, access them normally:
await worker.eval(`
const pass = Deno.env.get("DB_PASS"); // "super_secret"
`);
await worker.close();Use worker.env.get(key) / worker.env.set(key, value) for runtime env access with startup persistence.
import { DenoWorker } from "deno-director";
const worker = new DenoWorker({
permissions: { env: true }, // enable env in the worker
env: { APP_MODE: "dev" }, // pass a value at startup or restart
});
console.log(await worker.env.get("APP_MODE")); // "dev"
await worker.env.set("APP_MODE", "prod");
console.log(await worker.env.get("APP_MODE")); // "prod"
await worker.restart();
console.log(await worker.env.get("APP_MODE")); // "prod" (persisted across restart)
await worker.close();Permission note:
- If you provide
envas a map andpermissions.envis missing (or[]), the runtime auto-populatespermissions.envwith those env-map keys. - If
permissions.envis already set, it is not changed. worker.env.get/setthrow whenpermissions.env === false(orpermissions === false).- If configured env keys are not readable under
permissions.env, startup emits a warning. - If
permissions.runis enabled, spawned subprocesses may observe host environment values unless command env is explicitly constrained.
📁 Worker CWD API
cwd is explicit-only. If omitted, worker uses a unique internal sandbox cwd (<tmp>/deno-director/sandbox/w-<id>) instead of host process.cwd().
import { DenoWorker } from "deno-director";
import fs from "node:fs/promises";
import path from "node:path";
// 1) Explicitly pass host cwd into runtime (opt-in passthrough)
const hostCwdWorker = new DenoWorker({
cwd: process.cwd(),
imports: true,
});
console.log(await hostCwdWorker.cwd.get()); // host process cwd
await hostCwdWorker.close();
// 2) Internal/default sandbox cwd (use worker.cwd.get() for host file operations)
// Note: `cwd: true` is not a valid option. Omit `cwd` to use internal sandbox cwd.
const worker = new DenoWorker({ imports: true, permissions: { read: true, write: true } });
console.log(worker.id); // stable worker id (used in default cwd path)
const sandboxCwd = await worker.cwd.get();
console.log(sandboxCwd); // "<tmp>/deno-director/sandbox/w-<id>"
// add/remove files using the runtime cwd path
const demoFile = path.join(sandboxCwd, "tmp-note.txt");
await fs.writeFile(demoFile, "hello sandbox", "utf8");
await fs.unlink(demoFile);
await worker.cwd.set("/var/sandbox"); // path must already exist
await worker.close();Notes:
- Provided
cwdmust exist and be a directory, or worker startup fails. - Relative
cwdvalues are resolved from hostprocess.cwd()when explicitly provided.
📖 API Documentation
🎬 class DenoDirector
The primary class for orchestrating multiple DenoWorker instances.
| Method | Returns | Description |
| --- | --- | --- |
| start(options?: DenoDirectorStartOptions) | Promise<DenoDirectedRuntime> | Spawns a new managed Deno runtime. |
| get(id: string) | DenoDirectedRuntime | undefined | Retrieves a runtime by its unique ID. |
| getByLabel(label: string) | DenoDirectedRuntime[] | Retrieves all runtimes matching a specific label. |
| list(filter?: DenoDirectorListOptions) | DenoDirectedRuntime[] | Lists runtimes, optionally filtering by label and/or tag. |
| setLabel(runtime, label: string) | boolean | Updates a runtime's label. |
| setTags(runtime, tags: string[]) | boolean | Replaces a runtime's tags. |
| addTag/removeTag(runtime, tag) | boolean | Modifies tags on an existing runtime. |
| stop(runtimeOrId) | Promise<boolean> | Gracefully closes a runtime and removes it from the pool. |
| stopByLabel(label: string) | Promise<number> | Stops all runtimes matching a given label. |
| stopAll() | Promise<number> | Destroys the entire fleet. |
🛡️ class DenoWorker
The core runtime isolate. Maps 1:1 with a V8 thread.
Execution Methods
eval<T = any>(src: string, options?: EvalOptions): Promise<T>Evaluates JavaScript or TypeScript asynchronously.evalSync<T = any>(src: string, options?: EvalOptions): TEvaluates JavaScript or TypeScript synchronously (blocks Node event loop while waiting).module.eval<T>(src: string, options?: DenoWorkerModuleEvalOptions): Promise<T>Evaluates the source as an ES Module and returns a callable Proxy namespace to the exports.module.register(moduleName: string, source: string, options?: { srcLoader?: string }): Promise<void>Registers source under a module name for future imports.module.clear(moduleName: string): Promise<boolean>Clears a previously registered module by name.module.import<T>(specifier: string): Promise<T>Imports a module specifier through the runtime import pipeline and returns a callable Proxy namespace to the exports.stream.connect(key: string, options?: { unsafeSharedMemory?: boolean }): Promise<Duplex>Opens a bidirectional stream session and returns a Node.jsDuplexstream.id: stringStable worker id for correlation (also used in default internal cwd suffix).cwd.get(): Promise<string>Returns current worker cwd (runtimeDeno.cwd()when running).cwd.set(path: string): Promise<string>Updates worker cwd and restarts runtime when currently running.env.get(key: string): Promise<string | undefined>Reads worker environment value forkey.env.set(key: string, value: string): Promise<void>Sets worker environment value and persists it for restart/next start.
EvalOptions.srcLoader (and module.eval(..., { srcLoader })) defaults to "js".
Use "ts", "tsx", or "jsx" to request TS/JSX transpilation for that call.
Custom loader names are supported through DenoWorkerOptions.sourceLoaders callback pipelines.
module.eval(..., { cjs: true }) enables Node-style CommonJS wrapping for eval source
(exports, require, module, __filename, __dirname) and returns an ESM facade
with default mapped to module.exports plus detected named exports.
worker.envthrows when env permission is explicitly disabled (permissions.env === falseorpermissions === false).- async iteration:
for await (const chunk of reader) { ... }
Environment & Memory
worker.global mirrors the handle operation surface, rooted at globalThis.
global.set(path: string, value: any, options?: DenoWorkerHandleExecOptions): Promise<void>Set a global value by dot-path.global.get<T = any>(path: string, options?: DenoWorkerHandleExecOptions): Promise<T>Read a global value by dot-path.global.has(path: string, options?: DenoWorkerHandleExecOptions): Promise<boolean>Check if a global path exists.global.delete(path: string, options?: DenoWorkerHandleExecOptions): Promise<boolean>Delete a global path.global.keys(path?: string, options?: DenoWorkerHandleExecOptions): Promise<any[]>Return keys fromglobalThisroot or nested path.global.entries(path?: string, options?: DenoWorkerHandleExecOptions): Promise<any[]>Return entries fromglobalThisroot or nested path.global.getOwnPropertyDescriptor(path: string, options?: DenoWorkerHandleExecOptions): Promise<PropertyDescriptor | undefined>Read a property descriptor from a global path.global.define(path: string, descriptor: PropertyDescriptor, options?: DenoWorkerHandleExecOptions): Promise<boolean>Define a property descriptor at a global path.global.isCallable(path?: string, options?: DenoWorkerHandleExecOptions): Promise<boolean>Check whether a global value is callable.global.isPromise(path?: string, options?: DenoWorkerHandleExecOptions): Promise<boolean>Check whether a global value is promise-like.global.call<T = any>(path: string, args?: any[], options?: DenoWorkerHandleExecOptions): Promise<T>Call a global function by path.global.construct<T = any>(path: string, args?: any[], options?: DenoWorkerHandleExecOptions): Promise<T>Construct a global constructor by path.global.await<T = any>(path: string, options?: DenoWorkerHandleAwaitOptions & DenoWorkerHandleExecOptions): Promise<T>Await a global promise-like value by path.global.clone(path: string, options?: DenoWorkerHandleExecOptions): Promise<DenoWorkerHandle>Create a durable handle from a global path.global.toJSON<T = any>(path?: string, options?: DenoWorkerHandleExecOptions): Promise<T>Return a JSON snapshot from global root or nested path.global.apply<T = any[]>(path: string, ops: DenoWorkerHandleApplyOp[], options?: DenoWorkerHandleExecOptions): Promise<T>Run batched handle operations against a global path root in one roundtrip.global.getType(path?: string, options?: DenoWorkerHandleExecOptions): Promise<DenoWorkerHandleTypeInfo>Read runtime type metadata for global root or nested path.global.instanceOf(path: string, constructorPath: string, options?: DenoWorkerHandleExecOptions): Promise<boolean>Checkinstanceofagainst a constructor path.stats.activeOps: numberCurrent count of active async runtime operations tracked by the wrapper.stats.lastExecution: { cpuTimeMs?: number, evalTimeMs?: number }Returns telemetry for the most recent runtime operation that reports execution stats (for example: eval, module eval, handle ops, and global ops routed through the handle bridge).stats.cpu(options?: { measureMs?: number }): Promise<{ usagePercentage: number, measureMs: number, cpuTimeMs: number }>Returns CPU usage estimate over a recent window.usagePercentageis clamped to0-100.stats.rates(options?: { windowMs?: number }): Promise<{ windowMs: number, evalPerSec: number, handlePerSec: number, globalPerSec: number, messagesPerSec: number }>Returns operation/message throughput over a rolling window.stats.latency(options?: { windowMs?: number }): Promise<{ windowMs: number, count: number, avgMs: number, p50Ms: number, p95Ms: number, p99Ms: number, maxMs: number }>Returns rolling latency percentiles/summary for tracked operations.stats.eventLoopLag(options?: { measureMs?: number }): Promise<{ measureMs: number, lagMs: number }>Measures host event-loop lag over a short timer window.stats.stream: { activeStreams: number, queuedChunks: number, queuedBytes: number, creditDebtBytes: number, backlogSize: number }Returns stream/backpressure queue snapshot from wrapper state.stats.totals: { ops: number, errors: number, restarts: number, messagesOut: number, messagesIn: number, bytesOut: number, bytesIn: number }Monotonic counters since startup or last stats reset.stats.reset(options?: { keepTotals?: boolean }): voidClears rolling samples (cpu,rates,latency) and optionally totals.stats.memory(): Promise<DenoWorkerMemory>Returns granular V8 heap statistics (totalHeapSize,mallocedMemory, etc.).
Messaging & Lifecycle
postMessage(msg: any): voidFires an event into Deno'sglobalThis.onmessage.on(event: "message" | "close" | "lifecycle" | "runtime", cb: Function)Listen for messages from Deno (hostPostMessage), close events, or lifecycle transitions (beforeStart,onCrash, etc.)."runtime"events emit operation telemetry such aseval.begin/end,evalSync.begin/end,import.requested/resolved,stream.connect,handle.*, anderror.thrown.close(options?: { force?: boolean }): Promise<void>Gracefully shuts down the V8 isolate. Useforce: trueto instantly terminate execution.restart(options?: { force?: boolean }): Promise<void>Reboots the isolate in place, re-applying all template configurations and global variables.
⚙️ Configuration Types
DenoWorkerOptions
Passed into new DenoWorker(opts) or used as workerOptions in templates.
type DenoWorkerOptions = {
limits?: {
maxHandle?: number; // Active handle cap (default 128)
maxEvalMs?: number; // Default timeout for eval + handle runtime operations
maxCpuMs?: number; // Default CPU-budget timeout for eval + handle runtime operations
maxMemoryBytes?: number; // V8 Heap limit
};
bridge?: { // Transport tuning
channelSize?: number; // Per-queue capacity (control/data/node callback queues)
streamWindowBytes?: number; // Per-stream flow-control window
streamCreditFlushBytes?: number; // Credit flush threshold
streamBacklogLimit?: number; // Max unaccepted worker->Node stream opens to backlog (default 256)
streamHighWaterMarkBytes?: number; // Reader-side high water mark (defaults to streamWindowBytes)
enableUnsafeStreamMemory?: boolean; // Experimental unsafe shared-memory stream mode (untrusted-code risk)
};
cwd?: string; // Optional sandbox root. Omitted => unique <tmp>/deno-director/sandbox/w-<id>. Provided path must exist.
startup?: string; // Script evaluated before user code runs
permissions?: boolean | { // true=allow all, false=deny all, or per-capability config
read?: boolean | string[]; // Allow read everywhere, or specific paths
write?: boolean | string[]; // Allow write everywhere, or specific paths
net?: boolean | string[]; // Allow network, or specific domains/ports
env?: boolean | string[]; // Allow env access, or specific variables
run?: boolean | string[]; // Allow subprocess execution (high risk)
ffi?: boolean | string[]; // Allow FFI (global or allow-list)
sys?: boolean | string[]; // OS Info access (global or allow-list)
import?: boolean | string[]; // Deno import capability permission allow-list
hrtime?: boolean; // High-resolution timing access
wasm?: boolean; // Enable/disable .wasm module loading (default true)
};
env?: boolean | string | Record<string, string>; // true enables runtime env access, string=dotenv path, map=explicit values
envFile?: string | boolean; // true => load <cwd>/.env with warning when missing; string => resolve from cwd and error when missing
nodeJs?: true | { // true => { modules: true, runtime: true, cjsInterop: true }
modules?: boolean; // Node-style module resolution
runtime?: boolean; // Node runtime compatibility behavior
cjsInterop?: boolean; // CJS execution + ESM facade interop
cjsForcePaths?: Array<string | RegExp>; // Force CJS for matching files (literal, glob, regex)
};
imports?: boolean | ImportsCallback; // Custom module resolution interceptor
sourceLoaders?: false | Array<(ctx: { // process custom source loader values. jsx, tsx and ts handled by built-in loader
src: string;
srcLoader: string;
kind: "eval" | "module-eval" | "import";
specifier?: string;
referrer?: string;
isDynamicImport?: boolean;
}) => string | { src: string; srcLoader?: string } | void | Promise<string | { src: string; srcLoader?: string } | void>>;
tsCompiler?: { // TS/JSX transpiler options
jsx?: "react" | "react-jsx" | "react-jsxdev" | "preserve";
jsxFactory?: string;
jsxFragmentFactory?: string;
cacheDir?: string; // Optional on-disk transpile output cache directory
};
moduleLoader?: {
httpsResolve?: boolean; // Enable https:// imports
httpResolve?: boolean; // Enable http:// imports (insecure; startup warning emitted)
jsrResolve?: boolean; // Resolve jsr: and @std/* via jsr.io
cacheDir?: string; // Where to cache remote imports
reload?: boolean; // Bypass cache
maxPayloadBytes?: number; // Remote module payload size cap in bytes (-1 disables limit, default 10 MiB)
};
console?: DenoWorkerConsoleOption; // Route/disable console methods
inspect?: boolean | { host?: string; port?: number; break?: boolean; }; // V8 Debugging
globals?: Record<string, any>; // Startup globals applied to globalThis
modules?: Record<string, string | { src: string; srcLoader?: string }> | Map<string, string | { src: string; srcLoader?: string }>; // string shorthand => { src: string, srcLoader: "js" }
lifecycle?: DenoWorkerLifecycleHooks; // beforeStart/afterStart/beforeStop/afterStop/onCrash hooks
};
Source-loader notes:
sourceLoaderscallbacks run in array order.- Callback return values:
undefined/void: no changestring: replace source, keep current source loader{ src, srcLoader? }: replace source and optionally switch source loader
evalSynccannot run async source-loader callbacks.- If source loader is omitted everywhere, default is
"js". - Built-in runtime loader runs last, after all callbacks.
sourceLoaders: falseenables strict JS mode:- disables custom callbacks and built-in TS/TSX/JSX transpilation
- only final source loader
"js"is allowed
- For
module.eval(..., { moduleName }), built-in loaders ("js","ts","tsx","jsx") are supported. - For
modulesstartup registration entries, bothRecord<string, ...>andMap<string, ...>are supported. - For
modulesstartup registration entries, object form{ src, srcLoader? }is supported. - For
modulesstartup registration entries, string values are shorthand for{ src: "...", srcLoader: "js" }. - Custom loader names must still resolve (through
sourceLoaders) to a built-in runtime loader.
EvalOptions (used by eval, evalSync, and module.eval) in practice:
type EvalOptions = {
filename?: string;
type?: "script" | "module";
srcLoader?: string; // default "js"
args?: any[];
maxEvalMs?: number;
maxCpuMs?: number;
};module.eval-specific options:
type DenoWorkerModuleEvalOptions = Omit<EvalOptions, "type"> & {
moduleName?: string;
cjs?: boolean; // Node-style CJS wrapper for eval source
};srcLoader behavior:
- Built-in values:
"js": no transpilation"ts" | "tsx" | "jsx": built-in transpilation
- Custom values:
- allowed when transformed by
sourceLoadersinto a built-in final value - if unresolved by the end of the pipeline, the call is rejected
- allowed when transformed by
sourceLoaders: false:- only
"js"is accepted - any non-
"js"srcLoaderis rejected for eval/module/import flows
- only
imports callback virtual modules:
type ImportsCallbackResult =
| boolean
| string // shorthand for: { src: "...", srcLoader: "js" }
| { resolve: string }
| { src: string; srcLoader?: string }; // default srcLoader is "js"Notes
- This package builds a native addon during install (
cargo+ Rust toolchain required). - For module imports, configure
importsand related permissions/options based on your use case. - Prefer async host callbacks for heavy work; synchronous host callbacks execute on Node's main thread and can starve the event loop.
limits.maxMemoryBytesapplies to V8 heap accounting; WebAssembly memory may not be fully constrained by this limit.
