/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/node/polyfills/worker_threads.ts
2 335 строк
78 KB
Kenta Moriuchi
chore: make prefer-primordials lint an internal plugin (#35978)
23 июл 2026, 11:53
Не верифицирован
23 июл 2026, 11:53
5b55d08
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. // Copyright Joyent and Node contributors. All rights reserved. MIT license. (function () { const { core, internals, primordials } = __bootstrap; const { op_create_worker, op_host_get_worker_cpu_usage, op_host_post_message, op_host_post_message_raw, op_host_recv_ctrl, op_host_recv_message, op_host_recv_message_sync, op_host_terminate_worker, op_mark_as_untransferable, op_message_port_post_message_raw, op_message_port_recv_message, op_message_port_recv_message_sync, op_node_worker_thread_post_message, op_node_worker_thread_recv_message, op_node_worker_thread_register, op_node_worker_thread_set_listener_count, op_worker_get_resource_limits, op_worker_threads_filename, } = core.ops; const { deserializeJsMessageData, deserializeMessageData, isUncloneable, markAsUncloneable: webMarkAsUncloneable, MessageChannel, MessagePort, MessagePortIdSymbol, MessagePortPrototype, MessagePortReceiveMessageOnPortSymbol, nodeWorkerThreadCloseCb, refMessagePort, serializeJsMessageData, serializeMessageData, unrefParentPort, } = core.loadExtScript("ext:deno_web/13_message_port.js"); const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js"); const { notImplemented } = core.loadExtScript("ext:deno_node/_utils.ts"); const { ERR_CLOSED_MESSAGE_PORT, ERR_CONSTRUCT_CALL_REQUIRED, ERR_INVALID_ARG_TYPE, ERR_INVALID_URL_SCHEME, ERR_OUT_OF_RANGE, ERR_WORKER_INVALID_EXEC_ARGV, ERR_WORKER_MESSAGING_ERRORED, ERR_WORKER_MESSAGING_FAILED, ERR_WORKER_MESSAGING_SAME_THREAD, ERR_WORKER_MESSAGING_TIMEOUT, ERR_WORKER_NOT_RUNNING, ERR_WORKER_PATH, } = core.loadExtScript("ext:deno_node/internal/errors.ts"); const { validateArray, validateInteger, validateObject, } = core.loadExtScript("ext:deno_node/internal/validators.mjs"); const { EventEmitter } = core.loadExtScript("ext:deno_node/_events.mjs"); const { channel: createDiagnosticsChannel } = core.loadExtScript( "ext:deno_node/diagnostics_channel.js", ); const workerThreadsChannel = createDiagnosticsChannel("worker_threads"); const lazyStream = core.createLazyLoader("node:stream"); const { BroadcastChannel: WebBroadcastChannel, refBroadcastChannel, } = core.loadExtScript("ext:deno_web/01_broadcast_channel.js"); const { untransferableSymbol } = core.loadExtScript( "ext:deno_node/internal_binding/util.ts", ); const lazyProcess = core.createLazyLoader("node:process"); const lazyUrl = core.createLazyLoader("node:url"); const lazyModule = core.createLazyLoader("node:module"); // Eagerly bind: node:process is in the eager ESM bundle so this is cheap, and // it's used pervasively throughout this module. `Readable`/`Writable`, // `fileURLToPath`, and `createRequire` stay deferred via their `lazy*` // loaders. const { ArrayBufferIsView, ArrayIsArray, ArrayPrototypeIndexOf, ArrayPrototypePush, ArrayPrototypeSlice, ArrayPrototypeSplice, Error, EvalError, FunctionPrototypeApply, FunctionPrototypeBind, FunctionPrototypeCall, NumberIsFinite, NumberIsNaN, ObjectAssign, ObjectCreate, ObjectDefineProperty, ObjectHasOwn, ObjectKeys, ObjectPrototypeIsPrototypeOf, Promise, PromisePrototypeThen, PromiseReject, PromiseResolve, queueMicrotask, RangeError, ReferenceError, SafeMap, SafeRegExp, SafeSet, String, StringPrototypeIndexOf, StringPrototypeSlice, StringPrototypeSplit, StringPrototypeStartsWith, StringPrototypeTrim, Symbol, SymbolAsyncDispose, SymbolFor, SymbolIterator, SyntaxError, TypeError, URIError, Float64Array, } = primordials; // Map error names to native constructors so that worker error events // preserve err.constructor (e.g. SyntaxError, TypeError). const nativeErrorConstructors: Record<string, ErrorConstructor> = { __proto__: null as unknown as ErrorConstructor, Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, }; const workerCpuUsageBuffer = new Float64Array(2); const debugWorkerThreads = false; function debugWT(...args) { if (debugWorkerThreads) { // deno-lint-ignore deno-internal/prefer-primordials no-console console.log(...args); } } interface WorkerOnlineMsg { type: "WORKER_ONLINE"; } function isWorkerOnlineMsg(data: unknown): data is WorkerOnlineMsg { return typeof data === "object" && data !== null && ObjectHasOwn(data, "type") && (data as { "type": unknown })["type"] === "WORKER_ONLINE"; } interface WorkerStdioMsg { type: "WORKER_STDERR" | "WORKER_STDOUT"; // deno-lint-ignore no-explicit-any data: any; } interface WorkerStdinMsg { type: "WORKER_STDIN"; // deno-lint-ignore no-explicit-any data: any; } interface WorkerStdinEndMsg { type: "WORKER_STDIN_END"; } function isWorkerStdinMsg(data: unknown): data is WorkerStdinMsg { return typeof data === "object" && data !== null && ObjectHasOwn(data, "type") && (data as { "type": unknown })["type"] === "WORKER_STDIN"; } function isWorkerStdinEndMsg(data: unknown): data is WorkerStdinEndMsg { return typeof data === "object" && data !== null && ObjectHasOwn(data, "type") && (data as { "type": unknown })["type"] === "WORKER_STDIN_END"; } function isWorkerStderrMsg(data: unknown): data is WorkerStdioMsg { return typeof data === "object" && data !== null && ObjectHasOwn(data, "type") && (data as { "type": unknown })["type"] === "WORKER_STDERR"; } function isWorkerStdoutMsg(data: unknown): data is WorkerStdioMsg { return typeof data === "object" && data !== null && ObjectHasOwn(data, "type") && (data as { "type": unknown })["type"] === "WORKER_STDOUT"; } // Flags that are valid Node.js environment flags but not allowed in workers // because they affect per-process state. const workerDisallowedFlags = new SafeSet([ "--title", "--redirect-warnings", "--trace-event-file-pattern", "--trace-event-categories", "--trace-events-enabled", "--diagnostic-dir", "--report-signal", "--report-filename", "--report-dir", "--report-directory", "--report-compact", "--report-on-signal", "--report-on-fatalerror", "--report-uncaught-exception", ]); // V8 profiling flags Node accepts in worker execArgv. Deno doesn't wire up // the underlying profiling integration, but rejecting them with // ERR_WORKER_INVALID_EXEC_ARGV breaks scripts that pass them unconditionally, // so accept and ignore. const workerSilentlyIgnoredFlags = new SafeSet([ "--heap-prof", "--heap-prof-interval", "--heap-prof-name", "--heap-prof-dir", "--cpu-prof", "--cpu-prof-interval", "--cpu-prof-name", "--cpu-prof-dir", ]); interface WorkerOptions { // only for typings argv?: unknown[]; env?: Record<string, unknown>; execArgv?: string[]; stdin?: boolean; stdout?: boolean; stderr?: boolean; trackUnmanagedFds?: boolean; resourceLimits?: { maxYoungGenerationSizeMb?: number; maxOldGenerationSizeMb?: number; codeRangeSizeMb?: number; stackSizeMb?: number; }; eval?: boolean; transferList?: Transferable[]; workerData?: unknown; name?: string; } const privateWorkerRef = Symbol("privateWorkerRef"); class NodeWorker extends EventEmitter { #id = 0; #name = ""; #refed = true; #messagePromise = undefined; #controlPromise = undefined; #messageLoopPromise = undefined; #workerOnline = false; #exited = false; // "RUNNING" | "CLOSED" | "TERMINATED" // "TERMINATED" means that any controls or messages received will be // discarded. "CLOSED" means that we have received a control // indicating that the worker is no longer running, but there might // still be messages left to receive. #status = "RUNNING"; // https://nodejs.org/api/worker_threads.html#workerthreadid threadId = this.#id; // https://nodejs.org/api/worker_threads.html#workerresourcelimits resourceLimits: WorkerOptions["resourceLimits"] = {}; // https://nodejs.org/api/worker_threads.html#workerstdin // deno-lint-ignore no-explicit-any stdin: any = null; // https://nodejs.org/api/worker_threads.html#workerstdout // deno-lint-ignore no-explicit-any stdout: any = new (lazyStream().Readable)({ read() {} }); // https://nodejs.org/api/worker_threads.html#workerstderr // deno-lint-ignore no-explicit-any stderr: any = new (lazyStream().Readable)({ read() {} }); constructor(specifier: URL | string, options?: WorkerOptions) { super(); // Ensure the creating thread is wired up to send/receive cross-thread // `postMessageToThread` messages. On the main thread under the deferred // node bootstrap `__initWorkerThreads` (which normally does this) never // runs, so without this the main thread can neither be addressed by // `postMessageToThread(0, ...)` nor have its `workerMessage` listeners // counted. Idempotent, so the eager paths that already ran it are a // no-op here. setupCrossThreadMessaging(); if (options?.execArgv) { validateArray(options.execArgv, "options.execArgv"); if (options.execArgv.length > 0) { const invalidFlags = []; for (let i = 0; i < options.execArgv.length; i++) { const flag = options.execArgv[i]; // Items that don't start with '-' are arguments to the // preceding flag (e.g. "--conditions node"), not flags. if (!StringPrototypeStartsWith(flag, "-")) { continue; } const eqIdx = StringPrototypeIndexOf(flag, "="); const flagName = eqIdx === -1 ? flag : StringPrototypeSlice(flag, 0, eqIdx); if (workerSilentlyIgnoredFlags.has(flagName)) { continue; } if (!lazyProcess().default.allowedNodeEnvironmentFlags.has(flag)) { invalidFlags[invalidFlags.length] = flag; continue; } if (workerDisallowedFlags.has(flagName)) { invalidFlags[invalidFlags.length] = flag; } } if (invalidFlags.length > 0) { throw new ERR_WORKER_INVALID_EXEC_ARGV(invalidFlags); } } } if (options?.env) { const nodeOptions = options.env.NODE_OPTIONS; if (typeof nodeOptions === "string" && nodeOptions.length > 0) { // Parse NODE_OPTIONS and validate each flag const parts = StringPrototypeSplit( StringPrototypeTrim(nodeOptions), new SafeRegExp("\\s+"), ); let hasInvalid = false; for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (StringPrototypeStartsWith(part, "-")) { const eqIdx = StringPrototypeIndexOf(part, "="); const partName = eqIdx === -1 ? part : StringPrototypeSlice(part, 0, eqIdx); if (workerSilentlyIgnoredFlags.has(partName)) { continue; } if ( !lazyProcess().default.allowedNodeEnvironmentFlags.has(part) || workerDisallowedFlags.has(partName) ) { hasInvalid = true; break; } } } if (hasInvalid) { throw new ERR_WORKER_INVALID_EXEC_ARGV( [nodeOptions], "invalid NODE_OPTIONS env variable", ); } } } if (typeof specifier === "object") { if ( !(specifier.protocol === "data:" || specifier.protocol === "file:") ) { throw new ERR_INVALID_URL_SCHEME(["file", "data"]); } } else if (typeof specifier === "string" && !options?.eval) { // Node.js requires string specifiers to be absolute paths or // relative paths starting with './' or '../'. URLs passed as // strings must be wrapped with `new URL`. if ( StringPrototypeStartsWith(specifier, "file://") || StringPrototypeStartsWith(specifier, "data:") || StringPrototypeStartsWith(specifier, "http://") || StringPrototypeStartsWith(specifier, "https://") ) { throw new ERR_WORKER_PATH(specifier); } const path = specifier; if ( !StringPrototypeStartsWith(path, "/") && !StringPrototypeStartsWith(path, "./") && !StringPrototypeStartsWith(path, "../") && !StringPrototypeStartsWith(path, ".\\") && !StringPrototypeStartsWith(path, "..\\") ) { // On Windows, also allow drive-letter absolute paths (e.g. C:\...) const isWindowsAbsolute = path.length >= 3 && path[1] === ":" && (path[2] === "\\" || path[2] === "/"); if (!isWindowsAbsolute) { throw new ERR_WORKER_PATH(specifier); } } } // Serialize workerData before resolving the filename so that // DataCloneError is thrown before file-not-found errors, // matching Node.js behavior. // Handle the `env` option following Node.js semantics: // - undefined/null: snapshot current lazyProcess().default.env (isolated copy) // - SHARE_ENV: worker shares the parent's OS environment // - object: use that object, coercing values to strings // - anything else: throw ERR_INVALID_ARG_TYPE // See https://github.com/denoland/deno/issues/23522. let env_ = undefined; const envOpt = options?.env; if (envOpt != null && envOpt !== SHARE_ENV) { if (typeof envOpt !== "object") { throw new ERR_INVALID_ARG_TYPE( "options.env", ["object", "undefined", "null", "worker_threads.SHARE_ENV"], envOpt, ); } // Snapshot the provided env, coercing values to strings like Node.js. // This also handles passing `lazyProcess().default.env` (a Proxy in Deno) by // producing a plain object that can be structured-cloned. const envObj = {}; const keys = ObjectKeys(envOpt); for (let i = 0; i < keys.length; i++) { envObj[keys[i]] = String(envOpt[keys[i]]); } env_ = envObj; } else if (envOpt !== SHARE_ENV) { // Default: snapshot current lazyProcess().default.env so the worker gets an // isolated copy, not a live reference to the OS environment. // Wrap in try/catch because accessing lazyProcess().default.env requires // --allow-env permission in Deno. If unavailable, fall back to // shared OS env (env_ stays undefined). try { const envObj = {}; const keys = ObjectKeys(lazyProcess().default.env); for (let i = 0; i < keys.length; i++) { envObj[keys[i]] = lazyProcess().default.env[keys[i]]; } env_ = envObj; } catch { // No env permission - worker will share the OS environment. } } // When envOpt === SHARE_ENV, env_ stays undefined and the worker // will use the default lazyProcess().default.env backed by Deno.env (shared OS env). // Handle the `argv` option: must be an array or undefined. // Values are coerced to strings like Node.js does. let argv_: string[] | undefined = undefined; if (options?.argv != null) { if (!ArrayIsArray(options.argv)) { throw new ERR_INVALID_ARG_TYPE( "options.argv", "Array", options.argv, ); } argv_ = []; for (let i = 0; i < options.argv.length; i++) { argv_[i] = String(options.argv[i]); } } const resourceLimits_ = options?.resourceLimits ?? undefined; const serializedWorkerMetadata = serializeJsMessageData({ workerData: options?.workerData, environmentData: environmentData, env: env_, argv: argv_, execArgv: options?.execArgv ?? [], name: this.#name, isEval: !!options?.eval, isWorkerThread: true, hasStdin: !!options?.stdin, resourceLimits: resourceLimits_, }, options?.transferList ?? []); let sourceCode = ""; let hasSourceCode = false; if (options?.eval) { if (typeof specifier !== "string") { throw new TypeError( "The property 'options.eval' must be false when 'filename' is not a string.", ); } const code = specifier; // Node.js runs eval workers as CJS (sloppy mode). // Pass as source code for execute_script (sloppy mode). // `require` is already available from the Node worker bootstrap. // See: https://github.com/denoland/deno/issues/26739 sourceCode = `var __filename = ${ // deno-lint-ignore deno-internal/prefer-primordials JSON.stringify(lazyProcess().default.cwd() + "/[worker eval]")};\n` + `var __dirname = ${ // deno-lint-ignore deno-internal/prefer-primordials JSON.stringify(lazyProcess().default.cwd())};\n` + `var module = { exports: {} };\n` + `var exports = module.exports;\n` + code; hasSourceCode = true; specifier = `data:text/javascript,`; } else if ( !(typeof specifier === "object" && specifier.protocol === "data:") ) { // deno-lint-ignore deno-internal/prefer-primordials specifier = specifier.toString(); specifier = op_worker_threads_filename(specifier) ?? specifier; } // TODO(bartlomieu): this doesn't match the Node.js behavior, it should be // `[worker {threadId}] {name}` or empty string. let name = StringPrototypeTrim(options?.name ?? ""); if (options?.eval) { name = "[worker eval]"; } this.#name = name; const id = op_create_worker( { // deno-lint-ignore deno-internal/prefer-primordials specifier: specifier.toString(), hasSourceCode, sourceCode, permissions: null, name: this.#name, workerType: "node", closeOnIdle: true, resourceLimits: resourceLimits_, }, serializedWorkerMetadata, ); this.#id = id; this.threadId = id; if (resourceLimits_) { this.resourceLimits = { ...resourceLimits_ }; } if (options?.stdin) { // deno-lint-ignore no-this-alias const worker = this; this.stdin = new (lazyStream().Writable)({ write(chunk, _encoding, callback) { try { worker.postMessage({ type: "WORKER_STDIN", data: chunk, }); callback(); } catch (err) { callback(err); } }, final(callback) { try { worker.postMessage({ type: "WORKER_STDIN_END", }); callback(); } catch (err) { callback(err); } }, }); } this.#pollControl(); this.#messageLoopPromise = this.#pollMessages(); lazyProcess().default.nextTick(() => lazyProcess().default.emit("worker", this) ); if (workerThreadsChannel.hasSubscribers) { workerThreadsChannel.publish({ worker: this }); } } [privateWorkerRef](ref) { if (ref === this.#refed) { return; } this.#refed = ref; if (ref) { if (this.#controlPromise) { core.refOpPromise(this.#controlPromise); } if (this.#messagePromise) { core.refOpPromise(this.#messagePromise); } } else { if (this.#controlPromise) { core.unrefOpPromise(this.#controlPromise); } if (this.#messagePromise) { core.unrefOpPromise(this.#messagePromise); } } } #handleError(err) { this.emit("error", err); } #closeStdio() { if (!this.stdout.readableEnded) { FunctionPrototypeCall( lazyStream().Readable.prototype.push, this.stdout, null, ); } if (!this.stderr.readableEnded) { FunctionPrototypeCall( lazyStream().Readable.prototype.push, this.stderr, null, ); } } #pollControl = async () => { while (this.#status === "RUNNING") { this.#controlPromise = op_host_recv_ctrl(this.#id); if (!this.#refed) { core.unrefOpPromise(this.#controlPromise); } const { 0: type, 1: data } = await this.#controlPromise; // If terminate was called then we ignore all messages if (this.#status === "TERMINATED") { return; } switch (type) { case 1: { // TerminalError this.#status = "CLOSED"; if (this.listenerCount("error") > 0) { const errMsg = data.errorMessage ?? data.message; const errName = data.name; let err; if (errName === "ERR_WORKER_OUT_OF_MEMORY") { err = new Error(errMsg); err.code = errName; err.name = "Error"; } else { // Use the correct native error constructor so that // err.constructor matches (e.g. SyntaxError, TypeError). const Ctor = nativeErrorConstructors[errName] ?? Error; err = new Ctor(errMsg); } // Stack is unavailable from the worker context (e.g. prepareStackTrace // may have thrown). Match Node.js behavior of setting stack to undefined. err.stack = undefined; this.emit("error", err); } // Drain pending messages before closing stdio and emitting exit: // any stdio chunks still queued on the message channel must be // pushed onto the Readable streams *before* we EOF them, otherwise // we hit "stream.push() after EOF" if the Close control arrives // before the last stdout/stderr message (Node.js behavior). await this.#messageLoopPromise; this.#closeStdio(); this.resourceLimits = {}; if (!this.#exited) { this.#exited = true; this.emit("exit", data.exitCode ?? 1); } return; } case 2: { // Error this.#handleError(data); break; } case 3: { // Close debugWT(`Host got "close" message from worker: ${this.#name}`); this.#status = "CLOSED"; // Drain pending messages before closing stdio and emitting exit: // any stdio chunks still queued on the message channel must be // pushed onto the Readable streams *before* we EOF them, otherwise // we hit "stream.push() after EOF" if the Close control arrives // before the last stdout/stderr message (Node.js behavior). await this.#messageLoopPromise; this.#closeStdio(); this.resourceLimits = {}; if (!this.#exited) { this.#exited = true; this.emit("exit", data ?? 0); } return; } default: { throw new Error(`Unknown worker event: "${type}"`); } } } }; #dispatchWorkerThreadMessage(data) { let message, _transferables; try { const v = deserializeJsMessageData(data); message = v[0]; _transferables = v[1]; } catch (err) { this.emit("messageerror", err); return false; } if ( // only emit "online" event once, and since the message // has to come before user messages, we are safe to assume // it came from us !this.#workerOnline && isWorkerOnlineMsg(message) ) { this.#workerOnline = true; this.emit("online"); } else if (isWorkerStdoutMsg(message)) { FunctionPrototypeCall( lazyStream().Readable.prototype.push, this.stdout, message.data, ); } else if (isWorkerStderrMsg(message)) { FunctionPrototypeCall( lazyStream().Readable.prototype.push, this.stderr, message.data, ); } else { this.emit("message", message); } return true; } #pollMessages = async () => { while (this.#status !== "TERMINATED") { this.#messagePromise = op_host_recv_message(this.#id); if (!this.#refed) { core.unrefOpPromise(this.#messagePromise); } const data = await this.#messagePromise; if (this.#status === "TERMINATED" || data === null) { return; } if (!this.#dispatchWorkerThreadMessage(data)) return; // Drain messages already queued on the host side instead of taking the // async op + Promise path for each. The whole burst is processed within // this event-loop turn; the batch limit prevents starving the event loop // under a sustained flood. for (let i = 0; i < 1000 && this.#status !== "TERMINATED"; i++) { const syncData = op_host_recv_message_sync(this.#id); if (syncData === null) break; // Each message dispatch is its own task. Yield a microtask before // delivering this already-dequeued message so a handler that re-armed // itself in a microtask after the previous dispatch (e.g. an // `events.once` listener that re-attaches in a `.then`) is installed // first -- otherwise the message reaches the stale handler and is // lost. A synchronous checkpoint can't help: V8 won't run microtasks // reentrantly while we are already inside one. await new Promise((resolve) => queueMicrotask(() => resolve())); if (this.#status === "TERMINATED") return; if (!this.#dispatchWorkerThreadMessage(syncData)) return; } } }; postMessage(message, transferOrOptions = { __proto__: null }) { const prefix = "Failed to execute 'postMessage' on 'MessagePort'"; webidl.requiredArguments(arguments.length, 1, prefix); if (this.#status !== "RUNNING") return; // Fast path: no transferables if ( transferOrOptions === undefined || transferOrOptions === null || (arguments.length <= 1) ) { // Reject non-serializable values (e.g. URL) and per-instance // markAsUncloneable values before V8's serializer silently turns them // into `{}`, matching the web MessagePort path and Node's behavior. if (isUncloneable(message)) { throw new DOMException( "Cannot clone object of unsupported type.", "DataCloneError", ); } op_host_post_message_raw( this.#id, serializeMessageData(message), ); return; } message = webidl.converters.any(message); let options; if ( webidl.type(transferOrOptions) === "Object" && transferOrOptions !== undefined && transferOrOptions[SymbolIterator] !== undefined ) { const transfer = webidl.converters["sequence<object>"]( transferOrOptions, prefix, "Argument 2", ); options = { transfer }; } else { options = webidl.converters.StructuredSerializeOptions( transferOrOptions, prefix, "Argument 2", ); } const { transfer } = options; const data = serializeJsMessageData(message, transfer); op_host_post_message(this.#id, data); } // https://nodejs.org/api/worker_threads.html#workerterminate terminate() { if (this.#status === "TERMINATED") { return PromiseResolve(undefined); } this.#status = "TERMINATED"; op_host_terminate_worker(this.#id); this.#closeStdio(); if (!this.#exited) { this.#exited = true; this.emit("exit", 1); return PromiseResolve(1); } // Worker already exited - Node.js returns undefined in this case // (the internal handle is already null). return PromiseResolve(undefined); } async [SymbolAsyncDispose]() { await this.terminate(); } ref() { this[privateWorkerRef](true); } unref() { this[privateWorkerRef](false); } cpuUsage(prevValue?: { user: number; system: number }) { if (prevValue != null && !NumberIsNaN(prevValue)) { validateObject(prevValue, "prevValue"); if (typeof prevValue.user !== "number") { throw new ERR_INVALID_ARG_TYPE( "prevValue.user", "number", prevValue.user, ); } if (!NumberIsFinite(prevValue.user) || prevValue.user < 0) { throw new ERR_OUT_OF_RANGE( "prevValue.user", ">= 0 && <= 2^53", prevValue.user, ); } if (typeof prevValue.system !== "number") { throw new ERR_INVALID_ARG_TYPE( "prevValue.system", "number", prevValue.system, ); } if (!NumberIsFinite(prevValue.system) || prevValue.system < 0) { throw new ERR_OUT_OF_RANGE( "prevValue.system", ">= 0 && <= 2^53", prevValue.system, ); } } if (this.#status !== "RUNNING") { return PromiseReject(new ERR_WORKER_NOT_RUNNING()); } op_host_get_worker_cpu_usage(this.#id, workerCpuUsageBuffer); const user = workerCpuUsageBuffer[0]; const system = workerCpuUsageBuffer[1]; if (prevValue) { return PromiseResolve({ user: user - prevValue.user, system: system - prevValue.system, }); } return PromiseResolve({ user, system }); } // https://nodejs.org/api/worker_threads.html#workerthreadname get threadName(): string | null { if (this.#exited) { return null; } return this.#name; } readonly getHeapSnapshot = () => notImplemented("Worker.prototype.getHeapSnapshot"); // fake performance readonly performance = globalThis.performance; } let isMainThread; let resourceLimits; let threadName: string = ""; let threadId = 0; let workerData: unknown = null; let environmentData = new SafeMap(); // Forward-declared so `__initWorkerThreads` can sync the post-init values // of `parentPort`/`threadId`/etc. onto it as data properties. The tail of // this IIFE fills in the rest (classes, helper functions, initial values // for the mutable fields) and returns this object as the polyfill's // exports. Synthetic ESM `import` snapshots these properties at first // import - which is post-bootstrap - so consumers see the post-init // values. CJS `require("worker_threads")` reads the same object. const exportsObj: Record<string, unknown> = {}; // Like https://github.com/nodejs/node/blob/48655e17e1d84ba5021d7a94b4b88823f7c9c6cf/lib/internal/event_target.js#L611 interface NodeEventTarget extends Pick< EventEmitter, "eventNames" | "listenerCount" | "emit" | "removeAllListeners" > { setMaxListeners(n: number): void; getMaxListeners(): number; // deno-lint-ignore no-explicit-any off(eventName: string, listener: (...args: any[]) => void): NodeEventTarget; // deno-lint-ignore no-explicit-any on(eventName: string, listener: (...args: any[]) => void): NodeEventTarget; // deno-lint-ignore no-explicit-any once(eventName: string, listener: (...args: any[]) => void): NodeEventTarget; addListener: NodeEventTarget["on"]; removeListener: NodeEventTarget["off"]; } interface ParentPort extends NodeEventTarget { postMessage(message: unknown, transferOrOptions?: unknown): void; addEventListener( name: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions, ): void; removeEventListener( name: string, listener: EventListenerOrEventListenerObject, ): void; onmessage: ((ev: Event) => void) | null; // deno-lint-ignore no-explicit-any emit(...args: any[]): any; removeAllListeners(): void; setMaxListeners(n: number): void; getMaxListeners(): number; eventNames(): string[]; listenerCount(): number; unref(): void; ref(): void; [key: symbol]: unknown; } // deno-lint-ignore no-explicit-any let parentPort: ParentPort = null as any; internals.__initWorkerThreads = ( runningOnMainThread: boolean, workerId, maybeWorkerMetadata, moduleSpecifier, ) => { isMainThread = runningOnMainThread; internals.__isWorkerThread = !runningOnMainThread; if (isMainThread) { resourceLimits = {}; } if (!isMainThread) { // TODO(bartlomieju): this is a really hacky way to provide // require in worker_threads - this should be rewritten to use proper // CJS/ESM loading if (moduleSpecifier) { globalThis.require = lazyModule().createRequire( StringPrototypeStartsWith(moduleSpecifier, "data:") ? `${Deno.cwd()}/[worker eval]` : moduleSpecifier, ); } // Per-event-name listener map so the same listener reference // passed to parentPort.on twice for the same event registers only // once, matching Node MessagePort behavior. Maps are created on // demand since parentPort accepts any event name. // deno-lint-ignore no-explicit-any type ListenerMap = SafeMap<(...args: any[]) => void, (ev: any) => any>; const parentPortListeners: Record<string, ListenerMap> = ObjectCreate(null); const getParentPortListenerMap = (name: string): ListenerMap => { let map = parentPortListeners[name]; if (map === undefined) { map = new SafeMap(); parentPortListeners[name] = map; } return map; }; // Create parentPort as a separate object that delegates to the web // worker's native APIs. We capture the native methods here (before // user code runs) so that user code overriding globalThis.postMessage // (e.g. Emscripten/z3-solver) doesn't cause infinite recursion. const nativePostMessage = FunctionPrototypeBind( globalThis.postMessage, globalThis, ); const nativeAddEventListener = FunctionPrototypeBind( globalThis.addEventListener, globalThis, ); const nativeRemoveEventListener = FunctionPrototypeBind( globalThis.removeEventListener, globalThis, ); // Track message listener count to prevent double delivery. // When parentPort message listeners exist, suppress the web IDL // onmessage handler (globalThis.onmessage) since both would fire // for the same MessageEvent. let messageListenerCount = 0; // Track Web API message listeners to prevent underflow on // double-remove (same concern as the Node-style off() path). const webMessageListeners = new SafeSet(); parentPort = ObjectCreate(null) as ParentPort; parentPort.postMessage = function (message, transferOrOptions?) { return nativePostMessage(message, transferOrOptions); }; parentPort.addEventListener = function (name, listener, options?) { nativeAddEventListener(name, listener, options); if (name === "message" && !webMessageListeners.has(listener)) { webMessageListeners.add(listener); messageListenerCount++; } }; parentPort.removeEventListener = function (name, listener) { nativeRemoveEventListener(name, listener); if (name === "message" && webMessageListeners.has(listener)) { webMessageListeners.delete(listener); messageListenerCount--; } }; // Delegate parentPort.onmessage to globalThis.onmessage so that // setting parentPort.onmessage = handler works like the old code // where parentPort === globalThis. ObjectDefineProperty(parentPort, "onmessage", { __proto__: null, get() { return globalThis.onmessage; }, set(handler) { globalThis.onmessage = handler; }, configurable: true, enumerable: true, }); // Only intercept globalThis.onmessage for Node worker threads // (not plain Deno web workers) to prevent double message delivery // when both parentPort.on('message') and self.onmessage are set. if (maybeWorkerMetadata) { let storedOnmessage: ((ev: Event) => void) | null = null; // Dynamically add/remove the forwarding listener so we don't // keep a permanent "message" listener on globalThis. A permanent // listener would make hasMessageEventListener() always true and // prevent the worker from exiting. let onmessageForwarder: ((ev: Event) => void) | null = null; ObjectDefineProperty(globalThis, "onmessage", { __proto__: null, get() { return storedOnmessage; }, set(handler) { // Remove old forwarder if any if (onmessageForwarder) { nativeRemoveEventListener("message", onmessageForwarder); onmessageForwarder = null; } storedOnmessage = handler; // Add forwarder only when a handler is set if (typeof handler === "function") { onmessageForwarder = (ev: Event) => { if (messageListenerCount > 0) return; if (typeof storedOnmessage === "function") { storedOnmessage(ev); } }; nativeAddEventListener("message", onmessageForwarder); } }, configurable: true, enumerable: true, }); } threadId = workerId; let isWorkerThread = false; if (maybeWorkerMetadata) { const { 0: metadata, 1: _ } = maybeWorkerMetadata; workerData = metadata.workerData; environmentData = metadata.environmentData; isWorkerThread = metadata.isWorkerThread; threadName = metadata.name ?? ""; const env = metadata.env; if (env) { lazyProcess().default.env = env; } // Get resolved resource limits from the Rust side (includes V8 // defaults for unspecified fields), matching Node.js behavior. const resolvedLimits = op_worker_get_resource_limits(); if (resolvedLimits) { resourceLimits = resolvedLimits; } else { resourceLimits = {}; } // Set lazyProcess().default.argv for worker threads. // In Node.js, worker lazyProcess().default.argv is [execPath, scriptPath, ...argv]. if (isWorkerThread) { let scriptPath; if (metadata.isEval) { scriptPath = "[worker eval]"; } else if ( moduleSpecifier && StringPrototypeStartsWith(moduleSpecifier, "file:") ) { scriptPath = lazyUrl().fileURLToPath(moduleSpecifier); } else { scriptPath = moduleSpecifier ?? ""; } lazyProcess().default.argv = [ lazyProcess().default.execPath, scriptPath, ]; if (metadata.argv) { for (let i = 0; i < metadata.argv.length; i++) { lazyProcess().default.argv[i + 2] = metadata.argv[i]; } } // Set lazyProcess().default.execArgv for worker threads. if (metadata.execArgv) { lazyProcess().default.execArgv = metadata.execArgv; core.loadExtScript( "ext:deno_node/internal_binding/node_options.ts", ).setOptionSourceExecArgv(metadata.execArgv); for (let i = 0; i < metadata.execArgv.length; i++) { if (metadata.execArgv[i] === "--trace-warnings") { lazyProcess().default.traceProcessWarnings = true; } } } // Replace lazyProcess().default.stdin with a Readable that receives // data from the parent via WORKER_STDIN messages. if (metadata.hasStdin) { const workerStdin = new (lazyStream().Readable)({ read() {} }); lazyProcess().default.stdin = workerStdin; // Register an early listener to intercept stdin messages // before any user-registered handlers. Remove the listener // once stdin ends so the worker can exit cleanly. const stdinHandler = (ev) => { const msg = ev.data; if (isWorkerStdinMsg(msg)) { // deno-lint-ignore deno-internal/prefer-primordials workerStdin.push(msg.data); ev.stopImmediatePropagation(); } else if (isWorkerStdinEndMsg(msg)) { // deno-lint-ignore deno-internal/prefer-primordials workerStdin.push(null); parentPort.removeEventListener("message", stdinHandler); ev.stopImmediatePropagation(); } }; parentPort.addEventListener("message", stdinHandler); } // Forward stdout writes to the parent so worker.stdout // is readable from the host side. const origStdoutWrite = FunctionPrototypeBind( lazyProcess().default.stdout.write, lazyProcess().default.stdout, ); lazyProcess().default.stdout.write = function ( chunk, encoding, callback, ) { parentPort.postMessage({ type: "WORKER_STDOUT", data: chunk, }); return FunctionPrototypeCall( origStdoutWrite, lazyProcess().default.stdout, chunk, encoding, callback, ); }; // Forward stderr writes to the parent so worker.stderr // is readable from the host side. const origStderrWrite = FunctionPrototypeBind( lazyProcess().default.stderr.write, lazyProcess().default.stderr, ); lazyProcess().default.stderr.write = function ( chunk, encoding, callback, ) { parentPort.postMessage({ type: "WORKER_STDERR", data: chunk, }); return FunctionPrototypeCall( origStderrWrite, lazyProcess().default.stderr, chunk, encoding, callback, ); }; } } patchMessagePortIfFound(workerData); parentPort.off = parentPort.removeListener = function ( name, listener, ) { const map = parentPortListeners[name]; if (map !== undefined) { const wrapper = map.get(listener); if (wrapper !== undefined) { nativeRemoveEventListener(name, wrapper); map.delete(listener); if (name === "message") messageListenerCount--; } } return parentPort; }; parentPort.on = parentPort.addListener = function ( name, listener, ) { const map = getParentPortListenerMap(name); if (map.has(listener)) { // Same listener already registered for this event -- dedup to // match Node MessagePort behavior. return parentPort; } // deno-lint-ignore no-explicit-any const _listener = (ev: any) => { const message = ev.data; patchMessagePortIfFound(message); return listener(message); }; map.set(listener, _listener); nativeAddEventListener(name, _listener); if (name === "message") messageListenerCount++; return parentPort; }; parentPort.once = function (name, listener) { const map = getParentPortListenerMap(name); if (map.has(listener)) { return parentPort; } // deno-lint-ignore no-explicit-any const _listener = (ev: any) => { map.delete(listener); if (name === "message") messageListenerCount--; const message = ev.data; patchMessagePortIfFound(message); return listener(message); }; map.set(listener, _listener); nativeAddEventListener(name, _listener, { once: true }); if (name === "message") messageListenerCount++; return parentPort; }; // mocks parentPort.setMaxListeners = () => {}; parentPort.getMaxListeners = () => Infinity; parentPort.eventNames = () => [""]; parentPort.listenerCount = () => 0; parentPort.emit = () => notImplemented("parentPort.emit"); parentPort.removeAllListeners = () => notImplemented("parentPort.removeAllListeners"); nativeAddEventListener("offline", () => { parentPort.emit("close"); }); parentPort.unref = () => { parentPort[unrefParentPort] = true; // Also set on globalThis so runtime/js/99_main.js event loop // check (globalThis[unrefParentPort]) still works. globalThis[unrefParentPort] = true; }; parentPort.ref = () => { parentPort[unrefParentPort] = false; globalThis[unrefParentPort] = false; }; if (isWorkerThread) { // Notify the host that the worker is online parentPort.postMessage( { type: "WORKER_ONLINE", } satisfies WorkerOnlineMsg, ); } } // In Node, `globalThis.MessageChannel` / `globalThis.MessagePort` ARE // the `worker_threads` versions (so port instances have // `.on`/`.off`/`.emit`/etc., and `worker_threads.MessagePort` is // identity-equal to the global `MessagePort`). Tests in the // node_compat suite import individual helpers from `worker_threads` // and then use `MessageChannel` from the global scope, expecting // Node-style ports. Override the global symbols once at bootstrap so // this pattern works. `instanceof MessageChannel` / `instanceof // MessagePort` still work either way because both Node wrappers // route through the web prototype chain. try { ObjectDefineProperty(globalThis, "MessageChannel", { __proto__: null, value: NodeMessageChannel, writable: true, enumerable: false, configurable: true, }); ObjectDefineProperty(globalThis, "MessagePort", { __proto__: null, value: NodeMessagePort, writable: true, enumerable: false, configurable: true, }); } catch { // globalThis may be a sandbox without writable property descriptors; // ignore. } // Register this thread in the cross-thread registry so it can receive // `postMessageToThread` calls. setupCrossThreadMessaging(); // Sync the post-init values of the module-local lets onto the exports // object so first-import snapshots (via the synthetic ESM dispatch) and // `require("worker_threads")` reads both see the bootstrap-resolved // values rather than the initial placeholders. exportsObj.parentPort = parentPort; exportsObj.threadId = threadId; exportsObj.workerData = workerData; exportsObj.isMainThread = isMainThread; exportsObj.resourceLimits = resourceLimits; exportsObj.threadName = threadName; }; function getEnvironmentData(key: unknown) { return environmentData.get(key); } function setEnvironmentData(key: unknown, value?: unknown) { if (value === undefined) { environmentData.delete(key); } else { environmentData.set(key, value); } } const SHARE_ENV = SymbolFor("nodejs.worker_threads.SHARE_ENV"); // --------------------------------------------------------------- // Cross-thread messaging (`postMessageToThread` / `workerMessage`) // --------------------------------------------------------------- // // Each thread (main + every Node worker) registers itself with the Rust // side via `op_node_worker_thread_register`, which hands back a receive // channel. The send side lives in a process-wide table keyed by thread // id; any thread can post to any other thread by id, and the receive // loop here decodes the envelope and dispatches it as a `workerMessage` // event on `process`. // // A short envelope { sender, id, kind, payload, status? } wraps each // payload so we can carry the sender id alongside an ack message back to // the sender once the destination's `workerMessage` listeners have run. // The ack resolves the sender's pending promise (or rejects it on // listener throw / timeout), matching the Node.js semantics that // `postMessageToThread` only resolves once the destination has actually // handled the message. const kMessageKindData = 0; const kMessageKindAck = 1; const kAckStatusOk = 0; const kAckStatusError = 1; let threadMessageRid: number | undefined; let workerMessageListenerCount = 0; let nextThreadMessageId = 1; let crossThreadSetUp = false; let crossThreadMessagingSetUp = false; const pendingThreadMessages = new SafeMap< number, { resolve: (v: void) => void; reject: (err: unknown) => void; timer: number | undefined; } >(); function isCrossThreadEnvelope( v: unknown, ): v is { sender: number; id: number; kind: number; payload?: unknown; status?: number; } { return typeof v === "object" && v !== null && ObjectHasOwn(v, "__nodeWorkerMessage") && (v as { __nodeWorkerMessage: unknown }).__nodeWorkerMessage === true; } function deliverThreadMessageAck( id: number, status: number, ) { const pending = pendingThreadMessages.get(id); if (pending === undefined) return; pendingThreadMessages.delete(id); if (pending.timer !== undefined) clearTimeout(pending.timer); if (status === kAckStatusOk) { pending.resolve(undefined); } else { pending.reject(new ERR_WORKER_MESSAGING_ERRORED()); } } function sendAck( senderThreadId: number, id: number, status: number, ) { try { const envelope = { __nodeWorkerMessage: true, sender: threadId, id, kind: kMessageKindAck, status, }; const data = serializeJsMessageData(envelope, []); // Acks bypass the listener-count gate: the sender is, by definition, // waiting on this reply even though it has no `workerMessage` // listener of its own. op_node_worker_thread_post_message(senderThreadId, data, true); } catch { // Best-effort: a failed ack just means the sender will time out. } } async function pollCrossThreadMessages() { if (threadMessageRid === undefined) return; while (threadMessageRid !== undefined) { const promise = op_node_worker_thread_recv_message(threadMessageRid); // Don't let the cross-thread receive op keep the event loop alive // on its own: the thread should be free to exit when there's no // other work, even though we're permanently parked on this op. core.unrefOpPromise(promise); const data = await promise; if (data === null) return; let envelope; try { envelope = deserializeJsMessageData(data)[0]; } catch { continue; } if (!isCrossThreadEnvelope(envelope)) { continue; } if (envelope.kind === kMessageKindAck) { deliverThreadMessageAck(envelope.id, envelope.status ?? kAckStatusOk); continue; } // kMessageKindData - surface to user code as a `workerMessage` event // on `process`, then ack with whatever the listeners produced. let status = kAckStatusOk; try { lazyProcess().default.emit( "workerMessage", envelope.payload, envelope.sender, ); } catch { status = kAckStatusError; } sendAck(envelope.sender, envelope.id, status); } } // Eagerly observe listener add/remove for `workerMessage` so the count // is always in sync. Actual registration of the cross-thread resource // is deferred to `ensureCrossThreadMessaging` so threads that never // touch `postMessageToThread` don't carry an extra entry in // `core.resources()` (which several finalization tests assert on). function setupCrossThreadMessaging() { // Idempotent: under the deferred node bootstrap this is invoked lazily // from the `Worker` constructor on the main thread (where `initialize()` // -- and thus `__initWorkerThreads` -- never auto-runs), as well as // eagerly from `__initWorkerThreads` on workers and the eager main path. // Installing the `newListener` hook twice would double-count listeners. if (crossThreadMessagingSetUp) return; crossThreadMessagingSetUp = true; workerMessageListenerCount = lazyProcess().default.listenerCount?.("workerMessage") ?? 0; if (workerMessageListenerCount > 0) { ensureCrossThreadMessaging(); } lazyProcess().default.on("newListener", (eventName: string) => { if (eventName === "workerMessage") { workerMessageListenerCount++; ensureCrossThreadMessaging(); op_node_worker_thread_set_listener_count( threadId, workerMessageListenerCount, ); } }); lazyProcess().default.on("removeListener", (eventName: string) => { if (eventName === "workerMessage") { if (workerMessageListenerCount > 0) workerMessageListenerCount--; if (crossThreadSetUp) { op_node_worker_thread_set_listener_count( threadId, workerMessageListenerCount, ); } } }); } function ensureCrossThreadMessaging() { if (crossThreadSetUp) return; crossThreadSetUp = true; threadMessageRid = op_node_worker_thread_register(threadId); // Sync any listener count observed via `newListener` before the // registry slot existed. if (workerMessageListenerCount > 0) { op_node_worker_thread_set_listener_count( threadId, workerMessageListenerCount, ); } // The poll loop runs forever; the underlying receive op resolves to // null when the channel is torn down, ending the loop cleanly. pollCrossThreadMessages(); } // https://nodejs.org/api/worker_threads.html#workerpostmessagetothreadthreadid-value-transferlist-timeout function postMessageToThread( targetThreadId: number, value?: unknown, transferListOrTimeout?: unknown[] | number, maybeTimeout?: number, ): Promise<void> { let transferList: unknown[] | undefined; let timeout: number | undefined; try { validateInteger(targetThreadId, "threadId", 0); if (ArrayIsArray(transferListOrTimeout)) { transferList = transferListOrTimeout; timeout = maybeTimeout; } else if (typeof transferListOrTimeout === "number") { timeout = transferListOrTimeout; } else if (transferListOrTimeout !== undefined) { throw new ERR_INVALID_ARG_TYPE( "transferList", "Array", transferListOrTimeout, ); } if (timeout !== undefined) { validateInteger(timeout, "timeout", 0); } if (transferList !== undefined) { validateArray(transferList, "transferList"); } if (targetThreadId === threadId) { throw new ERR_WORKER_MESSAGING_SAME_THREAD(); } } catch (err) { return PromiseReject(err); } // Senders also need to be registered so the destination's ack can // find them. Cheap no-op after the first call. ensureCrossThreadMessaging(); const id = nextThreadMessageId++; let data; try { const envelope = { __nodeWorkerMessage: true, sender: threadId, id, kind: kMessageKindData, payload: value, }; data = serializeJsMessageData(envelope, transferList ?? []); } catch (err) { return PromiseReject(err); } // Register the pending entry *before* posting so that an ack delivered // synchronously by a single-threaded test runner still finds it. let pendingResolve!: (v: void) => void; let pendingReject!: (err: unknown) => void; const promise = new Promise<void>((resolve, reject) => { pendingResolve = resolve; pendingReject = reject; }); let timer: number | undefined; if (timeout !== undefined && timeout > 0) { timer = setTimeout(() => { if (pendingThreadMessages.has(id)) { pendingThreadMessages.delete(id); pendingReject(new ERR_WORKER_MESSAGING_TIMEOUT()); } }, timeout); } pendingThreadMessages.set(id, { resolve: pendingResolve, reject: pendingReject, timer, }); let result; try { result = op_node_worker_thread_post_message(targetThreadId, data, false); } catch (err) { pendingThreadMessages.delete(id); if (timer !== undefined) clearTimeout(timer); return PromiseReject(err); } // 0 = no such thread, 1 = destination has no `workerMessage` listener. if (result === 0 || result === 1) { pendingThreadMessages.delete(id); if (timer !== undefined) clearTimeout(timer); return PromiseReject(new ERR_WORKER_MESSAGING_FAILED()); } return promise; } function markAsUntransferable(obj: object) { // Primitives are silently ignored to match Node. if (obj === null) return; const t = typeof obj; if (t !== "object" && t !== "function") return; if (core.isArrayBuffer(obj)) { // Sets V8's detach key so postMessage(..., [ab]) throws DataCloneError // without detaching the buffer. op_mark_as_untransferable(obj as ArrayBuffer); } // For non-ArrayBuffer transferables (e.g. MessagePort) Node uses a // symbol marker that postMessage checks before adding to the transfer // list; we also set it on ArrayBuffer so `isMarkedAsUntransferable` // returns true without needing to poke at V8's detach key from JS. ObjectDefineProperty(obj, untransferableSymbol, { __proto__: null, value: true, enumerable: false, writable: false, configurable: false, }); } function isMarkedAsUntransferable(obj: unknown): boolean { if (obj === null) return false; const t = typeof obj; if (t !== "object" && t !== "function") return false; // Check own property -- Node's spec is explicit that the mark is *not* // inherited through the prototype chain. return ObjectHasOwn(obj as object, untransferableSymbol) && (obj as Record<symbol, unknown>)[untransferableSymbol] === true; } function markAsUncloneable(obj: unknown) { return webMarkAsUncloneable(obj); } const lazyVm = () => core.loadExtScript("ext:deno_node/vm.js"); // Move a MessagePort into a vm.Context. The returned object lives in the // target context (so its prototype chain is in that realm and it is *not* // `instanceof Object` in the calling realm). Messages arriving on the // underlying port are deserialized without the global host-object // deserializers, mirroring Node's behavior where the target context lacks // the JS classes registered in the source realm: any host object (e.g. a // crypto KeyObject) triggers `messageerror` with the // `ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE` code, while plain transferable // data is delivered as `message`. function moveMessagePortToContext( port: MessagePort, context: object, ): object { if (!(ObjectPrototypeIsPrototypeOf(MessagePortPrototype, port))) { throw new ERR_INVALID_ARG_TYPE("port", "MessagePort", port); } // Node checks closed-port state before vm.Context to give a clearer // error when the port is detached -- order matters for tests in the // node_compat suite that pass an empty {} as the context. const portId = port[MessagePortIdSymbol]; if (portId === null) { throw new ERR_CLOSED_MESSAGE_PORT(); } const vm = lazyVm(); if (!vm.isContext(context)) { throw new ERR_INVALID_ARG_TYPE("context", "vm.Context", context); } // Take ownership of the port: clear the id on the original so it can no // longer be used from this context. port[MessagePortIdSymbol] = null; // Allocate the wrapper inside the target context so its prototype chain // is the target realm's (i.e., `wrapper instanceof Object` in the caller // realm is false, matching Node). const wrapper = vm.runInContext("({})", context); wrapper.onmessage = null; wrapper.onmessageerror = null; let enabled = false; let closed = false; const dispatchMessageError = (err: object) => { if (typeof wrapper.onmessageerror === "function") { try { wrapper.onmessageerror({ data: err }); } catch { // Silently ignore - user handler errors must not break the loop. } } }; const dispatchMessage = (msg: unknown) => { if (typeof wrapper.onmessage === "function") { try { wrapper.onmessage({ data: msg }); } catch { // Silently ignore - user handler errors must not break the loop. } } }; wrapper.start = () => { if (enabled || closed) return; enabled = true; (async () => { while (!closed) { let data; try { data = await op_message_port_recv_message(portId); } catch { break; } if (data === null) break; // Intentionally deserialize without the host-object deserializers // registry. Any host object marker in the stream throws, which we // surface as `messageerror` per Node semantics. let message; try { // `data` is either the raw serialized buffer (no transferables) or a // `{ data, transferables }` object. Deserialize without the // host-object deserializers registry either way. message = deserializeMessageData( ArrayBufferIsView(data) ? data : data.data, false, ); } catch { const err = new Error( "Message could not be deserialized in the target context", ); // deno-lint-ignore no-explicit-any (err as any).code = "ERR_MESSAGE_TARGET_CONTEXT_UNAVAILABLE"; dispatchMessageError(err); continue; } dispatchMessage(message); } })(); }; wrapper.close = () => { if (closed) return; closed = true; try { core.close(portId); } catch { // ignore } }; wrapper.postMessage = (msg: unknown) => { if (closed) return; op_message_port_post_message_raw(portId, serializeMessageData(msg)); }; return wrapper; } /** * @param { MessagePort } port * @returns {object | undefined} */ function receiveMessageOnPort(port: MessagePort): object | undefined { if (!(ObjectPrototypeIsPrototypeOf(MessagePortPrototype, port))) { const err = new TypeError( 'The "port" argument must be a MessagePort instance', ); err["code"] = "ERR_INVALID_ARG_TYPE"; throw err; } port[MessagePortReceiveMessageOnPortSymbol] = true; const data = op_message_port_recv_message_sync(port[MessagePortIdSymbol]); if (data === null) return undefined; const message = deserializeJsMessageData(data)[0]; patchMessagePortIfFound(message); return { message }; } // Implemented as a function (not a class) so calling without `new` throws // our Node-style `ERR_CONSTRUCT_CALL_REQUIRED` instead of V8's default // "Class constructor X cannot be invoked without 'new'" -- the node_compat // suite asserts the specific code/constructor for both forms. // deno-lint-ignore no-explicit-any function NodeMessageChannel(this: any) { if (new.target === undefined) { throw new ERR_CONSTRUCT_CALL_REQUIRED("MessageChannel"); } { const channel = new MessageChannel(); const port1 = webMessagePortToNodeMessagePort(channel.port1); const port2 = webMessagePortToNodeMessagePort(channel.port2); // The web MessageChannel.prototype defines port1/port2 as getter-only // accessors, so a plain `this.port1 = ...` triggers a setter trap. Use // defineProperty to shadow the prototype accessors with own data // properties that hold our Node-style ports. ObjectDefineProperty(this, "port1", { __proto__: null, value: port1, writable: true, enumerable: true, configurable: true, }); ObjectDefineProperty(this, "port2", { __proto__: null, value: port2, writable: true, enumerable: true, configurable: true, }); // When one port is closed, force the paired port to start its recv // loop so it drains any in-flight messages and observes the channel // tear-down via its `end-of-stream` -> closeCb path. This also makes // sure the paired port's close event fires even if no message // listener was attached. const origClose1 = FunctionPrototypeBind(port1.close, port1); const origClose2 = FunctionPrototypeBind(port2.close, port2); port1.close = (cb) => { origClose1(cb); try { port2.start(); } catch { // start may throw if the port is already detached -- safe to // ignore. } }; port2.close = (cb) => { origClose2(cb); try { port1.start(); } catch { // see above } }; } } const nodePortListenersSymbol = Symbol("nodePortListeners"); // Reuse the web MessageChannel's prototype so `instanceof MessageChannel` // still works regardless of whether the user got the global MessageChannel // (NodeMessageChannel) or the web one. Port1/port2 own properties on each // instance shadow the prototype's getters. // deno-lint-ignore no-explicit-any (NodeMessageChannel as any).prototype = MessageChannel.prototype; function webMessagePortToNodeMessagePort(port: MessagePort) { // Patch idempotently: a port can be reached more than once through // patchMessagePortIfFound (e.g. when included in a nested message // payload). Re-patching would discard tracked listeners. // deno-lint-ignore no-explicit-any if ((port as any)[nodePortListenersSymbol]) return port; // Track listeners per port and per event name so the same handler // reference passed to `on` twice registers only once (Node parity), // and `off` can locate the wrapper that was actually attached. // // Maps are created on demand: Node's MessagePort, like EventEmitter, // accepts arbitrary event names so `port.on('foo', fn) + port.emit('foo')` // works. The Web-style events (`message`, `messageerror`, `close`) are // routed through addEventListener; everything else falls through to a // plain EventEmitter-like dispatch that runs the registered functions // with whatever args `emit` was given. // deno-lint-ignore no-explicit-any type ListenerMap = SafeMap<(...args: any[]) => void, (ev: any) => any>; const portListeners: Record<string | symbol, ListenerMap> = ObjectCreate( null, ); const getListenerMap = (name: string | symbol): ListenerMap => { let map = portListeners[name as string]; if (map === undefined) { map = new SafeMap(); portListeners[name as string] = map; } return map; }; // Listener bags for arbitrary event names (anything other than the three // Web event names) -- they don't go through EventTarget's dispatch path. const customEventListeners = new SafeMap< string | symbol, // deno-lint-ignore no-explicit-any Array<(...a: any[]) => void> >(); const isWebEvent = (name: string | symbol): boolean => name === "message" || name === "messageerror" || name === "close"; ObjectDefineProperty(port, nodePortListenersSymbol, { __proto__: null, value: portListeners, writable: false, enumerable: false, configurable: false, }); port.on = port.addListener = function (this: MessagePort, name, listener) { if (!isWebEvent(name)) { // EventEmitter-like dispatch path for arbitrary event names. // `port.on('foo', fn) + port.emit('foo', 'bar')` works like Node's // MessagePort, which mixes in EventEmitter semantics on top of the // Web EventTarget interface. let arr = customEventListeners.get(name); if (arr === undefined) { arr = []; customEventListeners.set(name, arr); } ArrayPrototypePush(arr, listener); return this; } const map = getListenerMap(name); if (map.has(listener)) { // Same listener already registered for this event on this port -- // matches Node.js MessagePort, where repeated `.on('message', fn)` // calls with the same reference do not produce duplicate // deliveries. return this; } // deno-lint-ignore no-explicit-any const _listener = (ev: any) => { patchMessagePortIfFound(ev.data); listener(ev.data); }; map.set(listener, _listener); port.addEventListener(name, _listener); if (name === "message") { // Mirror the auto-start behavior of `port.onmessage = fn` so Node // code that does `port.on('message', ...)` without an explicit // `port.start()` still receives messages. Deferred via a // microtask so `receiveMessageOnPort` (sync) gets a chance to // drain the queue first -- this is the same pattern the web // MessagePort applies in its `message` event handler init, which // `npm:piscina` relies on. Messages buffered before the listener // was attached are still picked up by the next iteration of the // recv loop (or by `MessagePort.close()`'s drain when the paired // port closes in the same turn). PromisePrototypeThen(PromiseResolve(undefined), () => port.start()); } return this; }; port.off = port.removeListener = function ( this: MessagePort, name, listener, ) { if (!isWebEvent(name)) { const arr = customEventListeners.get(name); if (arr !== undefined) { const idx = ArrayPrototypeIndexOf(arr, listener); if (idx !== -1) ArrayPrototypeSplice(arr, idx, 1); } return this; } const map = getListenerMap(name); const wrapper = map.get(listener); if (wrapper !== undefined) { port.removeEventListener(name, wrapper); map.delete(listener); } return this; }; // Node's MessagePort mixes in `emit` from EventEmitter so calling // `port.emit('foo', arg)` invokes listeners registered via `port.on` // *and* dispatches the equivalent Event for addEventListener listeners. // deno-lint-ignore no-explicit-any port.emit = function (this: MessagePort, name, ...args: any[]) { // Dispatch an Event so addEventListener listeners fire too. Use a // minimal Event with `detail` copied from the first arg to match the // common Node pattern of `emit('foo', payload)`. const ev = new Event(String(name)); if (args.length > 0) { // deno-lint-ignore no-explicit-any (ev as any).detail = args[0]; } port.dispatchEvent(ev); // Fire EventEmitter-style listeners. const arr = customEventListeners.get(name); if (arr !== undefined) { // Iterate over a copy so handlers that remove themselves don't // skip the next listener. const copy = ArrayPrototypeSlice(arr); for (let i = 0; i < copy.length; i++) { try { FunctionPrototypeApply(copy[i], undefined, args); } catch { // EventEmitter semantics swallow handler exceptions when // there's no 'error' listener; we just continue to the next // handler. } } } return arr !== undefined && arr.length > 0; }; port[nodeWorkerThreadCloseCb] = () => { // Dispatch asynchronously so listeners attached via `port.on('close', cb)` // (or via `port.close(cb)` which goes through addEventListener) see the // event after the current synchronous turn. This matches Node's // MessagePort, where close events fire on the next tick. queueMicrotask(() => { port[refMessagePort](false); // Mark the port as detached so `util.inspect(port)` shows // `active: false` after the close event has fired. The underlying // resource may already be closed (recv loop saw end-of-stream); // swallow any "bad resource id" error from a redundant core.close. if (port[MessagePortIdSymbol] !== null) { try { core.close(port[MessagePortIdSymbol]); } catch { // already closed } port[MessagePortIdSymbol] = null; } port.dispatchEvent(new Event("close")); }); }; port.unref = () => { port[refMessagePort](false); }; port.ref = () => { port[refMessagePort](true); }; const webPostMessage = port.postMessage; port.postMessage = function postMessage(message, transferOrOptions) { // Node-style validation + untransferable check on the second argument. // The Web spec normalizes via WebIDL dictionary conversion (different // error wording), so do the validation up-front and pass a normalized // value through. For iterables we materialize into an array so the // untransferable check below doesn't consume the iterator before the // serializer sees it. const normalized = transferOrOptions; if (arguments.length >= 2 && transferOrOptions !== undefined) { if (transferOrOptions === null) { // null is allowed; treated as no transferables. } else if ( typeof transferOrOptions !== "object" && typeof transferOrOptions !== "function" ) { const e = new TypeError( "Optional transferList argument must be an iterable", ); // deno-lint-ignore no-explicit-any (e as any).code = "ERR_INVALID_ARG_TYPE"; throw e; } else if (transferOrOptions[SymbolIterator] !== undefined) { // Only check untransferable markers when we can do so without // consuming the iterator -- a user-provided iterator can only // be drained once and we have to leave that drain to the // underlying web `postMessage`. Arrays are random-access so // we can inspect them without consuming. if (ArrayIsArray(transferOrOptions)) { for (let i = 0; i < transferOrOptions.length; i++) { const item = transferOrOptions[i]; if ( item !== null && typeof item === "object" && item[untransferableSymbol] === true ) { throw new DOMException( "Value not transferable", "DataCloneError", ); } } } // For non-array iterables, pass through unchanged so the // serializer below sees the original iterator. } else { // Treat as a StructuredSerializeOptions dict. Only `transfer` is // recognized; if present, validate that it's iterable. const t = transferOrOptions.transfer; if (t !== undefined) { if ( t === null || (typeof t !== "object" && typeof t !== "function") || t[SymbolIterator] === undefined || (typeof t[SymbolIterator] === "function" && typeof t[SymbolIterator]().next !== "function") ) { const e = new TypeError( "Optional options.transfer argument must be an iterable", ); // deno-lint-ignore no-explicit-any (e as any).code = "ERR_INVALID_ARG_TYPE"; throw e; } // Inspect arrays for untransferable markers. For non-array // iterables we have to leave the drain to the serializer // below -- consuming the iterator here would either lose // values or, for an infinite generator (the // terminate-transfer-list test), short-circuit Node's // expected hang. if (ArrayIsArray(t)) { for (let i = 0; i < t.length; i++) { const item = t[i]; if ( item !== null && typeof item === "object" && item[untransferableSymbol] === true ) { throw new DOMException( "Value not transferable", "DataCloneError", ); } } } } } } return FunctionPrototypeCall( webPostMessage, port, message, normalized, ); }; port.once = (name: string | symbol, listener) => { const fn = (event) => { port.off(name, fn); return listener(event); }; port.on(name, fn); }; return port; } // TODO(@marvinhagemeister): Recursively iterating over all message // properties seems slow. // Maybe there is a way we can patch the prototype of MessagePort _only_ // inside worker_threads? For now correctness is more important than perf. // deno-lint-ignore no-explicit-any function patchMessagePortIfFound(data: any, seen = new SafeSet<any>()) { if (data === null || typeof data !== "object" || seen.has(data)) { return; } seen.add(data); if (ObjectPrototypeIsPrototypeOf(MessagePortPrototype, data)) { webMessagePortToNodeMessagePort(data); } else { for (const obj in data as Record<string, unknown>) { if (ObjectHasOwn(data, obj)) { patchMessagePortIfFound(data[obj], seen); } } } } class BroadcastChannel extends WebBroadcastChannel { ref() { this[refBroadcastChannel](true); return this; } unref() { this[refBroadcastChannel](false); return this; } } const { locks } = core.createLazyLoader("ext:deno_web/locks.js")(); // Node's `worker_threads.MessagePort` is a function (not a class) that throws // `ERR_CONSTRUCT_CALL_INVALID` whether called as `MessagePort()` or // `new MessagePort()`. Mirror that here while keeping // `port instanceof MessagePort` and `port.constructor === MessagePort` true // by routing through the web MessagePort's prototype. Named "MessagePort" // so that `port.constructor.name === "MessagePort"` and the default // inspect output reads as MessagePort, not the wrapper. // deno-lint-ignore no-explicit-any const NodeMessagePort: any = { MessagePort: function MessagePort() { const err = new TypeError( "Constructor for class MessagePort cannot be invoked", ); // deno-lint-ignore no-explicit-any (err as any).code = "ERR_CONSTRUCT_CALL_INVALID"; throw err; }, }.MessagePort; NodeMessagePort.prototype = MessagePort.prototype; ObjectDefineProperty(MessagePort.prototype, "constructor", { __proto__: null, value: NodeMessagePort, writable: true, enumerable: false, configurable: true, }); ObjectAssign(exportsObj, { BroadcastChannel, MessagePort: NodeMessagePort, MessageChannel: NodeMessageChannel, Worker: NodeWorker, // Initial placeholders for fields that `__initWorkerThreads` overwrites // at bootstrap. Listed here so they appear as own enumerable string-keyed // properties of the exports object - the synthetic ESM dispatch derives // its export names from `Object.keys` of this object. parentPort: null, threadId: 0, workerData: null, isMainThread: true, // Deno has no internal Node worker threads (e.g. module loader threads), // so this is always false in the main thread and user-created workers. isInternalThread: false, // Main-thread default ({}). `__initWorkerThreads` overwrites it (with the // worker's resolved limits in a worker). Under node-defer, if that init never // runs for a worker_threads-only main program, this default still satisfies // Node's contract (an empty resourceLimits on the main thread). resourceLimits: {}, threadName: "", locks, markAsUncloneable, markAsUntransferable, isMarkedAsUntransferable, moveMessagePortToContext, postMessageToThread, receiveMessageOnPort, getEnvironmentData, setEnvironmentData, SHARE_ENV, }); // node-defer: under deferred bootstrap, `__initWorkerThreads` may never run on // the main thread (it ran from 01_require.js's `initialize`, which now only // fires on a require/node:module path). The global MessageChannel/MessagePort // alias to the Node classes is observable as soon as node:worker_threads // itself is imported (e.g. `import * as wt from "node:worker_threads"` and // then `wt.MessagePort === MessagePort`), so install it here unconditionally // when this module loads. The other half of __initWorkerThreads (parentPort, // setupCrossThreadMessaging) still runs from initialize() / the worker eager // bootstrap path because it depends on node:process being bootstrapped. try { ObjectDefineProperty(globalThis, "MessageChannel", { __proto__: null, value: NodeMessageChannel, writable: true, enumerable: false, configurable: true, }); ObjectDefineProperty(globalThis, "MessagePort", { __proto__: null, value: NodeMessagePort, writable: true, enumerable: false, configurable: true, }); } catch { // globalThis may be a sandbox without writable property descriptors; ignore. } return exportsObj; })();