Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/content/limitations/native-worker-threads.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
`node:worker_threads` runs workers as native threads. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. Nested workers inherit their parent's environment snapshot. Supported constructor options are `workerData` and `argv`; runtime paths, `eval`, loaders, custom stdio, shared environments, resource limits, and transfer lists are unsupported. Worker argument strings containing null bytes are also unsupported.
`node:worker_threads` runs workers as native threads. Worker entry points must resolve at build time: use a source-relative `new URL("./worker.ts", import.meta.url)`, the current module URL, an absolute path, or CommonJS `__filename`. Each worker initializes its own module state, event loop, environment snapshot, and reference-counted heap. Nested workers inherit their parent's environment snapshot. Supported constructor options are `workerData`, `argv`, and a `transferList` of MessagePorts; runtime paths, `eval`, loaders, custom stdio, shared environments, and resource limits are unsupported. Worker argument strings containing null bytes are also unsupported.

Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. Functions, proxies, other native handles, and nonempty transfer lists are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads).
Workers support `online`, `message`, `error`, and `exit` events, `postMessage`, `terminate`, `ref`, and `unref`. `new MessageChannel()` from `node:worker_threads` makes two entangled MessagePorts, in one thread or across threads once a port is transferred, with `receiveMessageOnPort`. Port messages arrive in Node's order: each port's queued messages as one batch before `setImmediate` callbacks, a port's own `'close'` after the poll phase, and the other port's `'close'` after its queued messages while the loop keeps running. A port's `onmessage` property, `addEventListener`, `receiveMessageOnPort(parentPort)`, and `BroadcastChannel` are unsupported. The parent port supports message listeners, `postMessage`, `close`, `start`, `ref`, `unref`, and `hasRef`. Messages clone primitives, plain object graphs, arrays, errors, ArrayBuffers, typed arrays, and DataViews, preserving cycles and aliases. Program class instances arrive as plain objects with their own data properties, without their prototype, methods, accessors, or private fields, as in Node. A MessagePort travels with a message when the message's transfer list (an array or `{ transfer }`) names it, and the sending port is unusable afterwards; a port in a message without that listing throws `DataCloneError`. Other transferable values, functions, proxies, and other native handles are refused. SharedArrayBuffers share their backing storage while retaining separate wrappers in each thread. Graphs made immutable with `@scriptc/threads` are passed by reference instead of cloned; see [Sharing Data Between Workers](/docs/threads).

SharedArrayBuffer storage is fixed-length. Atomics supports integer `load`, `store`, `exchange`, `compareExchange`, `add`, `sub`, `and`, `or`, and `xor` over 8-, 16-, and 32-bit integer arrays. `wait` and `notify` use Int32Array views; BigInt atomics, `waitAsync`, and growable buffers are unsupported. Atomics operations run as sequentially consistent atomic instructions and take no lock. Other reads and writes of shared storage access each element atomically; a copy, fill, or conversion that runs while another thread writes the same storage can observe some of those writes, as in Node. `terminate` unwinds at runtime checkpoints, including loops and parked atomic waits, without running JavaScript catch or finally blocks. Blocking native calls finish before the next checkpoint.

Expand Down
2 changes: 1 addition & 1 deletion docs/src/generated/node-v24-compatibility-meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "d91d55ce5aa065b163bb",
"artifactVersion": "aaeede2b0768674adb1e",
"rowCount": 3880
}
16 changes: 8 additions & 8 deletions docs/src/generated/node-v24-compatibility.json
Original file line number Diff line number Diff line change
Expand Up @@ -884,9 +884,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
"partial": 24,
"partial": 26,
"refused": 0,
"not-implemented": 42,
"not-implemented": 40,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
Expand Down Expand Up @@ -83081,9 +83081,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
Expand Down Expand Up @@ -84024,9 +84024,9 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the documented scriptc module-loader subset.",
"verification": "test-backed"
}
},
{
Expand Down
36 changes: 22 additions & 14 deletions internal/compatibility/generated/node-v24-backlog.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"tierItemCount": 3472,
"tiers": {
"static": {
"audit-partial": 1064,
"verify-gap": 2281,
"audit-partial": 1066,
"verify-gap": 2279,
"replace-refusal": 40,
"classify": 2,
"implement": 85
Expand Down Expand Up @@ -84003,12 +84003,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:worker_threads",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:worker_threads.MessageChannel",
"tests": [
"tests/corpus/5793-worker-message-channel.ts",
"tests/corpus/5796-worker-message-channel.cjs",
"tests/corpus/5797-worker-message-port-transfer.ts"
]
}
}
},
Expand Down Expand Up @@ -85092,12 +85096,16 @@
"priority": "high",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:worker_threads",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "compiler-feature:worker_threads.worker_threads.receiveMessageOnPort",
"tests": [
"tests/corpus/5793-worker-message-channel.ts",
"tests/corpus/5796-worker-message-channel.cjs",
"tests/corpus/5797-worker-message-port-transfer.ts"
]
}
}
},
Expand Down
25 changes: 19 additions & 6 deletions internal/compatibility/generated/node-v24-internal.json
Original file line number Diff line number Diff line change
Expand Up @@ -941,9 +941,9 @@
"apiEntries": 66,
"static": {
"supported": 0,
"partial": 24,
"partial": 26,
"refused": 0,
"not-implemented": 42,
"not-implemented": 40,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
Expand Down Expand Up @@ -112566,6 +112566,9 @@
"status": "partial",
"evidence": "derived:descendants",
"tests": [
"tests/corpus/5793-worker-message-channel.ts",
"tests/corpus/5796-worker-message-channel.cjs",
"tests/corpus/5797-worker-message-port-transfer.ts",
"tests/corpus/native-worker-clone-errors.ts",
"tests/corpus/native-worker-commonjs.cjs",
"tests/corpus/native-worker-delayed-messages.ts",
Expand Down Expand Up @@ -112834,8 +112837,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:worker_threads"
"status": "partial",
"evidence": "compiler-feature:worker_threads.MessageChannel",
"tests": [
"tests/corpus/5793-worker-message-channel.ts",
"tests/corpus/5796-worker-message-channel.cjs",
"tests/corpus/5797-worker-message-port-transfer.ts"
]
},
"anchorSource": "exact"
},
Expand Down Expand Up @@ -113906,8 +113914,13 @@
"inherited": true
},
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:worker_threads"
"status": "partial",
"evidence": "compiler-feature:worker_threads.worker_threads.receiveMessageOnPort",
"tests": [
"tests/corpus/5793-worker-message-channel.ts",
"tests/corpus/5796-worker-message-channel.cjs",
"tests/corpus/5797-worker-message-port-transfer.ts"
]
},
"anchorSource": "exact"
},
Expand Down
1 change: 1 addition & 0 deletions internal/compatibility/static-support.json
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
{"chapter": "worker_threads", "symbols": ["port.close", "port.start", "port.ref", "port.unref", "port.hasRef"], "status": "partial", "evidence": ["tests/corpus/native-worker-port-lifetime.ts"]},
{"chapter": "worker_threads", "parent": "Class: `Worker`", "signatures": ["Event: `'online'`", "Event: `'message'`", "Event: `'error'`", "Event: `'exit'`"], "status": "partial", "evidence": ["tests/corpus/native-worker-references.ts", "tests/corpus/native-worker-error.ts", "tests/corpus/native-worker-exit-listeners.ts"]},
{"chapter": "worker_threads", "parent": "Class: `MessagePort`", "signatures": ["Event: `'message'`", "Event: `'close'`"], "status": "partial", "evidence": ["tests/corpus/native-worker-port-lifetime.ts", "tests/corpus/native-worker-messages.ts"]},
{"chapter": "worker_threads", "symbols": ["MessageChannel", "worker_threads.receiveMessageOnPort"], "status": "partial", "evidence": ["tests/corpus/5793-worker-message-channel.ts", "tests/corpus/5796-worker-message-channel.cjs", "tests/corpus/5797-worker-message-port-transfer.ts"]},
{"chapter": "util", "symbols": ["util.types.isAnyArrayBuffer", "util.types.isArrayBuffer", "util.types.isArrayBufferView", "util.types.isDataView", "util.types.isTypedArray", "util.types.isUint8Array", "util.types.isUint8ClampedArray", "util.types.isUint16Array", "util.types.isUint32Array", "util.types.isInt8Array", "util.types.isInt16Array", "util.types.isInt32Array", "util.types.isFloat32Array", "util.types.isFloat64Array", "util.types.isMap", "util.types.isSet", "util.types.isWeakMap", "util.types.isWeakSet", "util.types.isRegExp", "util.types.isNativeError", "util.types.isPromise", "util.types.isProxy", "util.types.isAsyncFunction", "util.types.isGeneratorFunction", "util.types.isGeneratorObject"], "status": "partial", "evidence": ["tests/corpus/node24-util-types.ts", "tests/corpus/node24-util-types.js", "tests/corpus/node24-util-types.cjs", "tests/fixtures/node-types/util-types.ts"]},
{ "chapter": "util", "symbols": ["util.styleText"], "status": "partial", "evidence": ["tests/corpus/node24-style-text.ts", "tests/corpus/node24-style-text-validation.js", "tests/corpus/node24-style-text-environment.js", "tests/corpus/node24-style-text.cjs", "tests/fixtures/node-types/style-text.ts"] },
{ "chapter": "util", "symbols": ["util.isDeepStrictEqual"], "status": "partial", "evidence": ["tests/corpus/node24-deep-equality.ts", "tests/corpus/node24-deep-equality.cjs", "tests/corpus/node24-deep-equality-properties.js", "tests/fixtures/node-types/deep-equality.ts", "packages/compiler/test/deep-equality.test.ts"] },
Expand Down
7 changes: 7 additions & 0 deletions packages/compiler/ambient/scriptc-node-fallback.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5721,6 +5721,13 @@ declare module "worker_threads" {
removeAllListeners(event?: string): this;
listenerCount(event: string, listener?: (...args: any[]) => void): number;
}
/* Two entangled ports; a port moves to another thread through a
* postMessage or workerData transferList. */
export class MessageChannel {
readonly port1: MessagePort;
readonly port2: MessagePort;
}
export function receiveMessageOnPort(port: MessagePort): { message: any } | undefined;
export const parentPort: MessagePort | null;
export const workerData: any;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/backend/llvm/lib-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,8 @@ export const LIB_FN_SYMS: Record<string, string> = {
"worker.root": "scr_worker_root",
"worker.data": "scr_worker_data",
"worker.parentPort": "scr_worker_parent_port",
"worker.channel": "scr_worker_message_channel",
"worker.receiveMessageOnPort": "scr_worker_receive_message_on_port",
"process.stdinDestroy": "scr_process_stdin_destroy",
"process.stdinSetRawMode": "scr_process_stdin_set_raw_mode",
// node:events EventEmitter (scr_events_emitter.c): receivers borrowed,
Expand Down Expand Up @@ -1356,6 +1358,8 @@ export const USES_TIMERS_LIB_FNS = new Set<string>([
"util.debuglog",
"worker.new",
"worker.parentPort",
"worker.channel",
"worker.receiveMessageOnPort",
"process.stdio",
"fetch.start",
"fetch.input",
Expand Down
4 changes: 4 additions & 0 deletions packages/compiler/src/frontend/lowering/builtin-calls.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { lowerReceiveMessageOnPort } from "./builtins/workers.js";
import * as ts from "../ts7/adapter.js";
import { dynUndefinedExpr } from "../../ir/build.js";
import { DYN, type IrExpr, type SrcLoc } from "../../ir/ir.js";
Expand Down Expand Up @@ -88,6 +89,9 @@ export function lowerBuiltinCall(
if (timersInterval) return timersInterval;
const nodeModuleServed = lowerNodeModuleCall(lowerer, expr, bi, loc);
if (nodeModuleServed) return nodeModuleServed;
if (bi.module === "worker_threads" && bi.member === "receiveMessageOnPort") {
return lowerReceiveMessageOnPort(lowerer, expr);
}
const builtinFn = builtinModuleFnOf(lowerer, bi.module, bi.member);
if (!builtinFn) {
lowerer.noLowering(
Expand Down
68 changes: 68 additions & 0 deletions packages/compiler/src/frontend/lowering/builtins/workers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,71 @@ export function lowerWorkerNew(lowerer: Lowerer, expression: ts.NewExpression):
loc,
};
}

/** True when `node`'s type is worker_threads' `name` class (also reached
* through its global). */
function isWorkerThreadsClass(lowerer: Lowerer, node: ts.Expression, name: string): boolean {
const t = lowerer.checker.getTypeAtLocation(node);
const sym = t.getSymbol();
if (sym?.name !== name) return false;
return lowerer.checker.declarationsOf(sym).some((d) => {
if (!ts.isClassDeclaration(d) && !ts.isInterfaceDeclaration(d)) return false;
if (!lowerer.isStdlibFile(d.getSourceFile())) return false;
let up: ts.Node | undefined = d.parent;
while (up) {
if (ts.isModuleDeclaration(up) && ts.isStringLiteral(up.name))
return up.name.text === "worker_threads" || up.name.text === "node:worker_threads";
up = up.parent;
}
return false;
});
}

/** new MessageChannel(): { port1, port2 }, two entangled MessagePorts. */
export function lowerMessageChannelNew(
lowerer: Lowerer,
expression: ts.NewExpression,
): IrExpr | null {
if (!isWorkerThreadsClass(lowerer, expression, "MessageChannel")) return null;
if ((expression.arguments ?? []).length > 0) {
lowerer.noLowering(
"MessageChannel constructor arguments",
expression,
"use new MessageChannel()",
);
}
if (lowerer.targetPlatform === "wasi") {
lowerer.noLowering(
"MessageChannel in this target",
expression,
"use a native static executable",
);
}
lowerer.usesWorkers = true;
return { kind: "libCall", fn: "worker.channel", args: [], type: DYN, loc: locOf(expression) };
}

/** receiveMessageOnPort(port): { message } or undefined. */
export function lowerReceiveMessageOnPort(lowerer: Lowerer, call: ts.CallExpression): IrExpr {
const args = call.arguments;
if (args.length > 1 || args.some(ts.isSpreadElement)) {
lowerer.noLowering(`receiveMessageOnPort with ${args.length} arguments`, call);
}
lowerer.usesWorkers = true;
const loc = locOf(call);
const port: IrExpr = args[0]
? lowerer.lowerExprExpecting(args[0], DYN)
: {
kind: "dynFrom",
value: { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc },
type: DYN,
loc,
};
return {
kind: "libCall",
fn: "worker.receiveMessageOnPort",
args: [port],
type: DYN,
loc,
};
}
4 changes: 3 additions & 1 deletion packages/compiler/src/frontend/lowering/lower-classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ import { bufEncoding } from "./containers/bytes.js";
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
import { lowerSearchParamsNew } from "./builtins/url.js";
import { lowerTextCodecNew } from "./builtins/text-codecs.js";
import { lowerWorkerNew } from "./builtins/workers.js";
import { lowerMessageChannelNew, lowerWorkerNew } from "./builtins/workers.js";
import {
unsupportedPackageValueDiag,
unsupportedDiag,
Expand Down Expand Up @@ -8150,6 +8150,8 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
const loc = locOf(expr);
const worker = lowerWorkerNew(lowerer, expr);
if (worker) return worker;
const channel = lowerMessageChannelNew(lowerer, expr);
if (channel) return channel;
let intlConstructor = expr.expression;
if (ts.isIdentifier(intlConstructor)) {
const symbol = lowerer.resolveValueSymbol(intlConstructor);
Expand Down
38 changes: 37 additions & 1 deletion packages/compiler/src/frontend/type-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1234,7 +1234,9 @@ function isWorkerHandleSymbol(
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
): boolean {
return (
(symbol?.name === "Worker" || symbol?.name === "MessagePort") &&
(symbol?.name === "Worker" ||
symbol?.name === "MessagePort" ||
symbol?.name === "MessageChannel") &&
ctx.checker
.declarationsOf(symbol)
.some(
Expand All @@ -1246,6 +1248,37 @@ function isWorkerHandleSymbol(
);
}

/** The `{ message: any }` object type (or its union with undefined) that
* worker_threads declares as receiveMessageOnPort's result. */
function isReceiveMessageResult(
type: ts.Type,
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
): boolean {
const members = type.isUnionType() ? ts.constituentTypes(type) : [type];
let found = false;
for (const member of members) {
if (member.flags & ts.TypeFlags.Undefined) continue;
const decl = member.getSymbol()
? ctx.checker.declarationsOf(member.getSymbol()!)[0]
: undefined;
let fn: ts.Node | undefined = decl?.parent;
while (fn && ts.isUnionTypeNode(fn)) fn = fn.parent;
if (
!decl ||
!ts.isTypeLiteralNode(decl) ||
!fn ||
!ts.isFunctionDeclaration(fn) ||
fn.name?.text !== "receiveMessageOnPort" ||
!ctx.isStdlibFile(decl.getSourceFile()) ||
!isDeclaredInAmbientModule(fn, "worker_threads")
) {
return false;
}
found = true;
}
return found;
}

/** True for a type built only from worker handles, null/undefined, and
* arrays of those: values that keep the live DYN representation. */
export function isWorkerHandleType(
Expand Down Expand Up @@ -1330,6 +1363,9 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
const moduleId = ctx.moduleNamespaceId?.(type) ?? null;
if (moduleId !== null) return { kind: "moduleNs", moduleId };
}
// worker_threads.receiveMessageOnPort's `{ message: any } | undefined`:
// the runtime's { message } object, a checked-dynamic value.
if (isReceiveMessageResult(type, ctx)) return DYN;
// @types/node spells child_process.ExecFileException as an intersection
// type alias over two Error-derived metadata interfaces. The value is
// still one ordinary Error; recognize the owned alias before generic
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/ir/builtin-effects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"worker.new",
"worker.data",
"worker.parentPort",
"worker.channel",
"worker.receiveMessageOnPort",
"process.exit",
"text.encodeInto",
"buffer.isAscii",
Expand Down
Loading
Loading