diff --git a/test-parity/node-suite/worker_threads/README.md b/test-parity/node-suite/worker_threads/README.md index 402e8573a9..75e9166f30 100644 --- a/test-parity/node-suite/worker_threads/README.md +++ b/test-parity/node-suite/worker_threads/README.md @@ -2,3 +2,356 @@ Focused compatibility cases for `node:worker_threads` APIs that Perry wires through its Node-compat stdlib shim. + +## Upstream selection + +The expansion was compared against these primary-source snapshots: + +- Node.js + [`34c28d5`](https://github.com/nodejs/node/tree/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel), + especially the `test-worker-message-port-*`, `test-worker-message-channel*`, + `test-worker-message-mark-as-uncloneable`, `test-worker-invalid-workerdata`, + `test-worker-environmentdata`, `test-worker-event`, and + `test-worker-broadcastchannel` cases. +- Deno + [`f8a17c8`](https://github.com/denoland/deno/tree/f8a17c8171569fa2870d740030aaa59c91fdf9ee/tests/specs/node/worker_threads) + and its + [`worker_threads_test.ts`](https://github.com/denoland/deno/blob/f8a17c8171569fa2870d740030aaa59c91fdf9ee/tests/unit_node/worker_threads_test.ts) + selection, including port transfer, listener removal/deduplication, `unref`, + auto-exit, and broadcast coverage. +- Bun + [`0bffb47`](https://github.com/oven-sh/bun/tree/0bffb4767dd13b4f5aaf119b13dcf37bd094e2f1/test/js/node/worker_threads), + especially + [`worker_threads.test.ts`](https://github.com/oven-sh/bun/blob/0bffb4767dd13b4f5aaf119b13dcf37bd094e2f1/test/js/node/worker_threads/worker_threads.test.ts) + and + [`worker-transfer-list.test.ts`](https://github.com/oven-sh/bun/blob/0bffb4767dd13b4f5aaf119b13dcf37bd094e2f1/test/js/node/worker_threads/worker-transfer-list.test.ts). + +The follow-up inventory was refreshed against the 2026-07-16 heads of Node +[`cf882a79042c`](https://github.com/nodejs/node/tree/cf882a79042cba4146acfdb7993b6a97c21e7239/test), +Deno +[`68d51a1fc8f3`](https://github.com/denoland/deno/tree/68d51a1fc8f32faf200ac4c5ba7b69648699c610/tests/unit_node), +and Bun +[`aca54d5c2b87`](https://github.com/oven-sh/bun/tree/aca54d5c2b874ac304a3bbe1d67630e4daf17b43/test/js/node/worker_threads). +That pass added Node's Worker asynchronous-disposal, MessagePort and +BroadcastChannel custom-inspection, direct-message transferable graph, and +BroadcastChannel dispatch-time lifecycle contracts. Bun independently selects +Worker asynchronous disposal and Node's worker corpus, while Deno's unit suite +provides a second executable Node-compat comparison for the same core objects. +The latest deterministic audit also selected Node's filename and constructor +validation, MessagePort prototype/Event brands, and resource-limit metadata; +Deno's same-reference listener deduplication, pending synchronous receives, and +`process.env` option regression; and Bun's queued peer-close and orphaned +transferred-port regressions. The next audit selected Node's deterministic +FileHandle clone/transfer ownership and process restrictions, plus Bun's +FileHandle construction rollback and workerData alias regressions. +Resource-consuming in-use-handle races remain separate; these cases use small +controlled read-only files and explicit message/exit barriers. The following +pass selected Node's closing/closed-port listener safety and Bun's +spawning-thread `SHARE_ENV` tree isolation/founder regressions. Those +environment fixtures coordinate only through worker exit and scoped environment +stores, not nested-message scheduling. The latest pass adds Node's +regular-worker `isInternalThread` state, URL/eval and competing-option +constructor validation, closed-port `DOMException` branding, VM-context +FileHandle deserialization failure, and orphaned FileHandle ownership/rollback. +Deno's ordered listener and unref delivery selections and Bun's invalid-name +FileHandle rollback regression provide independent coverage for the same +lifecycle boundaries. The latest audit adds Node's worker-created buffer +lifetime, post-exit diagnostics, CPU-usage validation, process-environment +descriptors, and code-after-`process.exit()` contracts; Node's BroadcastChannel +WPT event metadata; and Bun's nested Map/Set FileHandle transfer regression. A +reverse worker-created MessagePort case closes the remaining deterministic +ownership direction without relying on scheduler timing. + +The inventory review covered all 146 current `test-worker-*` files across Node's +parallel and sequential directories, all 48 cases in Deno's unit selection plus +its directory fixtures, and 88 `test`/`it` declarations across Bun's current +`worker_threads/*.test.ts` files. Cases are represented here by observable +contract rather than copied one-for-one when several upstream files exercise the +same behavior. + +Node `26.5.0`, pinned by this repository, remains the executable differential +oracle. The upstream snapshots above guide case selection rather than changing +that oracle. + +## Coverage added + +- `main-thread/`: complete namespace export availability and types, main-thread + values, Worker and MessagePort prototype descriptors, constructor brands, and + the process-level `worker` event, including asynchronous late `process.emit` + lookup. +- `environment-data/`: key identity, live main-thread values, deletion, worker + snapshot cloning, built-in Map/Set/Date values, nested-worker inheritance, + cyclic and aliased graph snapshots, non-cloneable construction rejection, and + parent/worker mutation isolation. +- `structured-clone/`: ArrayBuffer cloning, typed-array backing identity, + built-in brands, cycles/aliasing, SharedArrayBuffer sharing, multiple views, + multiple SharedArrayBuffers, transfer detachment, detached-buffer retransfer + rejection, MessagePort ownership, unsupported URL rejection, overloads, and + atomic rollback, and FileHandle clone rejection, transfer detachment, + received-handle readability, and parent ownership loss. +- `message-port/`: synchronous FIFO receives, invalid-port validation, explicit + `start()`, event fields/ports, listener deduplication, close callback + ordering, listener removal/`once`, `onmessage` replacement, method receiver + validation, NodeEventTarget surface/listener validation, iterable MessageEvent + ports and transfer options, dispatch-time close flushing, + duplicate/self/closed transfers, atomic clone rollback, transfer-state + validation, queued delivery, overload validation, first-listener registration, + pending `once` removal, per-event listener scope, close-callback registration + order, tamper-resistant listener bookkeeping, post-dispatch `once` registry + state, non-function `onmessage` clearing, peer-close ref behavior, throwing + and malformed transfer iterators, accepted empty transfer forms, custom-event + EventTarget/EventEmitter bridging, max-listener controls, scoped + `removeAllListeners`, missing-message validation, transfer rejection during + close flushing, frozen EventTarget prototype resilience, MessageEvent + defaults/coercion, transfer targets, `ref`/`unref`/`hasRef` state, custom + inspection of active/refed state, and proof that synchronous draining + suppresses later event delivery, same-reference `.on()` deduplication and + event-name scoping, synchronous drains after `start()`, queued-message + ordering before a peer close, orphaned-transfer peer notification, and the + exact MessagePort/Event prototype brands. Listener aliases are also exercised + while both endpoints are closing and from the close callback after closure. + Distinct listener registration order, delivery while the receiving port is + explicitly unrefed, and the full closed-transfer `DOMException` brand are + checked separately. +- `message-channel/`: module/global identity, construction rules, port brands, + asynchronous and synchronous delivery, BroadcastChannel synchronous receive, + and VM-context port movement with cross-realm branding, closed-port, argument + validation, and FileHandle deserialization failure routing through + `messageerror`. Worker-created MessagePorts are also drained synchronously + after the creating worker exits, proving queued data and ownership survive the + source realm. +- `worker-lifecycle/`: constructor transfer validation, structured `workerData`, + default and built-in workerData, MessagePort/ArrayBuffer transfer, environment + snapshots, eval/CJS require, file URLs with Unicode names, argv and execArgv + coercion/validation, inherited/explicit/shared environments, SharedArrayBuffer + through workerData and postMessage, queued transferred-port receive, thread + id/name uniqueness and lifecycle, parentPort `onmessage`/ref state, Worker + EventEmitter surface, MessagePort/EventTarget inheritance brands and listener + validation, process restrictions, captured stdout/stderr, explicit and + `process.exitCode` exits, unsupported paths, method receivers, postMessage + clone rollback, `online`/`message`/`error`/`exit` ordering, ref return values, + natural exit, and deterministic repeated termination settlement, transferred + workerData port methods, sequential `SHARE_ENV` sibling visibility, post-exit + method safety, indexed `SHARE_ENV` keys, eval syntax-error routing, URL + postMessage rejection, missing postMessage validation, exit-listener exception + routing, global `postMessage` replacement, workerData alias/view/cycle + cloning, multiple and aliased MessagePorts, MessagePorts nested in Map/Set, + broader filename, falsy execArgv, and disallowed `NODE_OPTIONS` validation, + custom-stack and non-Error serialization, Worker asynchronous disposal, + construction-time transfer rollback, default environment isolation, and the + base file/eval `process.argv` shape, complete invalid filename types, + `process.env` as an explicit environment option, and deterministic + `resourceLimits` snapshots/reset without memory-pressure enforcement, + FileHandle construction rollback and workerData alias identity, default/falsy + thread names, the complete worker-disabled process stub/getter surface, and + disjoint/founder `SHARE_ENV` spawning-tree semantics, regular-worker + `isInternalThread` state, URL/eval rejection, constructor option-validation + precedence with transfer ownership, and referenced/unreferenced FileHandle + construction ownership and rollback. Reverse worker-created + ArrayBuffer/SharedArrayBuffer lifetime, process-environment descriptor rules, + post-exit diagnostic behavior, CPU-usage argument validation, Map/Set-nested + FileHandle traversal, and the hard boundary after `process.exit()` are covered + with exit or termination barriers. +- `transfer-markers/`: marker return/value semantics, inheritance boundaries, + clone rejection, transfer rejection, retained ownership after rejection, and + the distinction between marking a port uncloneable and transferring it, plus + private, unforgeable, and permanent marker behavior. +- `broadcast-channel/`: same-name fanout/FIFO isolation, sender exclusion, + listener management/`once`, `onmessage` replacement, name coercion, close/ref + idempotence, typed-array and SharedArrayBuffer cloning, untransferable-value + rejection, missing-message validation, clone-versus-closed error precedence, + closed-channel and method-receiver validation, custom inspection, complete + MessageEvent metadata, channel creation during dispatch, and suppression of + delivery already queued for a channel that closes. +- `web-locks/`: deterministic pre-aborted requests and lock stealing, extending + the existing surface, option, query, callback settlement/cleanup, + independent-name concurrency, and shared/exclusive ordering cases. +- `direct-message/`: delivery, no-listener, timeout, and handler-failure + rejection, plus thread id and timeout argument validation and atomic + ArrayBuffer/MessagePort transfer with observable ownership. + +Every asynchronous fixture uses a message, close, exit, stream-EOF, or +promise-settlement barrier. No added fixture uses a sleep as a completion +condition. + +## Stopping judgment + +The remaining upstream cases were not copied because they are redundant with the +cases above or belong to a separate slow/risky runtime feature: + +- Resource-limit enforcement, stdio backpressure/large-write timing, heap + snapshots, and CPU/heap profiling are resource- and platform-sensitive. Basic + resource-limit metadata/reset, captured stdout/stderr, CPU-usage validation, + and live/post-exit diagnostic method behavior are covered without introducing + pressure or timing assertions. +- Basic eval/CJS require, execArgv, file-URL construction, synchronous URL/eval + rejection, process restrictions, and exit codes are now covered. + Data-URL/ESM/custom loaders, preload chains, signals, inspector integration, + beforeExit and worker-side uncaught-exception variants, and source maps remain + separate loader, CLI, or process-subsystem work. +- Controlled nested workers now cover environment-data inheritance and + spawning-thread `SHARE_ENV` tree isolation without nested message scheduling. + Deeper nesting stress, GC/finalization of unreachable ports, shared + native-handle races, large message loops, and termination races remain + scheduler-sensitive. +- Basic FileHandle clone rejection, transfer ownership, construction rollback, + and workerData graph aliasing are covered with controlled read-only handles. + Orphaned transfer ownership, rollback after a later clone failure, and + invalid-name prevalidation are also covered. Pending read/close transfer + races, descriptor recycling/leak detection, and cross-context disposal remain + shared-handle stress rather than granular parity cases. +- SharedArrayBuffer aliasing is covered through same-thread MessagePort and + BroadcastChannel paths plus cross-agent workerData and postMessage. Atomics + wait/notify coordination remains separate scheduler-aware infrastructure. +- A Node `markAsUncloneable(Blob)` candidate was stable under Node 26.5.0 but + exceeded the 120-second Perry per-case compilation threshold. It belongs with + Blob runtime/compilation coverage rather than turning a marker diagnostic into + a granular timeout. +- A self-contained WebAssembly.Module worker-post candidate produced the same + module brand and result under Node 26.5.0, Deno 2.9.2, and Bun 1.2.18, but + exceeded the 120-second Perry compilation threshold. Like the Blob candidate, + that is WebAssembly/compiler coverage rather than a useful runtime diagnostic. +- A cross-worker BroadcastChannel delivery candidate was rejected after Node + required cross-agent scheduling turns while Perry's absent delivery left no + deterministic completion event. It belongs with scheduler-aware cross-agent + infrastructure rather than a sleep/timeout-based fixture here. +- A Node live-`process.cwd()` propagation candidate passed three oracle runs, + but Perry could not shut its worker down deterministically: `parentPort.close` + is unsupported, a consumed `once` listener retained the worker, and explicit + exit variably dropped the final message. That contract needs worker-shutdown + infrastructure rather than a timeout-prone granular fixture. +- Web Locks now cover deterministic pre-abort and steal behavior in addition to + surface, validation, shared/exclusive ordering, `ifAvailable`, and query + snapshots. In-flight abort races and cross-agent ownership remain excluded. +- The existing `direct-message/` fixtures already cover deterministic + `postMessageToThread` delivery, transferable ownership, rejection, handler + failures, and timeout behavior. Bun's prototype-tampering regression was not + retained: worker-to-main delivery can remain pending in Perry without a + non-timing completion event, so a faithful fixture would rely on an arbitrary + timeout. +- Bun's current non-function `process.emit` worker regression was not selected: + the pinned Node 26.5.0 oracle exits fatally from `process._fatalException` + before an `uncaughtException` handler can establish the recovery contract that + Bun asserts. It is therefore not a Node parity case. +- A main-to-grandchild `postMessageToThread` candidate completed under Node and + Deno, but Perry's nested worker never reached the explicit ready/inspection + barrier and retained the event loop beyond 30 seconds. It needs nested-worker + readiness/shutdown infrastructure rather than a granular timeout. +- Operating on the original MessagePort after `moveMessagePortToContext` was + rejected because Node 26.5.0 itself exited by signal 11. The retained + cross-realm-brand case observes the moved port without probing undefined + moved-from behavior. +- Invalid-path transfer rollback was not retained: a literal nonexistent Worker + target fails Perry compilation, while hiding it behind a dynamic path only + exercises the already covered unresolved-path fallback and leaves the buffer + untouched vacuously. Bun's peer-close-while-in-transit case and Deno's + cross-worker unref liveness case were also not copied literally: the missing + event in Perry retains the worker indefinitely. Their deterministic local + subcontracts are covered through peer-close ordering and unrefed delivery + without adding timeout-based completion. + +The measured focused result is `38/205`: all 17 pre-existing cases remain green, +21 added cases pass, and 167 added cases expose stable diagnostic differences. +The prior ten cases were each repeated three times under Node 26.5.0 and Perry +with stable output and exit status. Deno 2.9.2 was stable across the same +matrix. Bun 1.2.18 was stable for nine cases; its older orphaned-transfer +implementation did not emit the peer close and hit the external three-second +comparison timeout in three runs, while current Bun upstream carries that exact +regression test. The latest four additions plus the two strengthened +path/process cases were also stable in three runs per runtime under Node, Perry, +Deno, and Bun. Bun 1.2.18 still rejects FileHandle workerData transfer selected +by current Bun upstream, and its bare-relative Worker path exits before the +remaining path matrix; both behaviors were stable and are recorded as +old-runtime comparison gaps. The latest closed-listener and two `SHARE_ENV` tree +cases were each repeated three times under all four runtimes with stable output +and exit status. Deno shares the process-wide tree-isolation gap, while Bun +1.2.18 rejects nested `SHARE_ENV`; Perry exposes process-wide leakage but passes +founder preservation. The latest ten cases were likewise repeated three times +under Node 26.5.0, Perry, Deno 2.9.2, and Bun 1.2.18. Bun's older unrefed-port +implementation retained the external six-second comparison process in all three +runs; every retained Node/Perry case completed through an explicit event or exit +barrier. The latest eight cases and strengthened workerData built-in fixture +were also stable in three runs under all four runtimes. Bun 1.2.18 emits an +expected not-implemented warning for Worker performance; all retained processes +terminate without an external comparison timeout. + +The passing additions are `broadcast-channel/fanout-fifo.ts`, +`broadcast-channel/listener-management.ts`, +`message-port/start-and-listeners.ts`, +`worker-lifecycle/termination-ordering.ts`, +`worker-lifecycle/worker-ref-state.ts`, `web-locks/web-locks-abort.ts`, and +`web-locks/web-locks-steal.ts`. The latest broad pass also adds green coverage +for `environment-data/value-identity.ts`, +`message-port/onmessage-replacement.ts`, `worker-lifecycle/eval-basic.ts`, +`worker-lifecycle/method-receivers.ts`, and +`web-locks/web-locks-independent-names.ts`. Sequential sibling `SHARE_ENV` +visibility in `worker-lifecycle/share-env-siblings.ts` also passes. The +indexed-key variant in `worker-lifecycle/share-env-indexed.ts` passes too. The +accepted optional transfer forms in `message-port/transfer-optional-forms.ts` +also pass. The latest refresh adds passing dispatch-time coverage in +`broadcast-channel/create-during-dispatch.ts` and +`broadcast-channel/close-queued-delivery.ts`. Synchronously drained messages are +also proven not to reappear through the event listener in +`message-port/receive-consumes-event.ts`. Passing `process.env` directly as a +Worker environment option is also covered by +`worker-lifecycle/process-env-option.ts`. Explicit-env `SHARE_ENV` founder +preservation in `worker-lifecycle/share-env-founder.ts` passes too. Worker +CPU-usage argument validation in `worker-lifecycle/cpu-usage-validation.ts` also +matches Node. The diagnostic differences are: + +- All thirteen `structured-clone/` fixtures: Perry preserves some indexed values + but loses built-in/ArrayBuffer/view/SharedArrayBuffer brands and aliasing, + rejects cycles/BigInt, does not detach or move ownership, accepts invalid + lists, and treats FileHandles as cloneable plain objects. +- Forty-eight `message-port/` diagnostics: closing and dispatch flushing, queued + data/callbacks, NodeEventTarget surface, listener counts and validation, + receiver/options/iterables/transfers, MessageEvent construction and `ports`, + duplicate/self/closed rollback, moved ownership, and `hasRef()` state and + custom inspection differ, as do `.on()` deduplication, pending synchronous + receives, queued peer-close ordering, orphaned transfers, Event brands, and + listener alias behavior after closure. +- Seventy-one `worker-lifecycle/` diagnostics: invalid constructor payloads and + paths, execArgv, captured stdio, process restrictions/exit codes, shared + environments and nested messages, worker/parentPort EventEmitter behavior, + metadata/unique ids, repeated termination, workerData and postMessage SAB + sharing, queued port receive, rollback, post-exit values, error routing, + Worker asynchronous disposal, default environment isolation, process argv + shape, constructor rollback, invalid filename types, and resource-limit reset + differ. FileHandle rollback/aliasing, default/falsy thread names, and the full + disabled process surface also differ, as does spawning-tree `SHARE_ENV` + isolation. Basic eval with CJS require, explicit `process.env`, and preserving + an explicit-env founder value all pass. +- All five `transfer-markers/` fixtures: primitives are reported as marked, + nested clone rejection, private/permanent marker enforcement, ArrayBuffer + exceptions, and marked transferables/uncloneable ports differ. +- Twelve `broadcast-channel/` diagnostics: name coercion, once listeners, + close/ref behavior, typed-array/SharedArrayBuffer branding and sharing, + MessagePort validation, closed-channel posts, custom inspection, and complete + MessageEvent metadata differ. +- Eighteen additional diagnostics cover namespace/prototype completeness, + environment-data built-ins/inheritance/construction, MessageChannel + construction/callability and VM-context branding, direct-message argument + validation/transfer ownership, and Web Locks callback rejection cleanup. + +The clean measurement used: + +```sh +NODE_BIN="$(command -v node)" \ +PERRY_RUNTIME_DIR="$PWD/target/perry-dev" \ +python3 scripts/node_suite_run.py \ + "$PWD/target/perry-dev/perry" "$PWD" worker_threads +``` + +It reported `38/205 (18.5%), diff=167`, with no compile failures or timeouts. +The Worker URL-post diagnostic consistently exits by signal 11 in Perry while +Node rejects synchronously with `DataCloneError`, keeps the worker usable, and +terminates cleanly. Cyclic `workerData` construction also consistently exits by +signal 11 in Perry while Node preserves both cyclic references and exits zero. +The SharedArrayBuffer/Atomics boundary is backed by the added same-process +channel and cross-worker clone/alias cases plus the existing granular +`globals/atomics-*.ts`, `buffer/from/shared-array-buffer.ts`, and +`util/types/arraybuffer-sharedarraybuffer.ts` cases; cross-agent fixtures such +as Node's `test-worker-message-channel-sharedarraybuffer.js` and Deno's +`broadcast_channel_sab.mjs` remain outside this messaging-focused batch. A +broader differential regression check of all six `globals/atomics-*.ts` cases +also remained clean at `6/6`. diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/close-and-clone.ts b/test-parity/node-suite/worker_threads/broadcast-channel/close-and-clone.ts new file mode 100644 index 0000000000..399b865f99 --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/close-and-clone.ts @@ -0,0 +1,45 @@ +import { + BroadcastChannel, + MessageChannel, + receiveMessageOnPort, +} from "node:worker_threads"; + +function outcome(fn: () => void): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const sender = new BroadcastChannel("clone"); +const receiver = new BroadcastChannel("clone"); +const view = new Uint8Array([3, 1, 4]); +sender.postMessage(view); +view[0] = 9; + +const packet = receiveMessageOnPort(receiver); +const cloned = packet ? packet.message : undefined; +const clonedValues = typeof cloned?.join === "function" + ? cloned.join(",") + : "not-typed"; +console.log( + "typed clone:", + cloned instanceof Uint8Array, + clonedValues, + view.join(","), +); + +const channel = new MessageChannel(); +console.log( + "port rejected:", + outcome(() => sender.postMessage({ port: channel.port1 })), +); + +sender.close(); +console.log("post closed:", outcome(() => sender.postMessage("late"))); + +receiver.close(); +channel.port1.close(); +channel.port2.close(); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/close-and-ref-idempotence.ts b/test-parity/node-suite/worker_threads/broadcast-channel/close-and-ref-idempotence.ts new file mode 100644 index 0000000000..0a5f05c418 --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/close-and-ref-idempotence.ts @@ -0,0 +1,15 @@ +import { BroadcastChannel } from "node:worker_threads"; + +const channel = new BroadcastChannel("idempotent"); +console.log("ref return:", channel.ref() === channel); +console.log("unref return:", channel.unref() === channel); +console.log("ref again:", channel.ref() === channel); +console.log("close return:", channel.close()); +console.log("close twice:", channel.close()); + +try { + channel.postMessage("closed"); + console.log("post closed: ok"); +} catch (error: any) { + console.log("post closed:", error?.name, error?.code ?? ""); +} diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/close-queued-delivery.ts b/test-parity/node-suite/worker_threads/broadcast-channel/close-queued-delivery.ts new file mode 100644 index 0000000000..bf645546c2 --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/close-queued-delivery.ts @@ -0,0 +1,19 @@ +import { BroadcastChannel } from "node:worker_threads"; + +const sender = new BroadcastChannel("close-queued-delivery"); +const closed = new BroadcastChannel("close-queued-delivery"); +const barrier = new BroadcastChannel("close-queued-delivery"); +let closedCalls = 0; + +sender.unref(); +closed.onmessage = () => { + closedCalls += 1; +}; +barrier.onmessage = (event) => { + console.log("barrier:", event.data, "closed calls:", closedCalls); + sender.close(); + barrier.close(); +}; + +sender.postMessage("queued"); +closed.close(); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/constructor-and-name.ts b/test-parity/node-suite/worker_threads/broadcast-channel/constructor-and-name.ts new file mode 100644 index 0000000000..53619fdf91 --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/constructor-and-name.ts @@ -0,0 +1,20 @@ +import { BroadcastChannel } from "node:worker_threads"; + +for ( + const [label, value] of [ + ["missing", undefined], + ["empty", ""], + ["number", 42], + ["object", { toString: () => "object-name" }], + ] as const +) { + try { + const channel = value === undefined + ? new (BroadcastChannel as any)() + : new BroadcastChannel(value as any); + console.log(label, "name", channel.name, typeof channel.name); + channel.close(); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/constructor-coercion-edges.ts b/test-parity/node-suite/worker_threads/broadcast-channel/constructor-coercion-edges.ts new file mode 100644 index 0000000000..415ca053f5 --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/constructor-coercion-edges.ts @@ -0,0 +1,19 @@ +import { BroadcastChannel } from "node:worker_threads"; + +for ( + const [label, value] of [ + ["null", null], + ["bigint", 1n], + ["boolean", false], + ["infinity", Number.POSITIVE_INFINITY], + ["symbol", Symbol("channel")], + ] as const +) { + try { + const channel = new BroadcastChannel(value as any); + console.log(label, "name", channel.name); + channel.close(); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/create-during-dispatch.ts b/test-parity/node-suite/worker_threads/broadcast-channel/create-during-dispatch.ts new file mode 100644 index 0000000000..d4ef229f51 --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/create-during-dispatch.ts @@ -0,0 +1,20 @@ +import { BroadcastChannel } from "node:worker_threads"; + +const sender = new BroadcastChannel("create-during-dispatch"); +const first = new BroadcastChannel("create-during-dispatch"); + +sender.unref(); +first.onmessage = (event) => { + console.log("first:", event.data); + first.close(); + + const created = new BroadcastChannel("create-during-dispatch"); + created.onmessage = (nextEvent) => { + console.log("created:", nextEvent.data); + created.close(); + sender.close(); + }; + sender.postMessage("second"); +}; + +sender.postMessage("first"); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/custom-inspect.ts b/test-parity/node-suite/worker_threads/broadcast-channel/custom-inspect.ts new file mode 100644 index 0000000000..005a4a86fc --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/custom-inspect.ts @@ -0,0 +1,17 @@ +import { BroadcastChannel } from "node:worker_threads"; +import { inspect } from "node:util"; + +function outcome(fn: () => unknown): string { + try { + return String(fn()); + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const channel = new BroadcastChannel("inspect-channel"); +const custom = (channel as any)[inspect.custom]; +console.log("custom:", typeof custom, outcome(() => custom.call({}))); +console.log("active:", inspect(channel), inspect(channel, { depth: -1 })); +channel.close(); +console.log("closed:", inspect(channel)); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/error-precedence.ts b/test-parity/node-suite/worker_threads/broadcast-channel/error-precedence.ts new file mode 100644 index 0000000000..0159564aad --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/error-precedence.ts @@ -0,0 +1,15 @@ +import { BroadcastChannel } from "node:worker_threads"; + +function outcome(channel: BroadcastChannel, value: any): string { + try { + channel.postMessage(value); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const channel = new BroadcastChannel("error-precedence"); +console.log("open symbol:", outcome(channel, Symbol("value"))); +channel.close(); +console.log("closed symbol:", outcome(channel, Symbol("value"))); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/fanout-fifo.ts b/test-parity/node-suite/worker_threads/broadcast-channel/fanout-fifo.ts new file mode 100644 index 0000000000..f4172e4abb --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/fanout-fifo.ts @@ -0,0 +1,27 @@ +import { + BroadcastChannel, + receiveMessageOnPort, +} from "node:worker_threads"; + +function receive(channel: BroadcastChannel): string { + const packet = receiveMessageOnPort(channel); + return packet ? JSON.stringify(packet.message) : "empty"; +} + +const sender = new BroadcastChannel("fanout"); +const listenerA = new BroadcastChannel("fanout"); +const listenerB = new BroadcastChannel("fanout"); +const unrelated = new BroadcastChannel("other"); + +sender.postMessage({ index: 1 }); +sender.postMessage({ index: 2 }); + +console.log("listener a:", receive(listenerA), receive(listenerA)); +console.log("listener b:", receive(listenerB), receive(listenerB)); +console.log("sender excluded:", receive(sender)); +console.log("name isolated:", receive(unrelated)); + +sender.close(); +listenerA.close(); +listenerB.close(); +unrelated.close(); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/listener-management.ts b/test-parity/node-suite/worker_threads/broadcast-channel/listener-management.ts new file mode 100644 index 0000000000..1b8a957e97 --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/listener-management.ts @@ -0,0 +1,22 @@ +import { BroadcastChannel } from "node:worker_threads"; + +const sender = new BroadcastChannel("listener-management"); +const receiver = new BroadcastChannel("listener-management"); +let calls = 0; + +function listener(event: MessageEvent) { + calls += 1; + console.log("listener:", calls, event.data); + sender.close(); + receiver.close(); +} + +function removed() { + console.log("removed listener fired"); +} + +receiver.addEventListener("message", listener); +receiver.addEventListener("message", listener); +receiver.addEventListener("message", removed); +receiver.removeEventListener("message", removed); +sender.postMessage("once"); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/listener-once.ts b/test-parity/node-suite/worker_threads/broadcast-channel/listener-once.ts new file mode 100644 index 0000000000..104e8c2e1e --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/listener-once.ts @@ -0,0 +1,28 @@ +import { BroadcastChannel } from "node:worker_threads"; + +const sender = new BroadcastChannel("listener-once"); +const receiver = new BroadcastChannel("listener-once"); +const events: string[] = []; + +function removed(event: MessageEvent) { + events.push(`removed:${event.data}`); +} + +receiver.addEventListener("message", removed); +receiver.removeEventListener("message", removed); +receiver.addEventListener( + "message", + (event) => events.push(`once:${event.data}`), + { once: true }, +); +receiver.addEventListener("message", (event) => { + events.push(`regular:${event.data}`); + if (event.data === "second") { + console.log("events:", events.join(",")); + sender.close(); + receiver.close(); + } +}); + +sender.postMessage("first"); +sender.postMessage("second"); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/messageevent-metadata.ts b/test-parity/node-suite/worker_threads/broadcast-channel/messageevent-metadata.ts new file mode 100644 index 0000000000..7faf35362b --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/messageevent-metadata.ts @@ -0,0 +1,28 @@ +import { BroadcastChannel } from "node:worker_threads"; + +const sender = new BroadcastChannel("event-metadata"); +const receiver = new BroadcastChannel("event-metadata"); + +receiver.onmessage = (event: MessageEvent) => { + console.log( + "event:", + event instanceof MessageEvent, + event.type, + event.data, + event.target === receiver, + event.currentTarget === receiver, + ); + console.log( + "defaults:", + JSON.stringify({ + origin: event.origin ?? "missing", + lastEventId: event.lastEventId ?? "missing", + source: event.source ?? null, + ports: event.ports?.length ?? "missing", + }), + ); + sender.close(); + receiver.close(); +}; + +sender.postMessage("payload"); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/method-receivers.ts b/test-parity/node-suite/worker_threads/broadcast-channel/method-receivers.ts new file mode 100644 index 0000000000..fb0b2ccdaf --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/method-receivers.ts @@ -0,0 +1,22 @@ +import { BroadcastChannel } from "node:worker_threads"; + +function outcome(fn: () => any): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const prototype = BroadcastChannel.prototype as any; +const nameGetter = Object.getOwnPropertyDescriptor(prototype, "name")?.get; +console.log( + "name getter:", + typeof nameGetter, + outcome(() => (nameGetter as any).call({})), +); + +for (const name of ["postMessage", "close", "ref", "unref"] as const) { + console.log(`${name}:`, outcome(() => prototype[name].call({}))); +} diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/onmessage-replacement.ts b/test-parity/node-suite/worker_threads/broadcast-channel/onmessage-replacement.ts new file mode 100644 index 0000000000..c0cf7ef83d --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/onmessage-replacement.ts @@ -0,0 +1,23 @@ +import { BroadcastChannel } from "node:worker_threads"; + +const sender = new BroadcastChannel("onmessage-replacement"); +const receiver = new BroadcastChannel("onmessage-replacement"); +const events: string[] = []; + +receiver.onmessage = (event) => events.push(`first:${event.data}`); +receiver.onmessage = (event) => { + events.push(`second:${event.data}`); + receiver.onmessage = null; + sender.postMessage("after-null"); +}; +receiver.addEventListener("message", (event) => { + events.push(`observer:${event.data}`); + if (event.data === "after-null") { + console.log("events:", events.join(",")); + console.log("handler:", receiver.onmessage); + sender.close(); + receiver.close(); + } +}); + +sender.postMessage("value"); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/post-message-missing.ts b/test-parity/node-suite/worker_threads/broadcast-channel/post-message-missing.ts new file mode 100644 index 0000000000..324ac538d5 --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/post-message-missing.ts @@ -0,0 +1,10 @@ +import { BroadcastChannel } from "node:worker_threads"; + +const channel = new BroadcastChannel("missing-message"); +try { + (channel.postMessage as any)(); + console.log("post: ok"); +} catch (error: any) { + console.log("post:", error?.name, error?.code ?? ""); +} +channel.close(); diff --git a/test-parity/node-suite/worker_threads/broadcast-channel/shared-array-buffer.ts b/test-parity/node-suite/worker_threads/broadcast-channel/shared-array-buffer.ts new file mode 100644 index 0000000000..4328e19951 --- /dev/null +++ b/test-parity/node-suite/worker_threads/broadcast-channel/shared-array-buffer.ts @@ -0,0 +1,24 @@ +import { + BroadcastChannel, + receiveMessageOnPort, +} from "node:worker_threads"; + +const sender = new BroadcastChannel("shared-buffer"); +const receiver = new BroadcastChannel("shared-buffer"); +const source = new SharedArrayBuffer(4); +const sourceView = new Int32Array(source); +Atomics.store(sourceView, 0, 5); + +sender.postMessage(source); +const packet = receiveMessageOnPort(receiver); +const received = packet ? packet.message : undefined; +const receivedView = received instanceof SharedArrayBuffer + ? new Int32Array(received) + : undefined; + +console.log("brand/identity:", received instanceof SharedArrayBuffer, received === source); +Atomics.store(sourceView, 0, 27); +console.log("shared update:", receivedView ? Atomics.load(receivedView, 0) : "missing"); + +sender.close(); +receiver.close(); diff --git a/test-parity/node-suite/worker_threads/direct-message/handler-error-worker.cjs b/test-parity/node-suite/worker_threads/direct-message/handler-error-worker.cjs new file mode 100644 index 0000000000..1c8116c88b --- /dev/null +++ b/test-parity/node-suite/worker_threads/direct-message/handler-error-worker.cjs @@ -0,0 +1,7 @@ +const { parentPort } = require("node:worker_threads"); + +process.on("workerMessage", () => { + throw new Error("direct-handler-boom"); +}); +parentPort.once("message", () => {}); +parentPort.postMessage("ready"); diff --git a/test-parity/node-suite/worker_threads/direct-message/handler-error.ts b/test-parity/node-suite/worker_threads/direct-message/handler-error.ts new file mode 100644 index 0000000000..50553cecbd --- /dev/null +++ b/test-parity/node-suite/worker_threads/direct-message/handler-error.ts @@ -0,0 +1,23 @@ +import { postMessageToThread, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/direct-message"); + +const worker = new Worker("./handler-error-worker.cjs"); +worker.on("message", async (message) => { + if (message !== "ready") { + return; + } + + try { + await postMessageToThread(worker.threadId, "trigger"); + console.log("direct: resolved"); + } catch (error: any) { + console.log("direct:", error?.name, error?.code ?? ""); + } + worker.postMessage("close"); +}); +worker.on( + "error", + (error: any) => console.log("worker error:", error?.name, error?.code ?? ""), +); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/direct-message/transferables-worker.cjs b/test-parity/node-suite/worker_threads/direct-message/transferables-worker.cjs new file mode 100644 index 0000000000..51dc730bec --- /dev/null +++ b/test-parity/node-suite/worker_threads/direct-message/transferables-worker.cjs @@ -0,0 +1,21 @@ +const { + MessagePort, + parentPort, +} = require("node:worker_threads"); + +process.on("workerMessage", (value, source) => { + const port = value?.port; + const buffer = value?.buffer; + port?.postMessage({ + portBrand: port instanceof MessagePort, + bufferBrand: buffer instanceof ArrayBuffer, + values: buffer instanceof ArrayBuffer + ? Array.from(new Uint8Array(buffer)).join(",") + : "missing", + source, + }); + port?.close(); +}); + +parentPort.on("message", () => {}); +parentPort.postMessage("ready"); diff --git a/test-parity/node-suite/worker_threads/direct-message/transferables.ts b/test-parity/node-suite/worker_threads/direct-message/transferables.ts new file mode 100644 index 0000000000..780c0bfee8 --- /dev/null +++ b/test-parity/node-suite/worker_threads/direct-message/transferables.ts @@ -0,0 +1,36 @@ +import { + MessageChannel, + postMessageToThread, + receiveMessageOnPort, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/direct-message"); + +const worker = new Worker("./transferables-worker.cjs"); +const { port1, port2 } = new MessageChannel(); +const buffer = new Uint8Array([2, 4, 6, 8]).buffer; + +worker.once("message", async () => { + try { + await postMessageToThread( + worker.threadId, + { buffer, port: port2 }, + [buffer, port2], + ); + const delivered = receiveMessageOnPort(port1)?.message; + console.log( + "delivered:", + delivered?.portBrand, + delivered?.bufferBrand, + delivered?.values, + delivered?.source === 0, + ); + console.log("ownership:", buffer.byteLength, true); + } catch (error: any) { + console.log("direct:", error?.name, error?.code ?? ""); + } + port1.close(); + port2.close(); + worker.terminate().then((code) => console.log("terminate:", code)); +}); diff --git a/test-parity/node-suite/worker_threads/direct-message/validation.ts b/test-parity/node-suite/worker_threads/direct-message/validation.ts new file mode 100644 index 0000000000..f33e634acb --- /dev/null +++ b/test-parity/node-suite/worker_threads/direct-message/validation.ts @@ -0,0 +1,26 @@ +import { postMessageToThread } from "node:worker_threads"; + +async function check(label: string, fn: () => any) { + try { + await fn(); + console.log(label, "resolved"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} + +async function main() { + await check("missing id:", () => (postMessageToThread as any)()); + await check("string id:", () => postMessageToThread("1" as any, "value")); + await check("nan id:", () => postMessageToThread(Number.NaN, "value")); + await check("fraction id:", () => postMessageToThread(1.5, "value")); + await check("unknown id:", () => postMessageToThread(2147483647, "value")); + await check( + "bad timeout:", + () => postMessageToThread(2147483647, "value", undefined, -1), + ); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/environment-data/builtin-snapshot-worker.cjs b/test-parity/node-suite/worker_threads/environment-data/builtin-snapshot-worker.cjs new file mode 100644 index 0000000000..43cfa9ada2 --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/builtin-snapshot-worker.cjs @@ -0,0 +1,25 @@ +const { getEnvironmentData, parentPort } = require("node:worker_threads"); + +const value = getEnvironmentData("builtins"); +if (!(value instanceof Map)) { + parentPort.postMessage({ + map: false, + date: false, + set: false, + dateValue: "missing", + setValue: "missing", + localMutation: false, + }); +} else { + const date = value.get("date"); + const set = value.get("set"); + value.set("worker-only", true); + parentPort.postMessage({ + map: true, + date: date instanceof Date, + set: set instanceof Set, + dateValue: date instanceof Date ? date.toISOString() : "missing", + setValue: set instanceof Set ? Array.from(set).join(",") : "missing", + localMutation: value.has("worker-only"), + }); +} diff --git a/test-parity/node-suite/worker_threads/environment-data/builtin-snapshot.ts b/test-parity/node-suite/worker_threads/environment-data/builtin-snapshot.ts new file mode 100644 index 0000000000..b16a7d1492 --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/builtin-snapshot.ts @@ -0,0 +1,32 @@ +import { + getEnvironmentData, + setEnvironmentData, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/environment-data"); + +const map = new Map([ + ["date", new Date("2022-03-04T05:06:07.000Z")], + ["set", new Set([2, 4, 6])], +]); +setEnvironmentData("builtins", map); + +const worker = new Worker("./builtin-snapshot-worker.cjs"); +worker.on("message", (message) => { + console.log( + "worker brands:", + message.map, + message.date, + message.set, + ); + console.log( + "worker values:", + message.dateValue, + message.setValue, + message.localMutation, + ); + const parent = getEnvironmentData("builtins"); + console.log("parent unchanged:", parent.has("worker-only"), parent === map); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/environment-data/environment-clean-worker.cjs b/test-parity/node-suite/worker_threads/environment-data/environment-clean-worker.cjs new file mode 100644 index 0000000000..056e53fe93 --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/environment-clean-worker.cjs @@ -0,0 +1,3 @@ +const { getEnvironmentData, parentPort } = require("node:worker_threads"); + +parentPort.postMessage(getEnvironmentData("uncloneable") === undefined); diff --git a/test-parity/node-suite/worker_threads/environment-data/graph-snapshot-worker.cjs b/test-parity/node-suite/worker_threads/environment-data/graph-snapshot-worker.cjs new file mode 100644 index 0000000000..ac4d6c206e --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/graph-snapshot-worker.cjs @@ -0,0 +1,11 @@ +const { + getEnvironmentData, + parentPort, +} = require("node:worker_threads"); + +const graph = getEnvironmentData("graph-snapshot"); +parentPort.postMessage({ + cycle: graph !== undefined && graph.self === graph, + alias: graph?.left !== undefined && graph.left === graph.right, + value: graph?.left?.value ?? "missing", +}); diff --git a/test-parity/node-suite/worker_threads/environment-data/graph-snapshot.ts b/test-parity/node-suite/worker_threads/environment-data/graph-snapshot.ts new file mode 100644 index 0000000000..4b6fbe9b78 --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/graph-snapshot.ts @@ -0,0 +1,28 @@ +import { setEnvironmentData, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/environment-data"); + +const shared = { value: 11 }; +const graph: any = { left: shared, right: shared }; +graph.self = graph; +setEnvironmentData("graph-snapshot", graph); + +try { + const worker = new Worker("./graph-snapshot-worker.cjs"); + worker.on( + "message", + (message) => + console.log("graph:", message.cycle, message.alias, message.value), + ); + worker.on( + "error", + (error) => console.log("error:", error.name, (error as any).code ?? ""), + ); + worker.on("exit", (code) => { + console.log("exit:", code); + setEnvironmentData("graph-snapshot", undefined); + }); +} catch (error) { + console.log("construct:", (error as any)?.name, (error as any)?.code ?? ""); + setEnvironmentData("graph-snapshot", undefined); +} diff --git a/test-parity/node-suite/worker_threads/environment-data/nested-inheritance-worker.cjs b/test-parity/node-suite/worker_threads/environment-data/nested-inheritance-worker.cjs new file mode 100644 index 0000000000..ced59efd61 --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/nested-inheritance-worker.cjs @@ -0,0 +1,15 @@ +const path = require("node:path"); +const { + getEnvironmentData, + parentPort, + setEnvironmentData, + Worker, + workerData, +} = require("node:worker_threads"); + +const inherited = getEnvironmentData("nested-key"); +const first = inherited?.level ?? "missing"; +setEnvironmentData("nested-key", { level: workerData.level }); + +const nested = new Worker(path.join(__dirname, "nested-leaf-worker.cjs")); +nested.on("message", (second) => parentPort.postMessage({ first, second })); diff --git a/test-parity/node-suite/worker_threads/environment-data/nested-inheritance.ts b/test-parity/node-suite/worker_threads/environment-data/nested-inheritance.ts new file mode 100644 index 0000000000..8e46d4b321 --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/nested-inheritance.ts @@ -0,0 +1,30 @@ +import { + getEnvironmentData, + setEnvironmentData, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/environment-data"); + +setEnvironmentData("nested-key", { level: 0 }); +const worker = new Worker("./nested-inheritance-worker.cjs", { + workerData: { level: 1 }, +}); + +let result: any; +worker.on("message", (message) => { + result = message; +}); +worker.on( + "error", + (error: any) => console.log("error:", error?.name, error?.code ?? ""), +); +worker.on("exit", (code) => { + console.log( + "levels:", + result?.first ?? "missing", + result?.second ?? "missing", + ); + console.log("parent:", getEnvironmentData("nested-key").level); + console.log("exit:", code); +}); diff --git a/test-parity/node-suite/worker_threads/environment-data/nested-leaf-worker.cjs b/test-parity/node-suite/worker_threads/environment-data/nested-leaf-worker.cjs new file mode 100644 index 0000000000..2c48f98f0b --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/nested-leaf-worker.cjs @@ -0,0 +1,3 @@ +const { getEnvironmentData, parentPort } = require("node:worker_threads"); + +parentPort.postMessage(getEnvironmentData("nested-key")?.level ?? "missing"); diff --git a/test-parity/node-suite/worker_threads/environment-data/uncloneable-construction.ts b/test-parity/node-suite/worker_threads/environment-data/uncloneable-construction.ts new file mode 100644 index 0000000000..7fe2c5ae20 --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/uncloneable-construction.ts @@ -0,0 +1,16 @@ +import { setEnvironmentData, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/environment-data"); + +try { + setEnvironmentData("uncloneable", () => "value"); + new Worker("./worker-snapshot-worker.cjs"); + console.log("construct: ok"); +} catch (error: any) { + console.log("construct:", error?.name, error?.code ?? ""); +} + +setEnvironmentData("uncloneable", undefined); +const worker = new Worker("./environment-clean-worker.cjs"); +worker.on("message", (message) => console.log("after delete:", message)); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/environment-data/value-identity.ts b/test-parity/node-suite/worker_threads/environment-data/value-identity.ts new file mode 100644 index 0000000000..087aedfbbe --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/value-identity.ts @@ -0,0 +1,14 @@ +import { getEnvironmentData, setEnvironmentData } from "node:worker_threads"; + +const key = { key: true }; +const value = { nested: { count: 1 } }; + +setEnvironmentData(key, value); +console.log("same key:", getEnvironmentData(key) === value); +console.log("different key:", getEnvironmentData({ key: true })); + +value.nested.count = 2; +console.log("live value:", getEnvironmentData(key)?.nested.count); + +setEnvironmentData(key, undefined); +console.log("deleted:", getEnvironmentData(key)); diff --git a/test-parity/node-suite/worker_threads/environment-data/worker-snapshot-worker.cjs b/test-parity/node-suite/worker_threads/environment-data/worker-snapshot-worker.cjs new file mode 100644 index 0000000000..6f7c3203c7 --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/worker-snapshot-worker.cjs @@ -0,0 +1,19 @@ +const { getEnvironmentData, parentPort } = require("node:worker_threads"); + +const value = getEnvironmentData("snapshot"); +if (!value || !value.nested || !value.values) { + parentPort.postMessage({ + initialCount: "missing", + values: "missing", + localCount: "missing", + }); +} else { + const initialCount = value.nested.count; + value.nested.count = 5; + + parentPort.postMessage({ + initialCount, + values: value.values.join(","), + localCount: getEnvironmentData("snapshot").nested.count, + }); +} diff --git a/test-parity/node-suite/worker_threads/environment-data/worker-snapshot.ts b/test-parity/node-suite/worker_threads/environment-data/worker-snapshot.ts new file mode 100644 index 0000000000..5ccb378ec1 --- /dev/null +++ b/test-parity/node-suite/worker_threads/environment-data/worker-snapshot.ts @@ -0,0 +1,22 @@ +import { + getEnvironmentData, + setEnvironmentData, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/environment-data"); + +const original = { nested: { count: 1 }, values: [1, 2] }; +setEnvironmentData("snapshot", original); + +const worker = new Worker("./worker-snapshot-worker.cjs"); +setEnvironmentData("snapshot", { nested: { count: 9 }, values: [9] }); +original.nested.count = 7; + +worker.on("message", (message) => { + console.log("worker initial:", message.initialCount, message.values); + console.log("worker local:", message.localCount); + const current = getEnvironmentData("snapshot"); + console.log("parent current:", current?.nested?.count ?? "missing"); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/main-thread/export-surface.ts b/test-parity/node-suite/worker_threads/main-thread/export-surface.ts new file mode 100644 index 0000000000..e624f37b2a --- /dev/null +++ b/test-parity/node-suite/worker_threads/main-thread/export-surface.ts @@ -0,0 +1,36 @@ +import * as workerThreads from "node:worker_threads"; + +const expected = [ + "BroadcastChannel", + "MessageChannel", + "MessagePort", + "SHARE_ENV", + "Worker", + "getEnvironmentData", + "isInternalThread", + "isMainThread", + "isMarkedAsUntransferable", + "locks", + "markAsUncloneable", + "markAsUntransferable", + "moveMessagePortToContext", + "parentPort", + "postMessageToThread", + "receiveMessageOnPort", + "resourceLimits", + "setEnvironmentData", + "threadId", + "threadName", + "workerData", +]; + +for (const name of expected) { + const value = (workerThreads as Record)[name]; + console.log( + name, + Object.prototype.hasOwnProperty.call(workerThreads, name), + typeof value, + ); +} + +console.log("default:", typeof (workerThreads as Record).default); diff --git a/test-parity/node-suite/worker_threads/main-thread/process-worker-event-late-emit.ts b/test-parity/node-suite/worker_threads/main-thread/process-worker-event-late-emit.ts new file mode 100644 index 0000000000..290fb2cbb1 --- /dev/null +++ b/test-parity/node-suite/worker_threads/main-thread/process-worker-event-late-emit.ts @@ -0,0 +1,18 @@ +import { Worker } from "node:worker_threads"; + +const originalEmit = process.emit; +let worker: Worker; +let synchronous = true; + +worker = new Worker("", { eval: true }); +(process as any).emit = function (event: string, value: any) { + if (event === "worker") { + process.emit = originalEmit; + console.log("late emit:", !synchronous, value === worker); + return true; + } + return originalEmit.apply(this, arguments as any); +}; +synchronous = false; + +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/main-thread/process-worker-event.ts b/test-parity/node-suite/worker_threads/main-thread/process-worker-event.ts new file mode 100644 index 0000000000..e3ed77cbde --- /dev/null +++ b/test-parity/node-suite/worker_threads/main-thread/process-worker-event.ts @@ -0,0 +1,14 @@ +import { Worker } from "node:worker_threads"; + +let observedThreadId: number | undefined; +process.once("worker", (created: any) => { + observedThreadId = created.threadId; +}); + +const worker = new Worker("", { eval: true }); +const initialThreadId = worker.threadId; +worker.on("exit", (code) => { + console.log("event observed:", observedThreadId !== undefined); + console.log("thread match:", observedThreadId === initialThreadId); + console.log("exit:", code); +}); diff --git a/test-parity/node-suite/worker_threads/main-thread/prototype-surface.ts b/test-parity/node-suite/worker_threads/main-thread/prototype-surface.ts new file mode 100644 index 0000000000..c740a59b99 --- /dev/null +++ b/test-parity/node-suite/worker_threads/main-thread/prototype-surface.ts @@ -0,0 +1,43 @@ +import { MessageChannel, Worker } from "node:worker_threads"; + +function summarize(label: string, prototype: object, names: string[]) { + for (const name of names) { + const descriptor = Object.getOwnPropertyDescriptor(prototype, name); + console.log( + label, + name, + Boolean(descriptor), + typeof descriptor?.value, + typeof descriptor?.get, + descriptor?.enumerable, + ); + } +} + +summarize("Worker", Worker.prototype, [ + "postMessage", + "terminate", + "ref", + "unref", + "threadId", + "threadName", + "resourceLimits", + "stdin", + "stdout", + "stderr", + "performance", +]); + +const { port1, port2 } = new MessageChannel(); +summarize("MessagePort", Object.getPrototypeOf(port1), [ + "postMessage", + "start", + "close", + "ref", + "unref", + "hasRef", + "onmessage", + "onmessageerror", +]); +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-channel/broadcast-delivery.ts b/test-parity/node-suite/worker_threads/message-channel/broadcast-delivery.ts index 97a0875182..1a843d5fa2 100644 --- a/test-parity/node-suite/worker_threads/message-channel/broadcast-delivery.ts +++ b/test-parity/node-suite/worker_threads/message-channel/broadcast-delivery.ts @@ -9,38 +9,16 @@ const syncReceiver = new BroadcastChannel("perry-broadcast"); const globalSender = new globalThis.BroadcastChannel("perry-global-broadcast"); const globalListener = new globalThis.BroadcastChannel("perry-global-broadcast"); -let delivered = 0; const observed: Record = {}; -const finish = () => { - delivered += 1; - if (delivered !== 3) { - return; - } - console.log(observed.handler); - console.log(observed.event); - console.log(observed.global); - const afterEvent = receiveMessageOnPort(syncReceiver); - console.log("broadcast after event:", afterEvent ? afterEvent.message : afterEvent); - setTimeout(() => { - sender.close(); - listener.close(); - syncReceiver.close(); - globalSender.close(); - globalListener.close(); - }, 25); -}; listener.onmessage = (event: any) => { observed.handler = `broadcast handler: ${event.type} ${event.data} ${event.target === listener}`; - finish(); }; listener.addEventListener("message", (event: any) => { observed.event = `broadcast event: ${event.type} ${event.data} ${event.target === listener}`; - finish(); }); globalListener.onmessage = (event: any) => { observed.global = `global broadcast handler: ${event.type} ${event.data} ${event.target === globalListener}`; - finish(); }; console.log( @@ -55,3 +33,16 @@ globalSender.postMessage("global-bc-1"); const received = receiveMessageOnPort(syncReceiver); console.log("broadcast receive:", received ? received.message : received); + +setImmediate(() => { + console.log(observed.handler ?? "broadcast handler: missing"); + console.log(observed.event ?? "broadcast event: missing"); + console.log(observed.global ?? "global broadcast handler: missing"); + const afterEvent = receiveMessageOnPort(syncReceiver); + console.log("broadcast after event:", afterEvent ? afterEvent.message : afterEvent); + sender.close(); + listener.close(); + syncReceiver.close(); + globalSender.close(); + globalListener.close(); +}); diff --git a/test-parity/node-suite/worker_threads/message-channel/constructor-call-rules.ts b/test-parity/node-suite/worker_threads/message-channel/constructor-call-rules.ts new file mode 100644 index 0000000000..512382f395 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-channel/constructor-call-rules.ts @@ -0,0 +1,22 @@ +import { MessageChannel, MessagePort } from "node:worker_threads"; + +function outcome(fn: () => unknown): string { + try { + fn(); + return "created"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const channel = new MessageChannel(); +console.log( + "brand:", + channel.port1 instanceof MessagePort, + channel.port1.constructor === MessagePort, +); +console.log("port call:", outcome(() => (MessagePort as any)())); +console.log("port new:", outcome(() => new (MessagePort as any)())); +console.log("channel call:", outcome(() => (MessageChannel as any)())); +channel.port1.close(); +channel.port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-channel/constructor-options.ts b/test-parity/node-suite/worker_threads/message-channel/constructor-options.ts new file mode 100644 index 0000000000..b742a5c791 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-channel/constructor-options.ts @@ -0,0 +1,22 @@ +import { MessageChannel } from "node:worker_threads"; + +for ( + const [label, args] of [ + ["none", []], + ["one", [1]], + ["many", [1, 2, 3]], + ] as const +) { + try { + const channel = Reflect.construct(MessageChannel, args as readonly any[]); + console.log( + label, + channel.port1 instanceof MessagePort, + channel.port2 instanceof MessagePort, + ); + channel.port1.close(); + channel.port2.close(); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} diff --git a/test-parity/node-suite/worker_threads/message-channel/globals.ts b/test-parity/node-suite/worker_threads/message-channel/globals.ts index 861e3d2657..ffc16b0838 100644 --- a/test-parity/node-suite/worker_threads/message-channel/globals.ts +++ b/test-parity/node-suite/worker_threads/message-channel/globals.ts @@ -54,6 +54,14 @@ try { } console.log("MessageChannel call:", messageChannelCall); +let messagePortCall = "did-not-throw"; +try { + (globalThis.MessagePort as any)(); +} catch (_err) { + messagePortCall = "threw"; +} +console.log("MessagePort call:", messagePortCall); + let messagePortNew = "did-not-throw"; try { new (globalThis.MessagePort as any)(); diff --git a/test-parity/node-suite/worker_threads/message-channel/move-closed-validation.ts b/test-parity/node-suite/worker_threads/message-channel/move-closed-validation.ts new file mode 100644 index 0000000000..644b457174 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-channel/move-closed-validation.ts @@ -0,0 +1,34 @@ +import * as vm from "node:vm"; +import { + MessageChannel, + MessagePort, + moveMessagePortToContext, +} from "node:worker_threads"; + +function outcome(fn: () => any): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const closed = new MessageChannel(); +closed.port1.close(); +const context = vm.createContext({ MessagePort }); + +console.log( + "closed:", + outcome(() => moveMessagePortToContext(closed.port1, context)), +); +console.log( + "invalid port:", + outcome(() => moveMessagePortToContext({} as any, context)), +); +console.log( + "invalid context:", + outcome(() => moveMessagePortToContext(closed.port2, {} as any)), +); + +closed.port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-channel/move-context-filehandle-error.ts b/test-parity/node-suite/worker_threads/message-channel/move-context-filehandle-error.ts new file mode 100644 index 0000000000..1c55aa574e --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-channel/move-context-filehandle-error.ts @@ -0,0 +1,52 @@ +import { open } from "node:fs/promises"; +import { MessageChannel, moveMessagePortToContext } from "node:worker_threads"; +import { createContext } from "node:vm"; + +const fixture = + "test-parity/node-suite/worker_threads/worker-lifecycle/process-env-option-worker.cjs"; + +async function main() { + const handle = await open(fixture, "r"); + const { port1, port2 } = new MessageChannel(); + let moved: any; + + try { + moved = moveMessagePortToContext(port2, createContext({})); + } catch (error: any) { + console.log("setup:", error?.name, error?.code ?? ""); + port1.close(); + port2.close(); + await handle.close().catch(() => {}); + return; + } + + const events: string[] = []; + moved.onmessageerror = (event: any) => { + events.push(`error:${event?.data?.code ?? "missing"}`); + }; + moved.onmessage = async (event: any) => { + if (event.data === "barrier") { + console.log("events:", events.join(",")); + moved.close(); + port1.close(); + await handle.close().catch(() => {}); + return; + } + events.push( + `message:${event.data?.constructor?.name ?? typeof event.data}`, + ); + }; + moved.start(); + + try { + port1.postMessage(handle, [handle as any]); + console.log("transfer: ok", handle.fd === -1); + } catch (error: any) { + console.log("transfer:", error?.name, error?.code ?? "", handle.fd === -1); + } + port1.postMessage("barrier"); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/message-channel/move-context-realm-brand.ts b/test-parity/node-suite/worker_threads/message-channel/move-context-realm-brand.ts new file mode 100644 index 0000000000..9296428567 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-channel/move-context-realm-brand.ts @@ -0,0 +1,18 @@ +import * as vm from "node:vm"; +import { + MessageChannel, + MessagePort, + moveMessagePortToContext, +} from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const moved = moveMessagePortToContext(port1, vm.createContext({})); + +console.log( + "realm brand:", + moved instanceof MessagePort, + moved.constructor.name, +); + +moved.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/clone-error-rollback.ts b/test-parity/node-suite/worker_threads/message-port/clone-error-rollback.ts new file mode 100644 index 0000000000..e982646a2d --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/clone-error-rollback.ts @@ -0,0 +1,16 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const buffer = new ArrayBuffer(8); + +try { + port1.postMessage({ buffer, invalid() {} }, [buffer]); + console.log("post: ok"); +} catch (error: any) { + console.log("post:", error?.name, error?.code ?? ""); +} + +console.log("buffer retained:", buffer.byteLength); +console.log("nothing queued:", receiveMessageOnPort(port2)); +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/close-callback-idempotent.ts b/test-parity/node-suite/worker_threads/message-port/close-callback-idempotent.ts new file mode 100644 index 0000000000..a62eb33485 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/close-callback-idempotent.ts @@ -0,0 +1,21 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const events: string[] = []; +let closeEvents = 0; + +function closed(label: string) { + events.push(label); + closeEvents += 1; + if (closeEvents !== 2) return; + setImmediate(() => { + console.log("close events:", events.sort().join(",")); + console.log("closed methods:", port1.start(), port1.ref(), port1.unref()); + }); +} + +port1.on("close", () => closed("port1-event")); +port2.on("close", () => closed("port2-event")); +port1.close(() => events.push("port1-callback")); +port1.close(() => events.push("second-callback")); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/close-callback-order.ts b/test-parity/node-suite/worker_threads/message-port/close-callback-order.ts new file mode 100644 index 0000000000..1ca3d9ebac --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/close-callback-order.ts @@ -0,0 +1,40 @@ +import { MessageChannel } from "node:worker_threads"; + +function first(): Promise { + return new Promise((resolve) => { + const { port1, port2 } = new MessageChannel(); + const order: string[] = []; + port1.on("close", () => order.push("A")); + port1.close(() => order.push("B")); + port1.on("close", () => { + order.push("C"); + console.log("first:", order.join(",")); + port2.close(); + resolve(); + }); + order.push("sync"); + }); +} + +function second(): Promise { + return new Promise((resolve) => { + const { port1, port2 } = new MessageChannel(); + const order: string[] = []; + port1.close(() => order.push("B")); + port1.on("close", () => { + order.push("C"); + console.log("second:", order.join(",")); + port2.close(); + resolve(); + }); + }); +} + +async function main() { + await first(); + await second(); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/message-port/close-dispatch-flush.ts b/test-parity/node-suite/worker_threads/message-port/close-dispatch-flush.ts new file mode 100644 index 0000000000..c9ff61e728 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/close-dispatch-flush.ts @@ -0,0 +1,47 @@ +import { MessageChannel } from "node:worker_threads"; + +function closeBeforeDispatch(): Promise { + return new Promise((resolve) => { + const { port1, port2 } = new MessageChannel(); + const seen: number[] = []; + port1.on("message", (value) => seen.push(value)); + port1.on("close", () => { + console.log("outside dispatch:", seen.join(",") || "empty"); + port2.close(); + resolve(); + }); + port2.postMessage(1); + port2.postMessage(2); + port1.close(); + }); +} + +function closeInsideDispatch(): Promise { + return new Promise((resolve) => { + const { port1, port2 } = new MessageChannel(); + const seen: number[] = []; + port1.on("message", (value) => { + seen.push(value); + if (value === 1) { + port1.close(); + } + }); + port1.on("close", () => { + console.log("inside dispatch:", seen.join(",") || "empty"); + port2.close(); + resolve(); + }); + port2.postMessage(1); + port2.postMessage(2); + port2.postMessage(3); + }); +} + +async function main() { + await closeBeforeDispatch(); + await closeInsideDispatch(); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/message-port/close-ordering.ts b/test-parity/node-suite/worker_threads/message-port/close-ordering.ts new file mode 100644 index 0000000000..e807c27691 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/close-ordering.ts @@ -0,0 +1,23 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const events: string[] = []; +let closes = 0; + +function closed(side: string) { + events.push(`${side}-close`); + closes += 1; + if (closes === 2) { + console.log("events:", events.join(",")); + port2.postMessage("after-close"); + const packet = receiveMessageOnPort(port1); + console.log("after close:", packet?.message); + } +} + +port1.on("close", () => closed("port1")); +port2.on("message", (message) => events.push(`message:${message}`)); +port2.on("close", () => closed("port2")); + +port1.postMessage("before-close"); +port1.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/closed-transfer-error-brand.ts b/test-parity/node-suite/worker_threads/message-port/closed-transfer-error-brand.ts new file mode 100644 index 0000000000..c5015744cb --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/closed-transfer-error-brand.ts @@ -0,0 +1,27 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const { port1: target1, port2: target2 } = new MessageChannel(); +const buffer = new ArrayBuffer(8); + +port2.close(); + +try { + target1.postMessage({ buffer, port: port2 }, [buffer, port2]); + console.log("transfer: ok"); +} catch (error: any) { + console.log( + "error:", + error?.name, + error?.code ?? "", + error instanceof Error, + error instanceof DOMException, + error?.constructor?.name, + ); +} +console.log("rollback:", buffer.byteLength, port2 instanceof MessagePort); + +port1.close(); +port2.close(); +target1.close(); +target2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/custom-event-emit.ts b/test-parity/node-suite/worker_threads/message-port/custom-event-emit.ts new file mode 100644 index 0000000000..524092f205 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/custom-event-emit.ts @@ -0,0 +1,23 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const events: string[] = []; + +port2.addEventListener("custom", (event: any) => { + events.push( + `event:${event.type}:${event.detail}:${event.target === port2}`, + ); +}); +port2.on("custom", (detail) => events.push(`node:${detail}`)); + +const emit = (port2 as any).emit; +console.log( + "emit return:", + typeof emit === "function" + ? emit.call(port2, "custom", "value") + : "unsupported", +); +console.log("events:", events.join(",")); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/custom-inspect-ref-state.ts b/test-parity/node-suite/worker_threads/message-port/custom-inspect-ref-state.ts new file mode 100644 index 0000000000..13ba1c6fc2 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/custom-inspect-ref-state.ts @@ -0,0 +1,23 @@ +import { MessageChannel } from "node:worker_threads"; +import { inspect } from "node:util"; + +const { port1, port2 } = new MessageChannel(); + +function state(label: string) { + const output = inspect(port1); + console.log( + label, + output.includes("active: true"), + output.includes("refed: true"), + output.includes("refed: false"), + ); +} + +console.log("custom:", typeof (port1 as any)[inspect.custom]); +state("initial:"); +port1.ref(); +state("refed:"); +port1.unref(); +state("unrefed:"); +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/distinct-listener-order.ts b/test-parity/node-suite/worker_threads/message-port/distinct-listener-order.ts new file mode 100644 index 0000000000..6ca627ba93 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/distinct-listener-order.ts @@ -0,0 +1,15 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const events: string[] = []; + +port1.on("message", (value) => events.push(`first:${value}`)); +port1.on("message", (value) => events.push(`second:${value}`)); +port1.on("message", (value) => { + events.push(`third:${value}`); + console.log("events:", events.join(",")); + port1.close(); + port2.close(); +}); + +port2.postMessage("value"); diff --git a/test-parity/node-suite/worker_threads/message-port/duplicate-transfer.ts b/test-parity/node-suite/worker_threads/message-port/duplicate-transfer.ts new file mode 100644 index 0000000000..1f91c1220c --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/duplicate-transfer.ts @@ -0,0 +1,33 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +function outcome(label: string, fn: () => void) { + try { + fn(); + console.log(label, "ok"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} + +const carrier = new MessageChannel(); +const buffer = new ArrayBuffer(8); +outcome( + "duplicate buffer:", + () => carrier.port1.postMessage(buffer, [buffer, buffer]), +); +console.log("buffer retained:", buffer.byteLength); + +const movable = new MessageChannel(); +outcome( + "duplicate port:", + () => + carrier.port1.postMessage(movable.port1, [movable.port1, movable.port1]), +); +movable.port1.postMessage("retained"); +const retained = receiveMessageOnPort(movable.port2); +console.log("port retained:", retained ? retained.message : "missing"); + +carrier.port1.close(); +carrier.port2.close(); +movable.port1.close(); +movable.port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/event-ports.ts b/test-parity/node-suite/worker_threads/message-port/event-ports.ts new file mode 100644 index 0000000000..da6b67ebef --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/event-ports.ts @@ -0,0 +1,28 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const carrier = new MessageChannel(); +const movable = new MessageChannel(); + +carrier.port2.addEventListener("message", (event: MessageEvent) => { + const transferred = event.data?.port; + const ports = Array.isArray(event.ports) ? event.ports : []; + console.log( + "event ports:", + ports.length, + ports[0] === transferred, + typeof transferred?.postMessage, + ); + if (typeof transferred?.postMessage === "function") { + transferred.postMessage("via-event-port"); + } + const packet = receiveMessageOnPort(movable.port2); + console.log("delivery:", packet ? packet.message : undefined); + + if (typeof transferred?.close === "function") transferred.close(); + carrier.port1.close(); + carrier.port2.close(); + movable.port1.close(); + movable.port2.close(); +}); +carrier.port2.start(); +carrier.port1.postMessage({ port: movable.port1 }, [movable.port1]); diff --git a/test-parity/node-suite/worker_threads/message-port/frozen-eventtarget-prototype.ts b/test-parity/node-suite/worker_threads/message-port/frozen-eventtarget-prototype.ts new file mode 100644 index 0000000000..6228d68da0 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/frozen-eventtarget-prototype.ts @@ -0,0 +1,20 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +Object.freeze(EventTarget.prototype); +console.log("frozen:", Object.isFrozen(EventTarget.prototype)); + +const { port1, port2 } = new MessageChannel(); +try { + const listener = () => console.log("unexpected async listener"); + port2.on("message", listener); + console.log("registered:", port2.listenerCount("message")); + port1.postMessage("value"); + console.log("received:", receiveMessageOnPort(port2)?.message); + port2.off("message", listener); + console.log("removed:", port2.listenerCount("message")); +} catch (error) { + console.log("error:", (error as any)?.name, (error as any)?.code ?? ""); +} finally { + port1.close(); + port2.close(); +} diff --git a/test-parity/node-suite/worker_threads/message-port/listener-first-registration.ts b/test-parity/node-suite/worker_threads/message-port/listener-first-registration.ts new file mode 100644 index 0000000000..44923b63e1 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/listener-first-registration.ts @@ -0,0 +1,44 @@ +import { MessageChannel } from "node:worker_threads"; + +function run( + label: string, + first: "on" | "once", + second: "on" | "once", +): Promise { + return new Promise((resolve) => { + const { port1, port2 } = new MessageChannel(); + let calls = 0; + const listener = () => calls += 1; + + port1[first]("message", listener); + port1[second]("message", listener); + + const observer = (value: number) => { + if (value !== 2) return; + port1.off("message", observer); + const remaining = typeof (port1 as any).listenerCount === "function" + ? (port1 as any).listenerCount("message") + : "unsupported"; + console.log(`${label}:`, calls, remaining); + port1.close(); + }; + port1.on("message", observer); + port1.on("close", () => { + port2.close(); + resolve(); + }); + port2.postMessage(1); + port2.postMessage(2); + }); +} + +async function main() { + await run("on+on", "on", "on"); + await run("on+once", "on", "once"); + await run("once+on", "once", "on"); + await run("once+once", "once", "once"); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/message-port/listener-removal-once.ts b/test-parity/node-suite/worker_threads/message-port/listener-removal-once.ts new file mode 100644 index 0000000000..9e22292624 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/listener-removal-once.ts @@ -0,0 +1,31 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const events: string[] = []; + +function removed(value: any) { + events.push(`removed:${value}`); +} + +port1.on("message", removed); +port1.off("message", removed); +port1.once("message", (value) => events.push(`once:${value}`)); +port1.on("message", (value) => { + events.push(`on:${value}`); + if (value === "second") { + port1.close(); + } +}); +port1.on("close", () => { + console.log("events:", events.join(",")); + const count = typeof (port1 as any).listenerCount === "function" + ? `${(port1 as any).listenerCount("message")},${ + (port1 as any).listenerCount("close") + }` + : "unsupported"; + console.log("counts:", count); + port2.close(); +}); + +port2.postMessage("first"); +port2.postMessage("second"); diff --git a/test-parity/node-suite/worker_threads/message-port/listener-scope.ts b/test-parity/node-suite/worker_threads/message-port/listener-scope.ts new file mode 100644 index 0000000000..d30bd00ed3 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/listener-scope.ts @@ -0,0 +1,27 @@ +import { MessageChannel } from "node:worker_threads"; + +const first = new MessageChannel(); +const second = new MessageChannel(); +const events: string[] = []; + +function shared(value: any) { + events.push(typeof value === "string" ? value : value?.type ?? "unknown"); +} + +first.port1.on("message", shared); +first.port1.on("close", shared); +second.port1.on("message", shared); +first.port1.off("message", shared); + +second.port1.on("message", () => { + first.port1.close(); +}); +first.port1.on("close", () => { + console.log("events:", events.join(",")); + first.port2.close(); + second.port1.close(); + second.port2.close(); +}); + +first.port2.postMessage("first-message"); +second.port2.postMessage("second-message"); diff --git a/test-parity/node-suite/worker_threads/message-port/listener-validation.ts b/test-parity/node-suite/worker_threads/message-port/listener-validation.ts new file mode 100644 index 0000000000..e2a12fe060 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/listener-validation.ts @@ -0,0 +1,24 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); + +function outcome(label: string, fn: () => void) { + try { + fn(); + console.log(label, "ok"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} + +outcome("on null:", () => (port1 as any).on("message", null)); +outcome("once number:", () => (port1 as any).once("message", 1)); +outcome("addListener object:", () => (port1 as any).addListener("message", {})); +outcome("off undefined:", () => (port1 as any).off("message", undefined)); +outcome( + "addEventListener null:", + () => port1.addEventListener("message", null as any), +); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/listeners-while-closing.ts b/test-parity/node-suite/worker_threads/message-port/listeners-while-closing.ts new file mode 100644 index 0000000000..d0e9663601 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/listeners-while-closing.ts @@ -0,0 +1,31 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const dummy = () => {}; + +function outcome(fn: () => unknown): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +function exercise(port: any): string { + return [ + outcome(() => port.on("message", dummy)), + outcome(() => port.off("message", dummy)), + outcome(() => port.addListener("message", dummy)), + outcome(() => port.removeListener("message", dummy)), + ].join(","); +} + +port1.on("message", dummy); +port1.close(() => { + console.log("closed port:", exercise(port1)); + console.log("closed peer:", exercise(port2)); + port2.close(); +}); +console.log("closing port:", exercise(port1)); +console.log("closing peer:", exercise(port2)); diff --git a/test-parity/node-suite/worker_threads/message-port/max-listeners-remove-all.ts b/test-parity/node-suite/worker_threads/message-port/max-listeners-remove-all.ts new file mode 100644 index 0000000000..969e2dc430 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/max-listeners-remove-all.ts @@ -0,0 +1,56 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const getMaxListeners = (port1 as any).getMaxListeners; +const setMaxListeners = (port1 as any).setMaxListeners; + +console.log("methods:", typeof getMaxListeners, typeof setMaxListeners); +if ( + typeof getMaxListeners === "function" && + typeof setMaxListeners === "function" +) { + console.log("initial max:", getMaxListeners.call(port1)); + console.log("set return:", setMaxListeners.call(port1, 3)); + console.log("updated max:", getMaxListeners.call(port1)); +} + +const messageListener = () => {}; +const customListener = () => {}; +port1.on("message", messageListener); +port1.on("custom", customListener); + +const removeAllListeners = (port1 as any).removeAllListeners; +console.log( + "remove return:", + typeof removeAllListeners === "function" + ? removeAllListeners.call(port1, "message") === port1 + : "unsupported", +); +console.log( + "remaining:", + typeof (port1 as any).listenerCount === "function" + ? (port1 as any).listenerCount("message") + : "unsupported", + typeof (port1 as any).listenerCount === "function" + ? (port1 as any).listenerCount("custom") + : "unsupported", +); + +function invalid(label: string, value: any) { + try { + if (typeof setMaxListeners !== "function") { + throw new TypeError("unsupported"); + } + setMaxListeners.call(port1, value); + console.log(label, "ok"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} + +invalid("negative:", -1); +invalid("string:", "3"); +invalid("nan:", Number.NaN); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/messageevent-constructor.ts b/test-parity/node-suite/worker_threads/message-port/messageevent-constructor.ts new file mode 100644 index 0000000000..faeba4c2c1 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/messageevent-constructor.ts @@ -0,0 +1,42 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const event = new MessageEvent("message", { + data: { value: 1 }, + origin: "worker-origin", + lastEventId: "event-1", + source: port1, + ports: [port2], +}); + +console.log( + "event:", + event.type, + event.data?.value ?? "missing", + event.origin, + event.lastEventId, +); +console.log("source:", event.source === port1); +console.log( + "ports:", + event.ports?.length ?? "missing", + event.ports?.[0] === port2, +); +console.log("brands:", event instanceof Event, event instanceof MessageEvent); + +for ( + const [label, init] of [ + ["bad source", { source: {} }], + ["bad ports", { ports: [{}] }], + ] as const +) { + try { + new MessageEvent("message", init as any); + console.log(label, "ok"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/messageevent-defaults-coercion.ts b/test-parity/node-suite/worker_threads/message-port/messageevent-defaults-coercion.ts new file mode 100644 index 0000000000..a42406eff7 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/messageevent-defaults-coercion.ts @@ -0,0 +1,56 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); + +for ( + const [label, event] of [ + ["default", new MessageEvent("message")], + [ + "undefined", + new MessageEvent("message", { data: undefined, origin: "origin" }), + ], + [ + "coerced", + new MessageEvent("message", { + data: 2, + origin: 1 as any, + lastEventId: 0 as any, + }), + ], + [ + "source", + new MessageEvent("messageerror", { + lastEventId: "event", + source: port1, + }), + ], + ] as const +) { + console.log( + label, + event.type, + event.data, + event.origin, + event.lastEventId, + event.source === port1 ? "port" : event.source, + event.ports?.length ?? "missing", + ); +} + +for ( + const [label, init] of [ + ["number source", { source: 1 }], + ["noniterable ports", { ports: 0 }], + ["null port", { ports: [null] }], + ] as const +) { + try { + new MessageEvent("message", init as any); + console.log(label, "ok"); + } catch (error) { + console.log(label, (error as any)?.name, (error as any)?.code ?? ""); + } +} + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/messageevent-iterables.ts b/test-parity/node-suite/worker_threads/message-port/messageevent-iterables.ts new file mode 100644 index 0000000000..bc341ba735 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/messageevent-iterables.ts @@ -0,0 +1,36 @@ +import { MessageChannel } from "node:worker_threads"; + +const first = new MessageChannel(); +const second = new MessageChannel(); +let iterations = 0; + +const ports = { + *[Symbol.iterator]() { + iterations += 1; + yield first.port1; + yield second.port1; + }, +}; + +const event = new MessageEvent("message", { ports: ports as any }); +console.log( + "iterable:", + iterations, + event.ports?.length ?? "missing", + event.ports?.[0] === first.port1, + event.ports?.[1] === second.port1, +); + +try { + new MessageEvent("message", { + ports: new Set([first.port2, {} as MessagePort]) as any, + }); + console.log("invalid iterable: ok"); +} catch (error: any) { + console.log("invalid iterable:", error?.name, error?.code ?? ""); +} + +first.port1.close(); +first.port2.close(); +second.port1.close(); +second.port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/method-receivers.ts b/test-parity/node-suite/worker_threads/message-port/method-receivers.ts new file mode 100644 index 0000000000..ce2a1a8e87 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/method-receivers.ts @@ -0,0 +1,23 @@ +import { MessageChannel, MessagePort } from "node:worker_threads"; + +function check(name: string, method: Function, args: any[] = []) { + for (const receiver of [undefined, null, {}, new EventTarget()]) { + try { + Reflect.apply(method, receiver, args); + console.log(name, typeof receiver, "ok"); + } catch (error: any) { + console.log(name, typeof receiver, error?.name, error?.code ?? ""); + } + } +} + +check("postMessage", MessagePort.prototype.postMessage, ["value"]); +check("start", MessagePort.prototype.start); +check("close", MessagePort.prototype.close); +check("ref", MessagePort.prototype.ref); +check("unref", MessagePort.prototype.unref); +check("hasRef", MessagePort.prototype.hasRef); + +const { port1, port2 } = new MessageChannel(); +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/node-event-target-surface.ts b/test-parity/node-suite/worker_threads/message-port/node-event-target-surface.ts new file mode 100644 index 0000000000..933f15ce92 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/node-event-target-surface.ts @@ -0,0 +1,37 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const methods = [ + "addListener", + "emit", + "eventNames", + "listenerCount", + "listeners", + "off", + "on", + "once", + "prependListener", + "prependOnceListener", + "rawListeners", + "removeAllListeners", + "removeListener", +]; + +console.log( + "methods:", + methods.map((name) => `${name}:${typeof (port1 as any)[name]}`).join(","), +); +console.log( + "event target:", + typeof port1.addEventListener, + typeof port1.removeEventListener, + typeof port1.dispatchEvent, +); + +const eventNames = typeof (port1 as any).eventNames === "function" + ? JSON.stringify((port1 as any).eventNames()) + : "unsupported"; +console.log("event names:", eventNames); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/off-pending-once.ts b/test-parity/node-suite/worker_threads/message-port/off-pending-once.ts new file mode 100644 index 0000000000..dcc99b0601 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/off-pending-once.ts @@ -0,0 +1,21 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +let calls = 0; +const listener = () => calls += 1; + +port1.once("message", listener); +const before = typeof (port1 as any).listenerCount === "function" + ? (port1 as any).listenerCount("message") + : "unsupported"; +port1.off("message", listener); +const after = typeof (port1 as any).listenerCount === "function" + ? (port1 as any).listenerCount("message") + : "unsupported"; + +port1.on("message", () => { + console.log("counts:", before, after, calls); + port1.close(); + port2.close(); +}); +port2.postMessage("probe"); diff --git a/test-parity/node-suite/worker_threads/message-port/on-listener-dedup.ts b/test-parity/node-suite/worker_threads/message-port/on-listener-dedup.ts new file mode 100644 index 0000000000..dddb75d5fe --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/on-listener-dedup.ts @@ -0,0 +1,26 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const sharedCalls: string[] = []; + +function shared(value: unknown) { + sharedCalls.push(String(value)); +} + +port1.on("message", shared); +port1.on("message", shared); +port1.on("message", shared); + +port1.on("message", (value) => { + if (value === "first") { + console.log("first:", sharedCalls.join(",")); + port1.off("message", shared); + port2.postMessage("after-off"); + } else if (value === "after-off") { + console.log("after off:", sharedCalls.join(",")); + port1.close(); + port2.close(); + } +}); + +port2.postMessage("first"); diff --git a/test-parity/node-suite/worker_threads/message-port/on-listener-event-scope.ts b/test-parity/node-suite/worker_threads/message-port/on-listener-event-scope.ts new file mode 100644 index 0000000000..383d592ab2 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/on-listener-event-scope.ts @@ -0,0 +1,24 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const events: string[] = []; + +function shared(value?: unknown) { + events.push(typeof value === "string" ? `message:${value}` : "close"); +} + +port1.on("message", shared); +port1.on("message", shared); +port1.on("close", shared); +port1.on("close", shared); + +port1.on("message", () => { + port1.off("message", shared); + port1.close(); + port2.close(); +}); +port1.on("close", () => { + console.log("events:", events.join(",")); +}); + +port2.postMessage("value"); diff --git a/test-parity/node-suite/worker_threads/message-port/once-registry-after-fire.ts b/test-parity/node-suite/worker_threads/message-port/once-registry-after-fire.ts new file mode 100644 index 0000000000..d69d8c3af5 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/once-registry-after-fire.ts @@ -0,0 +1,30 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +let fired = 0; + +function count() { + return typeof (port1 as any).listenerCount === "function" + ? (port1 as any).listenerCount("message") + : "unsupported"; +} + +function named() { + return typeof (port1 as any).eventNames === "function" + ? (port1 as any).eventNames().includes("message") + : "unsupported"; +} + +port1.once("message", () => { + fired += 1; + console.log( + "after fire:", + fired, + count(), + named(), + ); + port1.close(); + port2.close(); +}); +console.log("before:", count()); +port2.postMessage("trigger"); diff --git a/test-parity/node-suite/worker_threads/message-port/onmessage-event.ts b/test-parity/node-suite/worker_threads/message-port/onmessage-event.ts new file mode 100644 index 0000000000..385ba97b67 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/onmessage-event.ts @@ -0,0 +1,17 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +port1.onmessage = (event) => { + console.log( + "event:", + event.type, + event.data.value, + event.target === port1, + event.currentTarget === port1, + Array.isArray(event.ports) ? event.ports.length : "missing", + ); + port1.close(); + port2.close(); +}; + +port2.postMessage({ value: 7 }); diff --git a/test-parity/node-suite/worker_threads/message-port/onmessage-nonfunction.ts b/test-parity/node-suite/worker_threads/message-port/onmessage-nonfunction.ts new file mode 100644 index 0000000000..0d2fb89ed9 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/onmessage-nonfunction.ts @@ -0,0 +1,27 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const assigned = { not: "callable" }; + +port1.onmessage = () => console.log("unexpected handler"); +console.log( + "before:", + typeof port1.onmessage, + port1.hasRef(), + typeof (port1 as any).listenerCount === "function" + ? (port1 as any).listenerCount("message") + : "unsupported", +); + +(port1 as any).onmessage = assigned; +console.log( + "after:", + port1.onmessage === assigned, + port1.hasRef(), + typeof (port1 as any).listenerCount === "function" + ? (port1 as any).listenerCount("message") + : "unsupported", +); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/onmessage-replacement.ts b/test-parity/node-suite/worker_threads/message-port/onmessage-replacement.ts new file mode 100644 index 0000000000..8426cfa1ef --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/onmessage-replacement.ts @@ -0,0 +1,19 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const events: string[] = []; + +port1.onmessage = (event) => events.push(`first:${event.data}`); +port1.onmessage = (event) => { + events.push(`second:${event.data}`); + port1.onmessage = null; + port2.postMessage("after-null"); + setImmediate(() => { + console.log("events:", events.join(",")); + console.log("handler:", port1.onmessage); + port1.close(); + port2.close(); + }); +}; + +port2.postMessage("value"); diff --git a/test-parity/node-suite/worker_threads/message-port/post-message-missing.ts b/test-parity/node-suite/worker_threads/message-port/post-message-missing.ts new file mode 100644 index 0000000000..f6720beca8 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/post-message-missing.ts @@ -0,0 +1,14 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); + +try { + (port1.postMessage as any)(); + console.log("post: ok"); +} catch (error: any) { + console.log("post:", error?.name, error?.code ?? ""); +} +console.log("queue:", receiveMessageOnPort(port2)); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/post-message-validation.ts b/test-parity/node-suite/worker_threads/message-port/post-message-validation.ts new file mode 100644 index 0000000000..2013ed07ae --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/post-message-validation.ts @@ -0,0 +1,29 @@ +import { MessageChannel } from "node:worker_threads"; + +function outcome(value: any, transfer?: any): string { + try { + if (arguments.length === 1) { + port1.postMessage(value); + } else { + port1.postMessage(value, transfer); + } + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const { port1, port2 } = new MessageChannel(); +console.log("function:", outcome(() => {})); +console.log("symbol:", outcome(Symbol("value"))); +console.log("number transfer:", outcome("value", 1)); +console.log("false transfer:", outcome("value", false)); +console.log("string transfer:", outcome("value", "bad")); +console.log("symbol transfer:", outcome("value", Symbol("bad"))); +console.log("options transfer:", outcome("value", { transfer: null })); +console.log("zero options transfer:", outcome("value", { transfer: 0 })); +console.log("false options transfer:", outcome("value", { transfer: false })); +console.log("object options transfer:", outcome("value", { transfer: {} })); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/prototype-own-surface.ts b/test-parity/node-suite/worker_threads/message-port/prototype-own-surface.ts new file mode 100644 index 0000000000..eb1f0bd628 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/prototype-own-surface.ts @@ -0,0 +1,22 @@ +import { MessageChannel, MessagePort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +console.log( + "own names:", + Object.getOwnPropertyNames(MessagePort.prototype).sort().join(","), +); +console.log( + "prototype identity:", + Object.getPrototypeOf(port1) === MessagePort.prototype, +); + +const event = new MessageEvent("message"); +console.log( + "event brands:", + event instanceof MessageEvent, + event instanceof Event, + Object.getPrototypeOf(event) === MessageEvent.prototype, +); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/queued-before-peer-close.ts b/test-parity/node-suite/worker_threads/message-port/queued-before-peer-close.ts new file mode 100644 index 0000000000..9385e68d48 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/queued-before-peer-close.ts @@ -0,0 +1,37 @@ +import { MessageChannel } from "node:worker_threads"; + +function run(label: string, closeFirst: boolean): Promise { + return new Promise((resolve) => { + const { port1, port2 } = new MessageChannel(); + const events: string[] = []; + + port2.postMessage("first"); + port2.postMessage("second"); + port2.close(); + + const onMessage = (value: unknown) => events.push(`message:${value}`); + const onClose = () => { + events.push("close"); + console.log(`${label}:`, events.join(",")); + port1.close(); + resolve(); + }; + + if (closeFirst) { + port1.on("close", onClose); + port1.on("message", onMessage); + } else { + port1.on("message", onMessage); + port1.on("close", onClose); + } + }); +} + +async function main() { + await run("message-first", false); + await run("close-first", true); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/message-port/receive-after-start.ts b/test-parity/node-suite/worker_threads/message-port/receive-after-start.ts new file mode 100644 index 0000000000..155c9b9331 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/receive-after-start.ts @@ -0,0 +1,18 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +console.log("empty:", receiveMessageOnPort(port1)); +port1.start(); +port2.postMessage({ sequence: 1 }); +port2.postMessage({ sequence: 2 }); + +const first = receiveMessageOnPort(port1)?.message; +const second = receiveMessageOnPort(port1)?.message; +console.log("received:", first?.sequence, second?.sequence); + +port1.on("message", (value) => { + console.log("event:", value); + port1.close(); + port2.close(); +}); +port2.postMessage("barrier"); diff --git a/test-parity/node-suite/worker_threads/message-port/receive-consumes-event.ts b/test-parity/node-suite/worker_threads/message-port/receive-consumes-event.ts new file mode 100644 index 0000000000..2fa2720c9f --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/receive-consumes-event.ts @@ -0,0 +1,18 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const events: string[] = []; + +port2.on("message", (message) => { + events.push(String(message)); + if (message === "barrier") { + console.log("events:", events.join(",")); + port1.close(); + port2.close(); + } +}); + +port1.postMessage("synchronous"); +const packet = receiveMessageOnPort(port2); +console.log("received:", packet?.message); +port1.postMessage("barrier"); diff --git a/test-parity/node-suite/worker_threads/message-port/receive-fifo-validation.ts b/test-parity/node-suite/worker_threads/message-port/receive-fifo-validation.ts new file mode 100644 index 0000000000..46281d117b --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/receive-fifo-validation.ts @@ -0,0 +1,32 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +function receive(value: any): string { + try { + const packet = receiveMessageOnPort(value); + return packet ? `message:${JSON.stringify(packet.message)}` : "empty"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const { port1, port2 } = new MessageChannel(); +console.log("initial:", receive(port2)); +port1.postMessage({ index: 1 }); +port1.postMessage({ index: 2 }); +console.log("fifo one:", receive(port2)); +console.log("fifo two:", receive(port2)); +console.log("drained:", receive(port2)); + +let listenerCalls = 0; +port2.on("message", () => { + listenerCalls += 1; +}); +port1.postMessage("sync-wins"); +console.log("sync with listener:", receive(port2), listenerCalls); + +for (const value of [null, 0, {}, []]) { + console.log("invalid:", receive(value)); +} + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/ref-after-peer-close.ts b/test-parity/node-suite/worker_threads/message-port/ref-after-peer-close.ts new file mode 100644 index 0000000000..1dadbd218b --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/ref-after-peer-close.ts @@ -0,0 +1,19 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); + +port1.on("message", () => {}); +port1.on("close", () => { + port1.ref(); + port1.onmessage = () => {}; + console.log( + "after peer close:", + port1.hasRef(), + typeof (port1 as any).listenerCount === "function" + ? (port1 as any).listenerCount("message") + : "unsupported", + typeof port1.onmessage, + ); + port1.close(); +}); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/ref-state.ts b/test-parity/node-suite/worker_threads/message-port/ref-state.ts new file mode 100644 index 0000000000..38edec3e7c --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/ref-state.ts @@ -0,0 +1,15 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +console.log("initial:", port1.hasRef()); + +const listener = () => {}; +port1.on("message", listener); +console.log("with listener:", port1.hasRef()); +console.log("unref return:", port1.unref() === port1, port1.hasRef()); +console.log("ref return:", port1.ref() === port1, port1.hasRef()); + +port1.off("message", listener); +console.log("without listener:", port1.hasRef()); +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/self-and-closed-transfer.ts b/test-parity/node-suite/worker_threads/message-port/self-and-closed-transfer.ts new file mode 100644 index 0000000000..f1650f5eb5 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/self-and-closed-transfer.ts @@ -0,0 +1,31 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +function outcome(label: string, fn: () => void) { + try { + fn(); + console.log(label, "ok"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} + +const self = new MessageChannel(); +outcome("self transfer:", () => self.port1.postMessage(null, [self.port1])); +self.port1.postMessage("usable"); +console.log("self retained:", receiveMessageOnPort(self.port2)?.message); + +const carrier = new MessageChannel(); +const closed = new MessageChannel(); +closed.port1.close(); +const buffer = new ArrayBuffer(4); +outcome( + "closed transfer:", + () => carrier.port1.postMessage({ buffer }, [buffer, closed.port1]), +); +console.log("rollback:", buffer.byteLength); + +self.port1.close(); +self.port2.close(); +carrier.port1.close(); +carrier.port2.close(); +closed.port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/start-and-listeners.ts b/test-parity/node-suite/worker_threads/message-port/start-and-listeners.ts new file mode 100644 index 0000000000..b8c3845707 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/start-and-listeners.ts @@ -0,0 +1,20 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const messages: string[] = []; + +function listener(event: MessageEvent) { + messages.push(String(event.data)); + if (messages.length === 2) { + console.log("delivered:", messages.join(",")); + port1.close(); + port2.close(); + } +} + +port1.addEventListener("message", listener); +port1.addEventListener("message", listener); +port2.postMessage("first"); +port2.postMessage("second"); +console.log("before start:", messages.length); +console.log("start return:", port1.start()); diff --git a/test-parity/node-suite/worker_threads/message-port/tampered-listener-registry.ts b/test-parity/node-suite/worker_threads/message-port/tampered-listener-registry.ts new file mode 100644 index 0000000000..67d7729e8e --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/tampered-listener-registry.ts @@ -0,0 +1,58 @@ +import { MessageChannel } from "node:worker_threads"; + +function boom(name: string) { + return function () { + throw new Error(`tampered ${name}`); + }; +} + +for ( + const [constructor, names] of [ + [Map, [ + "get", + "set", + "delete", + "has", + "values", + "keys", + "entries", + "forEach", + ]], + [Set, ["add", "delete", "has", "values", "keys", "entries", "forEach"]], + [WeakMap, ["get", "set", "has", "delete"]], + ] as const +) { + for (const name of names) { + (constructor.prototype as any)[name] = boom(`${constructor.name}.${name}`); + } + if (constructor !== WeakMap) { + Object.defineProperty(constructor.prototype, "size", { + configurable: true, + get: boom(`${constructor.name}.size`), + }); + } + (constructor.prototype as any)[Symbol.iterator] = boom( + `${constructor.name}[Symbol.iterator]`, + ); +} + +let channel: MessageChannel | undefined; +try { + channel = new MessageChannel(); + const { port1 } = channel; + const listener = () => {}; + port1.on("message", listener); + const before = (port1 as any).listenerCount("message"); + port1.once("close", () => {}); + const names = (port1 as any).eventNames().sort().join(","); + port1.off("message", listener); + const after = (port1 as any).listenerCount("message"); + port1.removeAllListeners(); + const empty = (port1 as any).eventNames().length; + console.log("registry:", before, names, after, empty); +} catch (error: any) { + console.log("registry error:", error?.name, error?.message); +} finally { + channel?.port1.close(); + channel?.port2.close(); +} diff --git a/test-parity/node-suite/worker_threads/message-port/transfer-during-close-flush.ts b/test-parity/node-suite/worker_threads/message-port/transfer-during-close-flush.ts new file mode 100644 index 0000000000..f6cdf732c9 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/transfer-during-close-flush.ts @@ -0,0 +1,33 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const closing = new MessageChannel(); +const carrier = new MessageChannel(); +let deliveries = 0; +let transfer = "missing"; + +closing.port1.on("message", () => { + deliveries += 1; + if (deliveries === 1) { + closing.port1.close(); + return; + } + + try { + carrier.port1.postMessage(null, [closing.port1]); + transfer = "ok"; + } catch (error: any) { + transfer = `${error?.name}:${error?.code ?? ""}`; + } +}); + +closing.port1.on("close", () => { + console.log("deliveries:", deliveries); + console.log("transfer:", transfer); + console.log("carrier queue:", receiveMessageOnPort(carrier.port2)); + closing.port2.close(); + carrier.port1.close(); + carrier.port2.close(); +}); + +closing.port2.postMessage("first"); +closing.port2.postMessage("second"); diff --git a/test-parity/node-suite/worker_threads/message-port/transfer-iterables.ts b/test-parity/node-suite/worker_threads/message-port/transfer-iterables.ts new file mode 100644 index 0000000000..0493ea3701 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/transfer-iterables.ts @@ -0,0 +1,48 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +function outcome(label: string, fn: () => void) { + try { + fn(); + console.log(label, "ok"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? "", error?.message); + } +} + +const { port1, port2 } = new MessageChannel(); +const valid = new ArrayBuffer(4); +new Uint8Array(valid)[0] = 7; +port1.postMessage( + { valid }, + { transfer: new Set([valid]) } as any, +); +const packet = receiveMessageOnPort(port2); +const received = packet ? packet.message?.valid : undefined; +console.log( + "valid set:", + valid.byteLength, + received instanceof ArrayBuffer, + received instanceof ArrayBuffer ? new Uint8Array(received)[0] : "missing", +); + +let iterations = 0; +const throwing = { + *[Symbol.iterator]() { + iterations += 1; + throw new Error("iterator-boom"); + }, +}; +outcome( + "throwing iterable:", + () => port1.postMessage({}, { transfer: throwing } as any), +); +console.log("iterations:", iterations); + +outcome( + "invalid set:", + () => port1.postMessage({}, { transfer: new Set([5]) } as any), +); +outcome("noniterable:", () => port1.postMessage({}, { transfer: 5 } as any)); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/transfer-iterator-error.ts b/test-parity/node-suite/worker_threads/message-port/transfer-iterator-error.ts new file mode 100644 index 0000000000..e7f00f977f --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/transfer-iterator-error.ts @@ -0,0 +1,20 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const expected = new Error("iterator-boom"); + +try { + port1.postMessage({}, { + transfer: { + *[Symbol.iterator]() { + throw expected; + }, + }, + } as any); + console.log("post: ok"); +} catch (error: any) { + console.log("same error:", error === expected, error?.message); +} + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/transfer-iterator-shapes.ts b/test-parity/node-suite/worker_threads/message-port/transfer-iterator-shapes.ts new file mode 100644 index 0000000000..334db96634 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/transfer-iterator-shapes.ts @@ -0,0 +1,29 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); + +function outcome(transfer: any): string { + try { + port1.postMessage("value", { transfer }); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +console.log( + "missing next:", + outcome({ [Symbol.iterator]: () => ({}) }), +); +console.log( + "number next:", + outcome({ [Symbol.iterator]: () => ({ next: 42 }) }), +); +console.log( + "null next:", + outcome({ [Symbol.iterator]: () => ({ next: null }) }), +); +console.log("queue:", receiveMessageOnPort(port2)); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/transfer-optional-forms.ts b/test-parity/node-suite/worker_threads/message-port/transfer-optional-forms.ts new file mode 100644 index 0000000000..9359f3ebd3 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/transfer-optional-forms.ts @@ -0,0 +1,21 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); + +port1.postMessage(1, null as any); +port1.postMessage(2, undefined); +port1.postMessage(3, []); +port1.postMessage(4, {}); +port1.postMessage(5, { transfer: undefined }); +port1.postMessage(6, { transfer: [] }); + +const received: number[] = []; +for (let index = 0; index < 6; index += 1) { + const packet = receiveMessageOnPort(port2); + if (packet) received.push(packet.message); +} +console.log("received:", received.join(",")); +console.log("drained:", receiveMessageOnPort(port2)); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/transfer-options.ts b/test-parity/node-suite/worker_threads/message-port/transfer-options.ts new file mode 100644 index 0000000000..6766e4739a --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/transfer-options.ts @@ -0,0 +1,42 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); + +function make(value: number): ArrayBuffer { + const buffer = new ArrayBuffer(1); + new Uint8Array(buffer)[0] = value; + return buffer; +} + +const arrayBuffer = make(1); +const optionsBuffer = make(2); +const iterableBuffer = make(3); + +port1.postMessage({ buffer: arrayBuffer }, [arrayBuffer]); +port1.postMessage({ buffer: optionsBuffer }, { transfer: [optionsBuffer] }); +port1.postMessage( + { buffer: iterableBuffer }, + (function* () { yield iterableBuffer; })(), +); + +console.log( + "detached:", + arrayBuffer.byteLength, + optionsBuffer.byteLength, + iterableBuffer.byteLength, +); + +const values: string[] = []; +for (let index = 0; index < 3; index += 1) { + const packet = receiveMessageOnPort(port2); + const buffer = packet?.message?.buffer; + values.push( + buffer instanceof ArrayBuffer + ? `${buffer.byteLength}:${new Uint8Array(buffer)[0]}` + : "not-buffer", + ); +} +console.log("received:", values.join(",")); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/transfer-state-validation.ts b/test-parity/node-suite/worker_threads/message-port/transfer-state-validation.ts new file mode 100644 index 0000000000..8de4d761a6 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/transfer-state-validation.ts @@ -0,0 +1,30 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +function outcome(fn: () => void): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const carrier = new MessageChannel(); +console.log( + "source transfer:", + outcome(() => carrier.port1.postMessage(null, [carrier.port1])), +); +carrier.port1.postMessage("still-open"); +const retained = receiveMessageOnPort(carrier.port2); +console.log("source retained:", retained ? retained.message : undefined); + +const closed = new MessageChannel(); +closed.port1.close(); +console.log( + "closed transfer:", + outcome(() => carrier.port1.postMessage(null, [closed.port1])), +); + +carrier.port1.close(); +carrier.port2.close(); +closed.port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/transfer-target.ts b/test-parity/node-suite/worker_threads/message-port/transfer-target.ts new file mode 100644 index 0000000000..80906ceb22 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/transfer-target.ts @@ -0,0 +1,17 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const buffer = new ArrayBuffer(8); +new Uint8Array(buffer)[0] = 5; + +try { + port2.postMessage({ port: port1, buffer }, [port1, buffer]); + console.log("post: ok"); +} catch (error: any) { + console.log("post:", error?.name, error?.code ?? ""); +} + +console.log("buffer detached:", buffer.byteLength); +console.log("target receives:", receiveMessageOnPort(port2)); +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/unread-transferred-port-close.ts b/test-parity/node-suite/worker_threads/message-port/unread-transferred-port-close.ts new file mode 100644 index 0000000000..ca57aeab97 --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/unread-transferred-port-close.ts @@ -0,0 +1,14 @@ +import { MessageChannel } from "node:worker_threads"; + +const carrier = new MessageChannel(); +const movable = new MessageChannel(); + +movable.port2.ref(); +movable.port2.on("close", () => { + console.log("peer closed"); + movable.port2.close(); + carrier.port1.close(); +}); + +carrier.port1.postMessage(movable.port1, [movable.port1]); +carrier.port2.close(); diff --git a/test-parity/node-suite/worker_threads/message-port/unref-delivery.ts b/test-parity/node-suite/worker_threads/message-port/unref-delivery.ts new file mode 100644 index 0000000000..7277e9b24c --- /dev/null +++ b/test-parity/node-suite/worker_threads/message-port/unref-delivery.ts @@ -0,0 +1,20 @@ +import { MessageChannel } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const events: string[] = []; + +port1.on("message", (value) => { + events.push(String(value)); + if (events.length === 3) { + console.log("events:", events.join(",")); + console.log("refs:", port1.hasRef(), port2.hasRef()); + port1.close(); + port2.close(); + } +}); +port1.unref(); +port2.ref(); + +port2.postMessage("a"); +port2.postMessage("b"); +port2.postMessage("c"); diff --git a/test-parity/node-suite/worker_threads/structured-clone/arraybuffer-clone.ts b/test-parity/node-suite/worker_threads/structured-clone/arraybuffer-clone.ts new file mode 100644 index 0000000000..fb561bd4b4 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/arraybuffer-clone.ts @@ -0,0 +1,23 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const buffer = new ArrayBuffer(8); +const view = new Uint16Array(buffer); +view.set([0x1234, 0x5678]); + +port1.postMessage({ buffer, view }); +view[0] = 0xffff; + +const packet = receiveMessageOnPort(port2); +const received = packet?.message; +console.log( + "clone shape:", + received?.buffer instanceof ArrayBuffer, + received?.view instanceof Uint16Array, + received?.view?.buffer === received?.buffer, +); +console.log("clone values:", received?.view?.[0], received?.view?.[1]); +console.log("source retained:", buffer.byteLength, view[0]); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/arraybuffer-transfer.ts b/test-parity/node-suite/worker_threads/structured-clone/arraybuffer-transfer.ts new file mode 100644 index 0000000000..4b213abda2 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/arraybuffer-transfer.ts @@ -0,0 +1,23 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const buffer = new ArrayBuffer(8); +const view = new Uint32Array(buffer); +view.set([0x12345678, 0x90abcdef]); + +port1.postMessage(view, [buffer]); +console.log("detached:", buffer.byteLength, view.byteLength, view.length); + +const packet = receiveMessageOnPort(port2); +const received = packet ? packet.message : undefined; +console.log( + "received:", + received instanceof Uint32Array, + received?.byteLength, + received?.length, + received?.[0]?.toString(16), + received?.[1]?.toString(16), +); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/builtin-types.ts b/test-parity/node-suite/worker_threads/structured-clone/builtin-types.ts new file mode 100644 index 0000000000..aba981cf7b --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/builtin-types.ts @@ -0,0 +1,43 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +let postStatus = "ok"; + +try { + port1.postMessage({ + date: new Date("2020-01-02T03:04:05.000Z"), + regexp: /worker/gi, + map: new Map([["answer", 42]]), + set: new Set([3, 1, 4]), + bigint: 9007199254740993n, + error: new TypeError("clone-me"), + }); +} catch (error: any) { + postStatus = `${error?.name}:${error?.code ?? ""}`; +} + +const packet = receiveMessageOnPort(port2); +const value = packet ? packet.message : undefined; +console.log("post:", postStatus); +console.log( + "brands:", + value?.date instanceof Date, + value?.regexp instanceof RegExp, + value?.map instanceof Map, + value?.set instanceof Set, + typeof value?.bigint, + value?.error instanceof TypeError, +); +console.log( + "values:", + value?.date?.toISOString?.(), + value?.regexp?.source, + value?.regexp?.flags, + value?.map?.get?.("answer"), + value?.set?.has?.(4), + String(value?.bigint), + value?.error?.message, +); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/cycles-and-aliases.ts b/test-parity/node-suite/worker_threads/structured-clone/cycles-and-aliases.ts new file mode 100644 index 0000000000..ba4c8d9f79 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/cycles-and-aliases.ts @@ -0,0 +1,27 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const shared = { label: "shared" }; +const source: any = { left: shared, right: shared }; +source.self = source; + +let postStatus = "ok"; +try { + port1.postMessage(source); +} catch (error: any) { + postStatus = `${error?.name}:${error?.code ?? ""}`; +} + +const packet = receiveMessageOnPort(port2); +const value = packet ? packet.message : undefined; +console.log("post:", postStatus); +console.log( + "identity:", + value?.self === value, + value?.left === value?.right, + value?.left === shared, + value?.left?.label, +); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/detached-retransfer.ts b/test-parity/node-suite/worker_threads/structured-clone/detached-retransfer.ts new file mode 100644 index 0000000000..2e009c3e94 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/detached-retransfer.ts @@ -0,0 +1,27 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const buffer = new ArrayBuffer(8); +const view = new Uint32Array(buffer); +view[0] = 0x12345678; + +port1.postMessage(view, [buffer]); +console.log("detached:", buffer.byteLength, view.byteLength); + +try { + port1.postMessage(view, [buffer]); + console.log("retransfer: ok"); +} catch (error: any) { + console.log("retransfer:", error?.name, error?.code ?? ""); +} + +const first = receiveMessageOnPort(port2); +console.log( + "first:", + first?.message instanceof Uint32Array, + first?.message?.[0], +); +console.log("second:", receiveMessageOnPort(port2)); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/filehandle-transfer.ts b/test-parity/node-suite/worker_threads/structured-clone/filehandle-transfer.ts new file mode 100644 index 0000000000..24bf9be470 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/filehandle-transfer.ts @@ -0,0 +1,75 @@ +import { open } from "node:fs/promises"; +import { MessageChannel } from "node:worker_threads"; + +const fixture = + "test-parity/node-suite/worker_threads/worker-lifecycle/process-env-option-worker.cjs"; + +async function main() { + const handle = await open(fixture, "r"); + const clone = new MessageChannel(); + + try { + try { + clone.port1.postMessage(handle); + console.log("clone: accepted", handle.fd >= 0); + } catch (error: any) { + console.log( + "clone:", + error?.name, + error?.code ?? "", + handle.fd >= 0, + ); + } finally { + clone.port1.close(); + clone.port2.close(); + } + + const transfer = new MessageChannel(); + const received = new Promise((resolve) => { + transfer.port2.on("message", async (value: any) => { + console.log( + "received:", + value?.constructor?.name, + typeof value?.fd === "number" && value.fd >= 0, + ); + try { + const text = await value.readFile({ encoding: "utf8" }); + console.log("received read:", text.startsWith("const { parentPort")); + } catch (error: any) { + console.log("received read:", error?.name, error?.code ?? ""); + } + await value?.close?.().catch?.(() => {}); + try { + await handle.readFile(); + console.log("parent read: ok"); + } catch (error: any) { + console.log("parent read:", error?.name, error?.code ?? ""); + } + transfer.port1.close(); + transfer.port2.close(); + resolve(); + }); + }); + + try { + transfer.port1.postMessage(handle, [handle as any]); + console.log("parent detached:", handle.fd === -1); + await received; + } catch (error: any) { + console.log( + "transfer:", + error?.name, + error?.code ?? "", + handle.fd === -1, + ); + transfer.port1.close(); + transfer.port2.close(); + } + } finally { + await handle.close().catch(() => {}); + } +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/structured-clone/message-port-transfer.ts b/test-parity/node-suite/worker_threads/structured-clone/message-port-transfer.ts new file mode 100644 index 0000000000..d7796d5e11 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/message-port-transfer.ts @@ -0,0 +1,37 @@ +import { + MessageChannel, + MessagePort, + receiveMessageOnPort, +} from "node:worker_threads"; + +const carrier = new MessageChannel(); +const movable = new MessageChannel(); + +carrier.port1.postMessage({ port: movable.port1 }, [movable.port1]); +const packet = receiveMessageOnPort(carrier.port2); +const transferred = packet ? packet.message?.port : undefined; +console.log("transferred type:", transferred instanceof MessagePort); + +if (typeof transferred?.postMessage === "function") { + transferred.postMessage("new-owner"); +} +const newOwnerPacket = receiveMessageOnPort(movable.port2); +console.log( + "new owner delivery:", + newOwnerPacket ? newOwnerPacket.message : undefined, +); + +movable.port1.postMessage("old-owner"); +const oldOwnerPacket = receiveMessageOnPort(movable.port2); +console.log( + "old owner detached:", + oldOwnerPacket ? oldOwnerPacket.message : undefined, +); + +if (typeof transferred?.close === "function") { + transferred.close(); +} +movable.port1.close(); +movable.port2.close(); +carrier.port1.close(); +carrier.port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/multiple-shared-arraybuffers.ts b/test-parity/node-suite/worker_threads/structured-clone/multiple-shared-arraybuffers.ts new file mode 100644 index 0000000000..6168e9a630 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/multiple-shared-arraybuffers.ts @@ -0,0 +1,30 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const value = [ + [new SharedArrayBuffer(0), new SharedArrayBuffer(1)], + [new SharedArrayBuffer(2), new SharedArrayBuffer(3)], +]; +const { port1, port2 } = new MessageChannel(); + +port1.postMessage(value); +const packet = receiveMessageOnPort(port2); +const received = packet?.message; +const flattened = Array.isArray(received) ? received.flat() : []; + +console.log( + "brands:", + flattened.map((item: any) => item instanceof SharedArrayBuffer).join(","), +); +console.log( + "lengths:", + flattened.map((item: any) => item?.byteLength ?? "missing").join(","), +); +console.log( + "shape:", + received?.length, + received?.[0]?.length, + received?.[1]?.length, +); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/multiple-views-transfer.ts b/test-parity/node-suite/worker_threads/structured-clone/multiple-views-transfer.ts new file mode 100644 index 0000000000..503f479a13 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/multiple-views-transfer.ts @@ -0,0 +1,34 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const buffer = new ArrayBuffer(16); +const bytes = new Uint8Array(buffer); +const dataView = new DataView(buffer); +dataView.setFloat64(0, Math.PI); +bytes[8] = 99; + +port1.postMessage({ dataView, bytes }, [buffer]); +let dataViewLength: number | string; +try { + dataViewLength = dataView.byteLength; +} catch (error: any) { + dataViewLength = error?.name; +} +console.log("source detached:", buffer.byteLength, bytes.byteLength, dataViewLength); + +const packet = receiveMessageOnPort(port2); +const value = packet ? packet.message : undefined; +console.log( + "brands/backing:", + value?.dataView instanceof DataView, + value?.bytes instanceof Uint8Array, + value?.dataView?.buffer === value?.bytes?.buffer, +); +console.log( + "values:", + value?.dataView?.getFloat64?.(0), + value?.bytes?.[8], +); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/shared-array-buffer.ts b/test-parity/node-suite/worker_threads/structured-clone/shared-array-buffer.ts new file mode 100644 index 0000000000..950958b2a7 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/shared-array-buffer.ts @@ -0,0 +1,25 @@ +import { MessageChannel, receiveMessageOnPort } from "node:worker_threads"; + +const { port1, port2 } = new MessageChannel(); +const source = new SharedArrayBuffer(8); +const sourceView = new Int32Array(source); +Atomics.store(sourceView, 0, 10); + +port1.postMessage(source); +const packet = receiveMessageOnPort(port2); +const received = packet ? packet.message : undefined; +const receivedView = received instanceof SharedArrayBuffer + ? new Int32Array(received) + : undefined; + +console.log( + "brand/identity:", + received instanceof SharedArrayBuffer, + received === source, + received?.byteLength, +); +Atomics.store(sourceView, 0, 42); +console.log("shared update:", receivedView ? Atomics.load(receivedView, 0) : "missing"); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/transfer-rollback.ts b/test-parity/node-suite/worker_threads/structured-clone/transfer-rollback.ts new file mode 100644 index 0000000000..6e1e435738 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/transfer-rollback.ts @@ -0,0 +1,31 @@ +import { MessageChannel } from "node:worker_threads"; + +function outcome(fn: () => void): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const channel = new MessageChannel(); +const buffer = new ArrayBuffer(8); +console.log( + "invalid after buffer:", + outcome(() => channel.port1.postMessage(buffer, [buffer, null as any])), + buffer.byteLength, +); + +const closed = new MessageChannel(); +closed.port1.close(); +const rollback = new ArrayBuffer(16); +console.log( + "closed port rollback:", + outcome(() => channel.port1.postMessage(null, [rollback, closed.port1])), + rollback.byteLength, +); + +channel.port1.close(); +channel.port2.close(); +closed.port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/transfer-validation.ts b/test-parity/node-suite/worker_threads/structured-clone/transfer-validation.ts new file mode 100644 index 0000000000..79eb2a5012 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/transfer-validation.ts @@ -0,0 +1,37 @@ +import { MessageChannel } from "node:worker_threads"; + +function outcome(fn: () => void): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const carrier = new MessageChannel(); +const extra = new MessageChannel(); + +const duplicate = new ArrayBuffer(8); +console.log( + "duplicate buffer:", + outcome(() => carrier.port1.postMessage(duplicate, [duplicate, duplicate])), + duplicate.byteLength, +); +console.log( + "source port:", + outcome(() => carrier.port1.postMessage(null, [carrier.port1])), +); +console.log( + "missing port transfer:", + outcome(() => carrier.port1.postMessage({ port: extra.port1 })), +); +console.log( + "invalid transfer entry:", + outcome(() => carrier.port1.postMessage("value", [1 as any])), +); + +carrier.port1.close(); +carrier.port2.close(); +extra.port1.close(); +extra.port2.close(); diff --git a/test-parity/node-suite/worker_threads/structured-clone/url-rejection.ts b/test-parity/node-suite/worker_threads/structured-clone/url-rejection.ts new file mode 100644 index 0000000000..ddc45c9345 --- /dev/null +++ b/test-parity/node-suite/worker_threads/structured-clone/url-rejection.ts @@ -0,0 +1,25 @@ +import { + BroadcastChannel, + MessageChannel, + receiveMessageOnPort, +} from "node:worker_threads"; + +function outcome(fn: () => void): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const url = new URL("https://example.org/path?q=value"); +const channel = new MessageChannel(); +console.log("port:", outcome(() => channel.port1.postMessage(url))); +console.log("port queue:", receiveMessageOnPort(channel.port2)); +channel.port1.close(); +channel.port2.close(); + +const broadcast = new BroadcastChannel("url-rejection"); +console.log("broadcast:", outcome(() => broadcast.postMessage(url))); +broadcast.close(); diff --git a/test-parity/node-suite/worker_threads/transfer-markers/marker-privacy.ts b/test-parity/node-suite/worker_threads/transfer-markers/marker-privacy.ts new file mode 100644 index 0000000000..20266e6aa5 --- /dev/null +++ b/test-parity/node-suite/worker_threads/transfer-markers/marker-privacy.ts @@ -0,0 +1,46 @@ +import { + isMarkedAsUntransferable, + markAsUncloneable, + markAsUntransferable, +} from "node:worker_threads"; + +function cloneOutcome(value: any): string { + try { + structuredClone(value); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const marked: any = { value: 1 }; +markAsUncloneable(marked); +console.log( + "private clone mark:", + Object.getOwnPropertySymbols(marked).length, + Reflect.ownKeys(marked).join(","), + cloneOutcome(marked), +); + +delete marked[Symbol.for("nodejs.worker_threads.uncloneable")]; +for (const symbol of Object.getOwnPropertySymbols(marked)) { + delete marked[symbol]; +} +console.log("permanent:", cloneOutcome(marked)); + +const forged: any = { value: 2 }; +forged[Symbol.for("nodejs.worker_threads.uncloneable")] = true; +forged.isUncloneable = true; +console.log("forged clone:", cloneOutcome(forged)); + +const forgedBuffer: any = new ArrayBuffer(4); +forgedBuffer[Symbol.for("nodejs.worker_threads.untransferable")] = true; +console.log("forged transfer mark:", isMarkedAsUntransferable(forgedBuffer)); + +const realBuffer = new ArrayBuffer(4); +markAsUntransferable(realBuffer); +console.log( + "private transfer mark:", + Object.getOwnPropertySymbols(realBuffer).length, + isMarkedAsUntransferable(realBuffer), +); diff --git a/test-parity/node-suite/worker_threads/transfer-markers/marker-semantics.ts b/test-parity/node-suite/worker_threads/transfer-markers/marker-semantics.ts new file mode 100644 index 0000000000..40a7e57525 --- /dev/null +++ b/test-parity/node-suite/worker_threads/transfer-markers/marker-semantics.ts @@ -0,0 +1,35 @@ +import { + isMarkedAsUntransferable, + markAsUncloneable, + markAsUntransferable, +} from "node:worker_threads"; + +for (const value of [undefined, null, false, 0, "value"]) { + console.log( + "primitive:", + typeof value, + markAsUntransferable(value), + isMarkedAsUntransferable(value), + ); +} + +const object = {}; +const array: any[] = []; +console.log("object before:", isMarkedAsUntransferable(object)); +console.log("object mark return:", markAsUntransferable(object)); +markAsUntransferable(array); +console.log( + "objects after:", + isMarkedAsUntransferable(object), + isMarkedAsUntransferable(array), +); + +const prototype = {}; +markAsUntransferable(prototype); +const child = Object.create(prototype); +console.log( + "not inherited:", + isMarkedAsUntransferable(prototype), + isMarkedAsUntransferable(child), +); +console.log("uncloneable return:", markAsUncloneable({})); diff --git a/test-parity/node-suite/worker_threads/transfer-markers/uncloneable-port-transfer.ts b/test-parity/node-suite/worker_threads/transfer-markers/uncloneable-port-transfer.ts new file mode 100644 index 0000000000..7532008808 --- /dev/null +++ b/test-parity/node-suite/worker_threads/transfer-markers/uncloneable-port-transfer.ts @@ -0,0 +1,28 @@ +import { + markAsUncloneable, + MessageChannel, + receiveMessageOnPort, +} from "node:worker_threads"; + +const carrier = new MessageChannel(); +const movable = new MessageChannel(); +markAsUncloneable(movable.port1); + +try { + carrier.port1.postMessage({ port: movable.port1 }, [movable.port1]); + console.log("transfer: ok"); +} catch (error: any) { + console.log("transfer:", error?.name, error?.code ?? ""); +} + +const packet = receiveMessageOnPort(carrier.port2); +console.log("received port:", packet?.message?.port instanceof MessagePort); +if (typeof packet?.message?.port?.postMessage === "function") { + packet.message.port.postMessage("through-transfer"); +} +console.log("peer:", receiveMessageOnPort(movable.port2)?.message); + +packet?.message?.port?.close?.(); +carrier.port1.close(); +carrier.port2.close(); +movable.port2.close(); diff --git a/test-parity/node-suite/worker_threads/transfer-markers/uncloneable-post.ts b/test-parity/node-suite/worker_threads/transfer-markers/uncloneable-post.ts new file mode 100644 index 0000000000..80c5dc3b2e --- /dev/null +++ b/test-parity/node-suite/worker_threads/transfer-markers/uncloneable-post.ts @@ -0,0 +1,41 @@ +import { + markAsUncloneable, + MessageChannel, + receiveMessageOnPort, +} from "node:worker_threads"; + +function outcome(fn: () => void): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const { port1, port2 } = new MessageChannel(); +const root = { value: 1 }; +markAsUncloneable(root); +console.log("root:", outcome(() => port1.postMessage(root))); + +const nested = { value: 2 }; +markAsUncloneable(nested); +console.log( + "nested:", + outcome(() => port1.postMessage({ nested })), +); + +const buffer = new ArrayBuffer(4); +markAsUncloneable(buffer); +console.log("arraybuffer:", outcome(() => port1.postMessage(buffer))); +const packet = receiveMessageOnPort(port2); +const received = packet ? packet.message : undefined; +console.log( + "arraybuffer cloned:", + received instanceof ArrayBuffer, + received?.byteLength, + buffer.byteLength, +); + +port1.close(); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/transfer-markers/untransferable-transfer.ts b/test-parity/node-suite/worker_threads/transfer-markers/untransferable-transfer.ts new file mode 100644 index 0000000000..a23b19867f --- /dev/null +++ b/test-parity/node-suite/worker_threads/transfer-markers/untransferable-transfer.ts @@ -0,0 +1,44 @@ +import { + isMarkedAsUntransferable, + markAsUntransferable, + MessageChannel, + receiveMessageOnPort, +} from "node:worker_threads"; + +function outcome(fn: () => void): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const carrier = new MessageChannel(); +const buffer = new ArrayBuffer(8); +markAsUntransferable(buffer); +console.log( + "buffer transfer:", + isMarkedAsUntransferable(buffer), + outcome(() => carrier.port1.postMessage(buffer, [buffer])), + buffer.byteLength, +); + +const movable = new MessageChannel(); +markAsUntransferable(movable.port1); +console.log( + "port transfer:", + isMarkedAsUntransferable(movable.port1), + outcome(() => carrier.port1.postMessage(movable.port1, [movable.port1])), +); +movable.port1.postMessage("still-owned"); +const retainedPacket = receiveMessageOnPort(movable.port2); +console.log( + "port retained:", + retainedPacket ? retainedPacket.message : undefined, +); + +carrier.port1.close(); +carrier.port2.close(); +movable.port1.close(); +movable.port2.close(); diff --git a/test-parity/node-suite/worker_threads/web-locks/web-locks-abort.ts b/test-parity/node-suite/worker_threads/web-locks/web-locks-abort.ts new file mode 100644 index 0000000000..c0a08029ae --- /dev/null +++ b/test-parity/node-suite/worker_threads/web-locks/web-locks-abort.ts @@ -0,0 +1,22 @@ +import { locks } from "node:worker_threads"; + +async function main() { + const controller = new AbortController(); + controller.abort(new Error("pre-aborted")); + let callbackCalled = false; + + try { + await locks.request("aborted-lock", { signal: controller.signal }, () => { + callbackCalled = true; + return "unexpected"; + }); + console.log("pre-aborted: resolved", callbackCalled); + } catch (error: any) { + console.log("pre-aborted:", error?.name, error?.message, callbackCalled); + } + + const snapshot = await locks.query(); + console.log("snapshot:", snapshot.held.length, snapshot.pending.length); +} + +main().catch((error) => console.log("unexpected:", error?.name, error?.message)); diff --git a/test-parity/node-suite/worker_threads/web-locks/web-locks-callback-settlement.ts b/test-parity/node-suite/worker_threads/web-locks/web-locks-callback-settlement.ts new file mode 100644 index 0000000000..a2d5d5a2cc --- /dev/null +++ b/test-parity/node-suite/worker_threads/web-locks/web-locks-callback-settlement.ts @@ -0,0 +1,35 @@ +import { locks } from "node:worker_threads"; + +async function main() { + console.log("sync:", await locks.request("sync", () => 42)); + console.log( + "promise:", + await locks.request("promise", async () => "resolved"), + ); + + try { + await locks.request("throw", () => { + throw new TypeError("callback-failure"); + }); + console.log("throw: resolved"); + } catch (error: any) { + console.log("throw:", error?.name, error?.message); + } + + try { + await locks.request( + "reject", + () => Promise.reject(new RangeError("rejected")), + ); + console.log("reject: resolved"); + } catch (error: any) { + console.log("reject:", error?.name, error?.message); + } + + const snapshot = await locks.query(); + console.log("cleanup:", snapshot.held.length, snapshot.pending.length); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/web-locks/web-locks-independent-names.ts b/test-parity/node-suite/worker_threads/web-locks/web-locks-independent-names.ts new file mode 100644 index 0000000000..1cc529aae7 --- /dev/null +++ b/test-parity/node-suite/worker_threads/web-locks/web-locks-independent-names.ts @@ -0,0 +1,28 @@ +import { locks } from "node:worker_threads"; + +async function main() { + const events: string[] = []; + let releaseA: () => void = () => {}; + + const a = locks.request("lock-a", () => { + events.push("a-held"); + return new Promise((resolve) => { + releaseA = resolve; + }); + }); + + const b = locks.request("lock-b", () => { + events.push("b-held"); + return "b-result"; + }); + + console.log("b result:", await b); + console.log("before release:", events.join(",")); + releaseA(); + await a; + console.log("after release:", events.join(",")); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/web-locks/web-locks-steal.ts b/test-parity/node-suite/worker_threads/web-locks/web-locks-steal.ts new file mode 100644 index 0000000000..89a0ed13ed --- /dev/null +++ b/test-parity/node-suite/worker_threads/web-locks/web-locks-steal.ts @@ -0,0 +1,32 @@ +import { locks } from "node:worker_threads"; + +async function main() { + let release: () => void = () => {}; + const events: string[] = []; + const held = locks.request("steal-lock", () => { + events.push("held"); + return new Promise((resolve) => { + release = resolve; + }); + }); + + const stolen = locks.request("steal-lock", { steal: true }, () => { + events.push("stolen"); + return "stolen-result"; + }); + + try { + await held; + events.push("held-resolved"); + } catch (error: any) { + events.push(`held-${error?.name}`); + } + + console.log("stolen result:", await stolen); + release(); + console.log("events:", events.join(",")); + const snapshot = await locks.query(); + console.log("final:", snapshot.held.length, snapshot.pending.length); +} + +main().catch((error) => console.log("unexpected:", error?.name, error?.message)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/argv-base-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/argv-base-worker.cjs new file mode 100644 index 0000000000..83dd7c2d9f --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/argv-base-worker.cjs @@ -0,0 +1,7 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.postMessage({ + exec: process.argv[0] === process.execPath, + script: process.argv[1]?.endsWith("argv-base-worker.cjs"), + tail: process.argv.slice(2), +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/argv-base.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/argv-base.ts new file mode 100644 index 0000000000..a86ff10202 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/argv-base.ts @@ -0,0 +1,50 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +async function run(label: string, worker: Worker) { + let message: any; + let error: any; + worker.once("message", (value) => { + message = value; + }); + worker.once("error", (value) => { + error = value; + }); + const code = await new Promise((resolve) => + worker.once("exit", resolve) + ); + console.log( + label, + message?.exec, + message?.script, + JSON.stringify(message?.tail), + error?.name ?? "no-error", + code, + ); +} + +async function main() { + await run( + "file:", + new Worker("./argv-base-worker.cjs", { argv: [null, 42] as any }), + ); + await run( + "eval:", + new Worker( + ` + const { parentPort } = require("node:worker_threads"); + parentPort.postMessage({ + exec: process.argv[0] === process.execPath, + script: process.argv[1] === "[worker eval]", + tail: process.argv.slice(2), + }); + `, + { eval: true, argv: [null, 42] as any }, + ), + ); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/argv-options.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/argv-options.ts new file mode 100644 index 0000000000..44c14ded41 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/argv-options.ts @@ -0,0 +1,22 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +function start(label: string, argv: any) { + try { + const worker = new Worker("./argv-worker.cjs", { argv }); + worker.on("message", (message) => console.log(label, message)); + worker.on( + "error", + (error: any) => + console.log(label, "error", error?.name, error?.code ?? ""), + ); + worker.on("exit", (code) => console.log(label, "exit", code)); + } catch (error: any) { + console.log(label, "throw", error?.name, error?.code ?? ""); + } +} + +start("coerced:", [null, "text", 123, true]); +start("invalid option:", "text"); +start("invalid entry:", [Symbol("value")]); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/argv-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/argv-worker.cjs new file mode 100644 index 0000000000..4233a6ed87 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/argv-worker.cjs @@ -0,0 +1,3 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.postMessage(JSON.stringify(process.argv.slice(2))); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/async-dispose.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/async-dispose.ts new file mode 100644 index 0000000000..d21ccd828c --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/async-dispose.ts @@ -0,0 +1,29 @@ +import { Worker } from "node:worker_threads"; + +const worker = new Worker( + ` + const { parentPort } = require("node:worker_threads"); + parentPort.on("message", () => {}); + parentPort.postMessage("ready"); +`, + { eval: true }, +); + +worker.once("message", async () => { + const key = (Symbol as any).asyncDispose; + const dispose = (worker as any)[key]; + console.log("method:", typeof dispose); + + if (typeof dispose !== "function") { + console.log("fallback terminate:", await worker.terminate()); + return; + } + + let exitCode: number | "pending" = "pending"; + worker.once("exit", (code) => { + exitCode = code; + }); + + const result = await Reflect.apply(dispose, worker, []); + console.log("settled:", result, exitCode, worker.threadId); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/builtin-data-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/builtin-data-worker.cjs new file mode 100644 index 0000000000..3cbb465268 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/builtin-data-worker.cjs @@ -0,0 +1,17 @@ +const { parentPort, workerData } = require("node:worker_threads"); +const data = workerData || {}; + +parentPort.postMessage({ + date: data.date instanceof Date, + dateValue: data.date instanceof Date ? data.date.toISOString() : undefined, + map: data.map instanceof Map, + mapValue: data.map instanceof Map ? data.map.get("key") : undefined, + set: data.set instanceof Set, + setValue: data.set instanceof Set ? data.set.has(3) : undefined, + regexp: data.regexp instanceof RegExp, + regexpValue: data.regexp instanceof RegExp ? data.regexp.source : undefined, + bigintType: typeof data.bigint, + bigintValue: String(data.bigint), + error: data.error instanceof TypeError, + errorValue: data.error?.message, +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/captured-output-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/captured-output-worker.cjs new file mode 100644 index 0000000000..2f2ab8b54e --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/captured-output-worker.cjs @@ -0,0 +1,5 @@ +const { parentPort } = require("node:worker_threads"); + +process.stdout.write("worker-stdout\n"); +process.stderr.write("worker-stderr\n"); +parentPort.postMessage("written"); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/captured-output.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/captured-output.ts new file mode 100644 index 0000000000..cd9a3fd111 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/captured-output.ts @@ -0,0 +1,37 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +async function main() { + const worker = new Worker("./captured-output-worker.cjs", { + stdout: true, + stderr: true, + }); + let stdout = ""; + let stderr = ""; + let message = "missing"; + + worker.stdout.setEncoding("utf8"); + worker.stderr.setEncoding("utf8"); + worker.stdout.on("data", (chunk) => stdout += chunk); + worker.stderr.on("data", (chunk) => stderr += chunk); + worker.on("message", (value) => message = value); + + const exit = new Promise((resolve) => worker.on("exit", resolve)); + const stdoutEnd = new Promise((resolve) => + worker.stdout.on("end", resolve) + ); + const stderrEnd = new Promise((resolve) => + worker.stderr.on("end", resolve) + ); + const [code] = await Promise.all([exit, stdoutEnd, stderrEnd]); + + console.log("message:", message); + console.log("stdout:", stdout.trim()); + console.log("stderr:", stderr.trim()); + console.log("exit:", code); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/constructor-rollback.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/constructor-rollback.ts new file mode 100644 index 0000000000..924e9f06cd --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/constructor-rollback.ts @@ -0,0 +1,56 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +function values(buffer: ArrayBuffer): string { + try { + return Array.from(new Uint8Array(buffer)).join(","); + } catch { + return "detached"; + } +} + +function probe( + label: string, + create: (buffer: ArrayBuffer) => Worker, +) { + const buffer = new Uint8Array([9, 8, 7]).buffer; + let status: string; + let worker: Worker | undefined; + + try { + worker = create(buffer); + status = "created"; + } catch (error: any) { + status = `${error?.name}:${error?.code ?? ""}`; + } + + console.log(label, status, buffer.byteLength, values(buffer)); + worker?.terminate().catch(() => {}); +} + +probe( + "name:", + (buffer) => + new Worker("./constructor-validation-worker.cjs", { + name: {} as any, + workerData: buffer, + transferList: [buffer], + }), +); +probe( + "invalid list:", + (buffer) => + new Worker("./constructor-validation-worker.cjs", { + workerData: buffer, + transferList: [buffer, null as any], + }), +); +probe( + "clone rollback:", + (buffer) => + new Worker("./constructor-validation-worker.cjs", { + workerData: { buffer, uncloneable: () => {} }, + transferList: [buffer], + }), +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/constructor-validation-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/constructor-validation-worker.cjs new file mode 100644 index 0000000000..cb123cde62 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/constructor-validation-worker.cjs @@ -0,0 +1 @@ +// Intentionally empty: invalid constructor inputs must fail before this runs. diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/constructor-validation.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/constructor-validation.ts new file mode 100644 index 0000000000..17b0e66abf --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/constructor-validation.ts @@ -0,0 +1,53 @@ +import { MessageChannel, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +async function outcome(create: () => Worker): Promise { + try { + const worker = create(); + await worker.terminate(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +async function main() { + console.log( + "uncloneable workerData:", + await outcome(() => new Worker("./constructor-validation-worker.cjs", { + workerData: () => {}, + })), + ); + + const duplicate = new ArrayBuffer(8); + console.log( + "duplicate transfer:", + await outcome(() => new Worker("./constructor-validation-worker.cjs", { + workerData: duplicate, + transferList: [duplicate, duplicate], + })), + duplicate.byteLength, + ); + + console.log( + "invalid transfer:", + await outcome(() => new Worker("./constructor-validation-worker.cjs", { + transferList: [null as any], + })), + ); + + const channel = new MessageChannel(); + console.log( + "missing port transfer:", + await outcome(() => new Worker("./constructor-validation-worker.cjs", { + workerData: channel.port1, + })), + ); + channel.port1.close(); + channel.port2.close(); +} + +main().catch((error) => { + console.log("unexpected:", error?.name, error?.message); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/cpu-usage-validation.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/cpu-usage-validation.ts new file mode 100644 index 0000000000..18262fc5b9 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/cpu-usage-validation.ts @@ -0,0 +1,33 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +function outcome(fn: () => any): string { + try { + const value = fn(); + value?.catch?.(() => {}); + return `ok:${value?.constructor?.name ?? typeof value}`; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const worker = new Worker("./diagnostics-hold-worker.cjs"); +worker.once("message", async () => { + console.log( + "accepted:", + [undefined, null, Number.NaN].map((value) => + outcome(() => worker.cpuUsage(value as any)) + ).join(","), + ); + console.log( + "rejected:", + [-1, 1.1, {}, [], true, Infinity].map((value) => + outcome(() => worker.cpuUsage(value as any)) + ).join(","), + ); + console.log("terminate:", await worker.terminate()); +}); +worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/data-and-environment.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/data-and-environment.ts new file mode 100644 index 0000000000..04b8b26466 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/data-and-environment.ts @@ -0,0 +1,44 @@ +import { + setEnvironmentData, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const environment = { label: "snapshot", version: 1 }; +setEnvironmentData("suite-environment", environment); + +const buffer = new ArrayBuffer(4); +const view = new Uint8Array(buffer); +view.set([1, 2, 3, 4]); +const events: string[] = []; + +const worker = new Worker("./data-worker.cjs", { + name: "data-worker", + workerData: { buffer, view, nested: { value: "worker-value" } }, + transferList: [buffer], +}); + +environment.version = 2; +console.log("parent detached:", buffer.byteLength, view.byteLength); + +worker.on("online", () => events.push("online")); +worker.on("message", (message: any) => { + events.push("message"); + console.log( + "worker data:", + message?.isMainThread, + message?.threadName, + message?.buffer, + message?.view, + message?.sharedBacking, + message?.values, + message?.nested, + ); + console.log("environment:", message?.environment); +}); +worker.on("exit", (code) => { + events.push("exit"); + console.log("lifecycle:", events.join(","), code); + setEnvironmentData("suite-environment", undefined); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/data-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/data-worker.cjs new file mode 100644 index 0000000000..b4a131811f --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/data-worker.cjs @@ -0,0 +1,25 @@ +const { + getEnvironmentData, + isMainThread, + parentPort, + threadName, + workerData, +} = require("node:worker_threads"); + +const environment = getEnvironmentData("suite-environment"); +const data = workerData || {}; +parentPort.postMessage({ + isMainThread, + threadName, + buffer: data.buffer instanceof ArrayBuffer, + view: data.view instanceof Uint8Array, + sharedBacking: + data.view instanceof Uint8Array && + data.buffer instanceof ArrayBuffer && + data.view.buffer === data.buffer, + values: typeof data.view?.join === "function" + ? data.view.join(",") + : "not-typed", + nested: data.nested?.value, + environment: `${environment?.label}:${environment?.version}`, +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/default-data-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/default-data-worker.cjs new file mode 100644 index 0000000000..3ba38f5833 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/default-data-worker.cjs @@ -0,0 +1,8 @@ +const { isMainThread, parentPort, workerData } = require("node:worker_threads"); + +parentPort.postMessage({ + isMainThread, + type: typeof workerData, + isNull: workerData === null, + value: workerData?.value, +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/diagnostics-hold-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/diagnostics-hold-worker.cjs new file mode 100644 index 0000000000..86dee53f2c --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/diagnostics-hold-worker.cjs @@ -0,0 +1,4 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.on("message", () => {}); +parentPort.postMessage("ready"); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/env-isolation-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/env-isolation-worker.cjs new file mode 100644 index 0000000000..34baa6ad27 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/env-isolation-worker.cjs @@ -0,0 +1,11 @@ +const { parentPort } = require("node:worker_threads"); + +const inherited = process.env.PERRY_ISOLATED_PARENT; +process.env.PERRY_ISOLATED_PARENT = "worker-change"; +process.env.PERRY_ISOLATED_WORKER = "worker-value"; + +parentPort.postMessage({ + inherited, + changed: process.env.PERRY_ISOLATED_PARENT, + worker: process.env.PERRY_ISOLATED_WORKER, +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/env-isolation.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/env-isolation.ts new file mode 100644 index 0000000000..f574ac84dd --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/env-isolation.ts @@ -0,0 +1,38 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const originalParent = process.env.PERRY_ISOLATED_PARENT; +const originalWorker = process.env.PERRY_ISOLATED_WORKER; +process.env.PERRY_ISOLATED_PARENT = "parent-value"; +delete process.env.PERRY_ISOLATED_WORKER; + +let observed: any; +let workerError: any; +const worker = new Worker("./env-isolation-worker.cjs"); +worker.once("message", (message) => { + observed = message; +}); +worker.once("error", (error) => { + workerError = error; +}); +worker.once("exit", (code) => { + console.log( + "worker:", + observed?.inherited, + observed?.changed, + observed?.worker, + workerError?.name ?? "no-error", + ); + console.log( + "parent:", + process.env.PERRY_ISOLATED_PARENT, + process.env.PERRY_ISOLATED_WORKER, + code, + ); + + if (originalParent === undefined) delete process.env.PERRY_ISOLATED_PARENT; + else process.env.PERRY_ISOLATED_PARENT = originalParent; + if (originalWorker === undefined) delete process.env.PERRY_ISOLATED_WORKER; + else process.env.PERRY_ISOLATED_WORKER = originalWorker; +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/env-options.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/env-options.ts new file mode 100644 index 0000000000..c4dc624936 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/env-options.ts @@ -0,0 +1,29 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +process.env.PERRY_PARENT_BEFORE = "before"; +delete process.env.PERRY_PARENT_AFTER; + +const inherited = new Worker("./env-worker.cjs", { workerData: "inherited" }); +process.env.PERRY_PARENT_AFTER = "after"; + +const explicit = new Worker("./env-worker.cjs", { + workerData: "explicit", + env: { PERRY_MANUAL: 42 as any, PERRY_BOOLEAN: true as any }, +}); + +for (const worker of [inherited, explicit]) { + worker.on( + "message", + (message) => console.log("env:", JSON.stringify(message)), + ); + worker.on("exit", (code) => console.log("exit:", code)); +} + +try { + new Worker("./env-worker.cjs", { env: 42 as any }); + console.log("invalid env: ok"); +} catch (error: any) { + console.log("invalid env:", error?.name, error?.code ?? ""); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/env-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/env-worker.cjs new file mode 100644 index 0000000000..0d7d006b77 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/env-worker.cjs @@ -0,0 +1,9 @@ +const { parentPort, workerData } = require("node:worker_threads"); + +parentPort.postMessage({ + label: workerData, + before: process.env.PERRY_PARENT_BEFORE ?? null, + after: process.env.PERRY_PARENT_AFTER ?? null, + manual: process.env.PERRY_MANUAL ?? null, + boolean: process.env.PERRY_BOOLEAN ?? null, +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/error-ordering.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/error-ordering.ts new file mode 100644 index 0000000000..4b65563a4b --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/error-ordering.ts @@ -0,0 +1,16 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const events: string[] = []; +const worker = new Worker("./error-worker.cjs", { name: "error-worker" }); + +worker.on("online", () => events.push("online")); +worker.on("error", (error: any) => { + events.push("error"); + console.log("error:", error?.name, error?.message); +}); +worker.on("exit", (code) => { + events.push("exit"); + console.log("lifecycle:", events.join(","), code); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/error-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/error-worker.cjs new file mode 100644 index 0000000000..7e52d993af --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/error-worker.cjs @@ -0,0 +1 @@ +throw new TypeError("controlled worker failure"); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/eval-basic.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/eval-basic.ts new file mode 100644 index 0000000000..37f8339b13 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/eval-basic.ts @@ -0,0 +1,30 @@ +import { Worker } from "node:worker_threads"; + +const events: string[] = []; +const worker = new Worker( + ` + const { parentPort, workerData } = require("node:worker_threads"); + const path = require("node:path"); + parentPort.postMessage({ + sum: workerData.left + workerData.right, + basename: path.basename("/tmp/value.txt"), + }); + `, + { + eval: true, + workerData: { left: 2, right: 3 }, + name: "eval-parity", + }, +); + +worker.on("online", () => events.push("online")); +worker.on("message", (message) => { + events.push(`message:${message.sum}:${message.basename}`); +}); +worker.on("error", (error: any) => { + events.push(`error:${error?.name}`); +}); +worker.on("exit", (code) => { + events.push(`exit:${code}`); + console.log("events:", events.join(",")); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/eval-error.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/eval-error.ts new file mode 100644 index 0000000000..7e21a141a3 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/eval-error.ts @@ -0,0 +1,18 @@ +import { Worker } from "node:worker_threads"; + +const worker = new Worker( + ` + throw new TypeError("eval-boom"); + `, + { eval: true }, +); + +const events: string[] = []; +worker.on("online", () => events.push("online")); +worker.on("error", (error: any) => { + events.push(`error:${error?.name}:${error?.message}`); +}); +worker.on("exit", (code) => { + events.push(`exit:${code}`); + console.log("events:", events.join(",")); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/eval-stack-getter-error.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/eval-stack-getter-error.ts new file mode 100644 index 0000000000..8c9030b0c7 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/eval-stack-getter-error.ts @@ -0,0 +1,23 @@ +import { Worker } from "node:worker_threads"; + +const worker = new Worker( + ` + Error.prepareStackTrace = (error) => { + if (error.message === "stack-boom") throw new Error("prepare-boom"); + return "custom-stack"; + }; + throw new Error("stack-boom"); + `, + { eval: true }, +); + +let errorSummary = "missing"; +worker.on("error", (error: any) => { + errorSummary = `${error?.name}:${error?.message}:${ + error?.stack === undefined + }`; +}); +worker.on("exit", (code) => { + console.log("error:", errorSummary); + console.log("exit:", code); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/eval-syntax-error.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/eval-syntax-error.ts new file mode 100644 index 0000000000..d1e501fff5 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/eval-syntax-error.ts @@ -0,0 +1,19 @@ +import { Worker } from "node:worker_threads"; + +const source = ["const broken = ", ")"].join(""); +let summary = "missing"; +let worker: Worker | undefined; + +try { + worker = new Worker(source, { eval: true }); + worker.on("message", () => console.log("unexpected message")); + worker.on("error", (error: any) => { + summary = `${error?.constructor === SyntaxError}:${error?.name}`; + }); + worker.on("exit", (code) => { + console.log("error:", summary); + console.log("exit:", code); + }); +} catch (error: any) { + console.log("construct:", error?.name, error?.code ?? ""); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/eval-unserializable-error.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/eval-unserializable-error.ts new file mode 100644 index 0000000000..0060a90d39 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/eval-unserializable-error.ts @@ -0,0 +1,25 @@ +import { Worker } from "node:worker_threads"; + +const worker = new Worker( + ` + const { MessageChannel } = require("node:worker_threads"); + throw new MessageChannel().port1; + `, + { eval: true }, +); + +let summary = "missing"; +worker.on("error", (error: any) => { + summary = [ + typeof error, + error instanceof Error, + Object.getPrototypeOf(error) === null, + Object.prototype.toString.call(error), + typeof error?.name, + typeof error?.message, + ].join(":"); +}); +worker.on("exit", (code) => { + console.log("error:", summary); + console.log("exit:", code); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/eventemitter-listeners.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/eventemitter-listeners.ts new file mode 100644 index 0000000000..069d694bc7 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/eventemitter-listeners.ts @@ -0,0 +1,27 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./eventemitter-worker.cjs"); +const events: string[] = []; + +function removed() { + events.push("removed"); +} + +worker.on("message", removed); +worker.off("message", removed); +worker.once("online", () => events.push("online-once")); +worker.on("online", () => events.push("online-on")); +worker.once("message", (value) => events.push(`once:${value}`)); +worker.on("message", (value) => events.push(`on:${value}`)); +worker.on("exit", (code) => { + events.push(`exit:${code}`); + console.log("events:", events.join(",")); + const count = typeof (worker as any).listenerCount === "function" + ? `${(worker as any).listenerCount("message")},${ + (worker as any).listenerCount("online") + }` + : "unsupported"; + console.log("counts:", count); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/eventemitter-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/eventemitter-worker.cjs new file mode 100644 index 0000000000..97c5e91903 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/eventemitter-worker.cjs @@ -0,0 +1,4 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.postMessage("first"); +parentPort.postMessage("second"); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/execargv-options.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/execargv-options.ts new file mode 100644 index 0000000000..77aadaae6c --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/execargv-options.ts @@ -0,0 +1,43 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +function run(label: string, options: Record): Promise { + return new Promise((resolve) => { + const worker = new Worker("./execargv-worker.cjs", options); + worker.on("message", (message) => console.log(label, message)); + worker.on( + "error", + (error: any) => + console.log(label, "error", error?.name, error?.code ?? ""), + ); + worker.on("exit", (code) => { + console.log(label, "exit", code); + resolve(); + }); + }); +} + +async function main() { + console.log("parent:", JSON.stringify(process.execArgv)); + await run("inherited:", {}); + await run("null:", { execArgv: null }); + await run("zero:", { execArgv: 0 }); + await run("false:", { execArgv: false }); + await run("empty:", { execArgv: [] }); + await run("custom:", { execArgv: ["--no-warnings"] }); + await run("nonstrings:", { execArgv: [1, null, true] as any }); + + try { + new Worker("./execargv-worker.cjs", { + execArgv: ["--not-a-real-node-flag"], + }); + console.log("invalid flag: ok"); + } catch (error: any) { + console.log("invalid flag:", error?.name, error?.code ?? ""); + } +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/execargv-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/execargv-worker.cjs new file mode 100644 index 0000000000..3685a944e2 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/execargv-worker.cjs @@ -0,0 +1,3 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.postMessage(JSON.stringify(process.execArgv)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/exit-code-explicit.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-code-explicit.ts new file mode 100644 index 0000000000..50203e3162 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-code-explicit.ts @@ -0,0 +1,14 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./exit-codes-worker.cjs", { + workerData: "explicit", +}); +const events: string[] = []; +worker.on("message", (message) => events.push(`message:${message}`)); +worker.on("error", (error: any) => events.push(`error:${error?.name}`)); +worker.on("exit", (code) => { + events.push(`exit:${code}`); + console.log("explicit:", events.join(",")); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/exit-code-property.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-code-property.ts new file mode 100644 index 0000000000..40194a2989 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-code-property.ts @@ -0,0 +1,14 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./exit-codes-worker.cjs", { + workerData: "exitCode", +}); +const events: string[] = []; +worker.on("message", (message) => events.push(`message:${message}`)); +worker.on("error", (error: any) => events.push(`error:${error?.name}`)); +worker.on("exit", (code) => { + events.push(`exit:${code}`); + console.log("exitCode:", events.join(",")); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/exit-codes-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-codes-worker.cjs new file mode 100644 index 0000000000..304f5dbb19 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-codes-worker.cjs @@ -0,0 +1,8 @@ +const { parentPort, workerData } = require("node:worker_threads"); + +if (workerData === "explicit") { + process.exit(5); +} else { + process.exitCode = 7; + parentPort.postMessage("set"); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/exit-listener-error.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-listener-error.ts new file mode 100644 index 0000000000..8c8ecc815c --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-listener-error.ts @@ -0,0 +1,14 @@ +import { Worker } from "node:worker_threads"; + +const events: string[] = []; + +process.once("uncaughtException", (error: any) => { + events.push(`uncaught:${error?.message}`); + console.log("events:", events.join(",")); +}); + +const worker = new Worker("", { eval: true }); +worker.on("exit", () => { + events.push("exit"); + throw new Error("exit-listener-boom"); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/exit-unreachable-code.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-unreachable-code.ts new file mode 100644 index 0000000000..383e886246 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-unreachable-code.ts @@ -0,0 +1,17 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const shared = new SharedArrayBuffer(4); +const view = new Int32Array(shared); +const events: string[] = []; +const worker = new Worker("./exit-unreachable-worker.cjs", { + workerData: shared, +}); + +worker.on("message", (value) => events.push(`message:${value}`)); +worker.on("error", (error: any) => events.push(`error:${error?.name}`)); +worker.on("exit", (code) => { + console.log("events:", events.join(",") || "none"); + console.log("exit:", code, Atomics.load(view, 0)); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/exit-unreachable-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-unreachable-worker.cjs new file mode 100644 index 0000000000..ea534bc3fd --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/exit-unreachable-worker.cjs @@ -0,0 +1,6 @@ +const { parentPort, workerData } = require("node:worker_threads"); + +const view = new Int32Array(workerData); +process.exit(4); +view[0] = 99; +parentPort.postMessage("unreachable"); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/file-url-unicode.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/file-url-unicode.ts new file mode 100644 index 0000000000..70371f9e54 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/file-url-unicode.ts @@ -0,0 +1,21 @@ +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const filename = path.resolve("url-Ï€-worker.cjs"); +try { + const worker = new Worker(pathToFileURL(filename), { + workerData: "unicode-ok", + }); + + worker.on("message", (message) => console.log("message:", message)); + worker.on( + "error", + (error: any) => console.log("error:", error?.name, error?.code ?? ""), + ); + worker.on("exit", (code) => console.log("exit:", code)); +} catch (error: any) { + console.log("construct:", error?.name, error?.code ?? ""); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-construction-rollback.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-construction-rollback.ts new file mode 100644 index 0000000000..ea9dc620e6 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-construction-rollback.ts @@ -0,0 +1,55 @@ +import { open } from "node:fs/promises"; +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +async function readable(handle: any): Promise { + try { + const buffer = Buffer.alloc(5); + const result = await handle.read(buffer, 0, buffer.length, 0); + return `${result.bytesRead}:${buffer.toString("utf8")}`; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +async function attempt( + label: string, + workerData: (handle: any) => unknown, + transferList: (handle: any) => any[], +) { + const handle = await open("./process-env-option-worker.cjs", "r"); + let worker: Worker | undefined; + let result = "created"; + + try { + worker = new Worker("./constructor-validation-worker.cjs", { + workerData: workerData(handle), + transferList: transferList(handle), + }); + } catch (error: any) { + result = `${error?.name}:${error?.code ?? ""}`; + } finally { + await worker?.terminate().catch(() => {}); + } + + console.log(label, result, handle.fd >= 0, await readable(handle)); + await handle.close().catch(() => {}); +} + +async function main() { + await attempt( + "clone rollback:", + (handle) => ({ handle, bad: () => {} }), + (handle) => [handle], + ); + await attempt( + "duplicate rollback:", + (handle) => ({ handle }), + (handle) => [handle, handle], + ); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-map-set-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-map-set-worker.cjs new file mode 100644 index 0000000000..75eaf89585 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-map-set-worker.cjs @@ -0,0 +1,24 @@ +const { parentPort, workerData } = require("node:worker_threads"); + +async function main() { + const mapHandle = workerData?.map?.get?.("handle"); + const setHandle = workerData?.set ? Array.from(workerData.set)[0] : undefined; + const result = { + mapBrand: workerData?.map?.constructor?.name, + setBrand: workerData?.set?.constructor?.name, + handleBrand: mapHandle?.constructor?.name, + same: mapHandle === setHandle, + read: "unsupported", + }; + if (typeof mapHandle?.read === "function") { + const buffer = Buffer.alloc(1); + const read = await mapHandle.read(buffer, 0, 1, 0); + result.read = `${read.bytesRead}:${buffer[0]}`; + await mapHandle.close(); + } + parentPort.postMessage(result); +} + +main().catch((error) => { + parentPort.postMessage({ error: `${error?.name}:${error?.code || ""}` }); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-map-set.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-map-set.ts new file mode 100644 index 0000000000..86d82147c7 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-map-set.ts @@ -0,0 +1,55 @@ +import { open } from "node:fs/promises"; +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +async function main() { + const handle = await open("./process-env-option-worker.cjs", "r"); + try { + const worker = new Worker("./filehandle-map-set-worker.cjs", { + workerData: { + map: new Map([["handle", handle]]), + set: new Set([handle]), + }, + transferList: [handle as any], + }); + console.log("construction: ok", handle.fd === -1); + worker.on("message", (value: any) => { + console.log( + "worker:", + value?.mapBrand, + value?.setBrand, + value?.handleBrand, + value?.same, + value?.read, + value?.error ?? "no-error", + ); + }); + worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); + }); + worker.on("exit", async (code) => { + console.log("exit:", code, handle.fd === -1); + await handle.close().catch(() => {}); + }); + } catch (error: any) { + console.log( + "construction:", + error?.name, + error?.code ?? "", + handle.fd === -1, + ); + const buffer = Buffer.alloc(1); + try { + const read = await handle.read(buffer, 0, 1, 0); + console.log("parent readable:", read.bytesRead, buffer[0]); + } catch (readError: any) { + console.log("parent read:", readError?.code ?? readError?.name); + } + await handle.close().catch(() => {}); + } +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-name-validation.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-name-validation.ts new file mode 100644 index 0000000000..c2f09909e3 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-name-validation.ts @@ -0,0 +1,49 @@ +import { open } from "node:fs/promises"; +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +async function main() { + const handle = await open("./process-env-option-worker.cjs", "r"); + try { + const worker = new Worker("./natural-exit-worker.cjs", { + name: {} as any, + workerData: { handle }, + transferList: [handle as any], + }); + console.log("construction: ok", handle.fd === -1); + worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); + }); + worker.on("exit", async (code) => { + console.log("exit:", code, handle.fd === -1); + const buffer = Buffer.alloc(1); + try { + const result = await handle.read(buffer, 0, 1, 0); + console.log("parent readable:", result.bytesRead, buffer[0]); + } catch (readError: any) { + console.log("parent read:", readError?.code ?? readError?.name); + } + await handle.close().catch(() => {}); + }); + } catch (error: any) { + console.log( + "construction:", + error?.name, + error?.code ?? "", + handle.fd === -1, + ); + const buffer = Buffer.alloc(1); + try { + const result = await handle.read(buffer, 0, 1, 0); + console.log("parent readable:", result.bytesRead, buffer[0]); + } catch (readError: any) { + console.log("parent read:", readError?.code ?? readError?.name); + } + await handle.close().catch(() => {}); + } +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-orphan-rollback.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-orphan-rollback.ts new file mode 100644 index 0000000000..cd702f1fe8 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-orphan-rollback.ts @@ -0,0 +1,49 @@ +import { open } from "node:fs/promises"; +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +async function main() { + const handle = await open("./process-env-option-worker.cjs", "r"); + let worker: Worker | undefined; + try { + worker = new Worker("./natural-exit-worker.cjs", { + workerData: () => "not cloneable", + transferList: [handle as any], + }); + console.log("construction: ok", handle.fd === -1); + worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); + }); + worker.on("exit", async (code) => { + console.log("exit:", code, handle.fd === -1); + const buffer = Buffer.alloc(1); + try { + const result = await handle.read(buffer, 0, 1, 0); + console.log("parent readable:", result.bytesRead, buffer[0]); + } catch (readError: any) { + console.log("parent read:", readError?.code ?? readError?.name); + } + await handle.close().catch(() => {}); + }); + } catch (error: any) { + console.log( + "construction:", + error?.name, + error?.code ?? "", + handle.fd === -1, + ); + const buffer = Buffer.alloc(1); + try { + const result = await handle.read(buffer, 0, 1, 0); + console.log("parent readable:", result.bytesRead, buffer[0]); + } catch (readError: any) { + console.log("parent read:", readError?.code ?? readError?.name); + } + await handle.close().catch(() => {}); + } +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-orphan-transfer.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-orphan-transfer.ts new file mode 100644 index 0000000000..8a46cc7fb1 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-orphan-transfer.ts @@ -0,0 +1,49 @@ +import { open } from "node:fs/promises"; +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +async function main() { + const handle = await open("./process-env-option-worker.cjs", "r"); + try { + const worker = new Worker("./natural-exit-worker.cjs", { + workerData: {}, + transferList: [handle as any], + }); + console.log("construction: ok", handle.fd === -1); + worker.on("message", (value) => console.log("message:", value)); + worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); + }); + worker.on("exit", async (code) => { + console.log("exit:", code, handle.fd === -1); + const buffer = Buffer.alloc(1); + try { + const result = await handle.read(buffer, 0, 1, 0); + console.log("parent readable:", result.bytesRead, buffer[0]); + } catch (readError: any) { + console.log("parent read:", readError?.code ?? readError?.name); + } + await handle.close().catch(() => {}); + }); + } catch (error: any) { + console.log( + "construction:", + error?.name, + error?.code ?? "", + handle.fd === -1, + ); + const buffer = Buffer.alloc(1); + try { + const result = await handle.read(buffer, 0, 1, 0); + console.log("parent readable:", result.bytesRead, buffer[0]); + } catch (readError: any) { + console.log("parent read:", readError?.code ?? readError?.name); + } + await handle.close().catch(() => {}); + } +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-workerdata-alias-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-workerdata-alias-worker.cjs new file mode 100644 index 0000000000..bdfeb5e19b --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-workerdata-alias-worker.cjs @@ -0,0 +1,27 @@ +const { parentPort, workerData } = require("node:worker_threads"); + +async function main() { + const fromMap = workerData.map?.get?.("handle"); + const fromSet = workerData.set ? Array.from(workerData.set)[0] : undefined; + const values = [workerData.direct, workerData.alias, fromMap, fromSet]; + let read; + + try { + const buffer = Buffer.alloc(5); + const result = await workerData.direct.read(buffer, 0, buffer.length, 0); + read = `${result.bytesRead}:${buffer.toString("utf8")}`; + } catch (error) { + read = `${error?.name}:${error?.code ?? ""}`; + } + + parentPort.postMessage({ + same: values.every((value) => value === values[0]), + brand: workerData.direct?.constructor?.name, + read, + }); + await workerData.direct?.close?.().catch?.(() => {}); +} + +main().catch((error) => { + parentPort.postMessage({ unexpected: `${error?.name}:${error?.message}` }); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-workerdata-alias.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-workerdata-alias.ts new file mode 100644 index 0000000000..dc6cb16a3b --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/filehandle-workerdata-alias.ts @@ -0,0 +1,46 @@ +import { open } from "node:fs/promises"; +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +async function main() { + const handle = await open("./process-env-option-worker.cjs", "r"); + let worker: Worker; + try { + worker = new Worker("./filehandle-workerdata-alias-worker.cjs", { + workerData: { + direct: handle, + alias: handle, + map: new Map([["handle", handle]]), + set: new Set([handle]), + }, + transferList: [handle as any], + }); + } catch (error: any) { + console.log("construction:", error?.name, error?.code ?? ""); + await handle.close().catch(() => {}); + return; + } + + console.log("parent detached:", handle.fd === -1); + worker.on("message", async (message) => { + console.log("worker:", JSON.stringify(message)); + try { + await handle.readFile(); + console.log("parent read: ok"); + } catch (error: any) { + console.log("parent read:", error?.name, error?.code ?? ""); + } + }); + worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); + }); + worker.on("exit", async (code) => { + console.log("exit:", code); + await handle.close().catch(() => {}); + }); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/filename-validation.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/filename-validation.ts new file mode 100644 index 0000000000..0ff8d83715 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/filename-validation.ts @@ -0,0 +1,30 @@ +import { Worker } from "node:worker_threads"; + +const values: Array<[string, any]> = [ + ["undefined", undefined], + ["null", null], + ["false", false], + ["zero", 0], + ["symbol", Symbol("worker")], + ["object", {}], + ["array", []], + ["function", () => {}], +]; + +async function main() { + for (const [label, value] of values) { + let worker: Worker | undefined; + try { + worker = new Worker(value); + console.log(label, "created"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } finally { + await worker?.terminate().catch(() => {}); + } + } +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/global-postmessage-override.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/global-postmessage-override.ts new file mode 100644 index 0000000000..a2b7320ec0 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/global-postmessage-override.ts @@ -0,0 +1,16 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); +const worker = new Worker("./global-postmessage-worker.cjs"); +worker.on("message", (message) => { + if (message === "ready") { + worker.postMessage("value"); + return; + } + console.log("message:", message); + worker.terminate(); +}); +worker.on("error", (error) => { + console.log("error:", error.name, (error as any).code ?? ""); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/global-postmessage-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/global-postmessage-worker.cjs new file mode 100644 index 0000000000..f4133c55f5 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/global-postmessage-worker.cjs @@ -0,0 +1,12 @@ +const { parentPort } = require("node:worker_threads"); + +Object.assign(globalThis, { + postMessage(message) { + parentPort.postMessage(message); + }, +}); + +parentPort.once("message", (message) => { + globalThis.postMessage(`echo:${message}`); +}); +parentPort.postMessage("ready"); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/inheritance-brands-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/inheritance-brands-worker.cjs new file mode 100644 index 0000000000..d3b2e3897d --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/inheritance-brands-worker.cjs @@ -0,0 +1,6 @@ +const { MessagePort, parentPort } = require("node:worker_threads"); + +parentPort.postMessage({ + eventTarget: parentPort instanceof EventTarget, + messagePort: parentPort instanceof MessagePort, +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/inheritance-brands.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/inheritance-brands.ts new file mode 100644 index 0000000000..4057c1eb89 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/inheritance-brands.ts @@ -0,0 +1,23 @@ +import { MessageChannel, MessagePort, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const { port1, port2 } = new MessageChannel(); +console.log( + "main port:", + port1 instanceof MessagePort, + port1 instanceof EventTarget, +); +port1.close(); +port2.close(); + +const worker = new Worker("./inheritance-brands-worker.cjs"); +console.log("worker:", worker instanceof EventTarget); +worker.on("message", (message: any) => { + console.log( + "parent port:", + message.messagePort, + message.eventTarget, + ); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/internal-thread-flag.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/internal-thread-flag.ts new file mode 100644 index 0000000000..d2e100e513 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/internal-thread-flag.ts @@ -0,0 +1,20 @@ +import * as workerThreads from "node:worker_threads"; +import { isInternalThread, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +console.log( + "main:", + isInternalThread, + workerThreads.isInternalThread, + workerThreads.isMainThread, +); + +const worker = new Worker("./internal-thread-worker.cjs"); +worker.on("message", (value: any) => { + console.log("worker:", value.named, value.namespace, value.main); +}); +worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/internal-thread-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/internal-thread-worker.cjs new file mode 100644 index 0000000000..f203e73ded --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/internal-thread-worker.cjs @@ -0,0 +1,7 @@ +const workerThreads = require("node:worker_threads"); + +workerThreads.parentPort.postMessage({ + named: workerThreads.isInternalThread, + namespace: require("node:worker_threads").isInternalThread, + main: workerThreads.isMainThread, +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/listener-validation.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/listener-validation.ts new file mode 100644 index 0000000000..fe958565cf --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/listener-validation.ts @@ -0,0 +1,25 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./termination-worker.cjs"); + +function outcome(label: string, fn: () => void) { + try { + fn(); + console.log(label, "ok"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} + +outcome("on null:", () => (worker as any).on("message", null)); +outcome("once number:", () => (worker as any).once("message", 1)); +outcome( + "addListener object:", + () => (worker as any).addListener("message", {}), +); +outcome("off undefined:", () => (worker as any).off("message", undefined)); + +worker.terminate().then((code) => console.log("terminate:", code)); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/method-receivers.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/method-receivers.ts new file mode 100644 index 0000000000..c76340b94a --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/method-receivers.ts @@ -0,0 +1,17 @@ +import { Worker } from "node:worker_threads"; + +function check(name: string, method: Function, args: any[] = []) { + for (const receiver of [undefined, null, {}]) { + try { + Reflect.apply(method, receiver, args); + console.log(name, typeof receiver, "ok"); + } catch (error: any) { + console.log(name, typeof receiver, error?.name, error?.code ?? ""); + } + } +} + +check("postMessage", Worker.prototype.postMessage, ["value"]); +check("terminate", Worker.prototype.terminate); +check("ref", Worker.prototype.ref); +check("unref", Worker.prototype.unref); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/multiple-worker-ids.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/multiple-worker-ids.ts new file mode 100644 index 0000000000..030203aa89 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/multiple-worker-ids.ts @@ -0,0 +1,38 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +function observe( + worker: Worker, +): Promise<{ reported: number | undefined; exit: number }> { + return new Promise((resolve) => { + let reported: number | undefined; + worker.on("message", (message) => reported = message.threadId); + worker.on("exit", (exit) => resolve({ reported, exit })); + }); +} + +async function main() { + const first = new Worker("./thread-metadata-worker.cjs", { name: "first" }); + const second = new Worker("./thread-metadata-worker.cjs", { name: "second" }); + const parentIds = [first.threadId, second.threadId]; + const observations = await Promise.all([observe(first), observe(second)]); + + console.log("parent positive:", parentIds.every((id) => id > 0)); + console.log("parent unique:", new Set(parentIds).size === 2); + console.log( + "reported:", + observations.map((item) => item.reported ?? "missing").join(","), + ); + console.log( + "reported match:", + observations[0].reported === parentIds[0], + observations[1].reported === parentIds[1], + ); + console.log("exits:", observations.map((item) => item.exit).join(",")); + console.log("increasing:", parentIds[1] > parentIds[0]); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/natural-exit-terminate.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/natural-exit-terminate.ts new file mode 100644 index 0000000000..885a84feb7 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/natural-exit-terminate.ts @@ -0,0 +1,15 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const events: string[] = []; +const worker = new Worker("./natural-exit-worker.cjs"); +worker.on("online", () => events.push("online")); +worker.on("message", (message) => events.push(`message:${message}`)); +worker.on("exit", (code) => { + events.push(`exit:${code}`); + worker.terminate().then((terminateCode) => { + events.push(`terminate:${String(terminateCode)}`); + console.log("events:", events.join(",")); + }); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/natural-exit-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/natural-exit-worker.cjs new file mode 100644 index 0000000000..e968f3d093 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/natural-exit-worker.cjs @@ -0,0 +1,3 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.postMessage("natural"); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/node-options-env-validation.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/node-options-env-validation.ts new file mode 100644 index 0000000000..6da5dd5368 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/node-options-env-validation.ts @@ -0,0 +1,17 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +try { + const worker = new Worker("./natural-exit-worker.cjs", { + env: { NODE_OPTIONS: "--title=worker" }, + }); + console.log("construct: ok"); + worker.on( + "error", + (error) => console.log("error:", error.name, (error as any).code ?? ""), + ); + worker.on("exit", (code) => console.log("exit:", code)); +} catch (error) { + console.log("construct:", (error as any)?.name, (error as any)?.code ?? ""); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/option-validation-precedence.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/option-validation-precedence.ts new file mode 100644 index 0000000000..9a2d53efd4 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/option-validation-precedence.ts @@ -0,0 +1,52 @@ +import { Worker } from "node:worker_threads"; + +function construct(label: string, options: Record): Promise { + return new Promise((resolve) => { + try { + const worker = new Worker("./internal-thread-worker.cjs", options); + console.log(label, "ok"); + worker.on("error", (error: any) => { + console.log(label, "error", error?.name, error?.code ?? ""); + }); + worker.on("exit", (code) => { + console.log(label, "exit", code); + resolve(); + }); + worker.terminate(); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? "", error?.message); + resolve(); + } + }); +} + +async function main() { + await construct("argv before env:", { + argv: [{ + toString() { + throw new Error("argv-coercion"); + }, + }], + env: 42, + }); + + const envBuffer = new ArrayBuffer(8); + await construct("env before transfer:", { + env: 42, + workerData: envBuffer, + transferList: [envBuffer], + }); + console.log("env ownership:", envBuffer.byteLength); + + const nameBuffer = new ArrayBuffer(8); + await construct("name before transfer:", { + name: {}, + workerData: nameBuffer, + transferList: [nameBuffer], + }); + console.log("name ownership:", nameBuffer.byteLength); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-onmessage-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-onmessage-worker.cjs new file mode 100644 index 0000000000..f6e7dd40d2 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-onmessage-worker.cjs @@ -0,0 +1,13 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.onmessage = (event) => { + parentPort.postMessage({ + type: event.type, + data: event.data * 2, + target: event.target === parentPort, + source: event.source === null, + }); + parentPort.postMessage(undefined); + parentPort.postMessage(null); + parentPort.postMessage(event.ports.length); +}; diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-onmessage.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-onmessage.ts new file mode 100644 index 0000000000..db676a5c92 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-onmessage.ts @@ -0,0 +1,23 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./parent-port-onmessage-worker.cjs"); +const values: string[] = []; +let valuesLogged = false; + +worker.on("message", (message) => { + values.push(message === undefined ? "undefined" : JSON.stringify(message)); + if (values.length === 4) { + console.log("values:", values.join(",")); + valuesLogged = true; + worker.terminate().then((code) => console.log("terminate:", code)); + } +}); +worker.on("exit", (code) => { + if (!valuesLogged) { + console.log("values: missing"); + } + console.log("exit:", code); +}); +worker.postMessage(2); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-ref-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-ref-worker.cjs new file mode 100644 index 0000000000..9594899673 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-ref-worker.cjs @@ -0,0 +1,14 @@ +const { parentPort } = require("node:worker_threads"); + +const hasRef = typeof parentPort.hasRef === "function"; +const initial = hasRef ? parentPort.hasRef() : "unsupported"; +const unrefReturn = typeof parentPort.unref === "function" + ? parentPort.unref() === parentPort + : "unsupported"; +const unrefed = hasRef ? parentPort.hasRef() : "unsupported"; +const refReturn = typeof parentPort.ref === "function" + ? parentPort.ref() === parentPort + : "unsupported"; +const refed = hasRef ? parentPort.hasRef() : "unsupported"; + +parentPort.postMessage({ initial, unrefReturn, unrefed, refReturn, refed }); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-ref.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-ref.ts new file mode 100644 index 0000000000..c3ef628a84 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/parent-port-ref.ts @@ -0,0 +1,17 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./parent-port-ref-worker.cjs"); +worker.on("message", (message) => { + console.log( + "states:", + message.initial, + message.unrefReturn, + message.unrefed, + message.refReturn, + message.refed, + ); + worker.terminate().then((code) => console.log("terminate:", code)); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/post-exit-diagnostics.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/post-exit-diagnostics.ts new file mode 100644 index 0000000000..8b31af7dc2 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/post-exit-diagnostics.ts @@ -0,0 +1,42 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +async function outcome(fn: () => Promise): Promise { + try { + const result = await fn(); + return `ok:${result?.constructor?.name ?? typeof result}`; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +async function main() { + const worker = new Worker("./natural-exit-worker.cjs"); + const code = await new Promise((resolve) => + worker.once("exit", resolve) + ); + try { + const utilization = worker.performance.eventLoopUtilization(); + console.log( + "utilization:", + Object.keys(utilization).sort().join(","), + [ + typeof utilization.idle, + typeof utilization.active, + typeof utilization.utilization, + ].join(","), + utilization.idle === 0 && utilization.active === 0 && + utilization.utilization === 0, + ); + } catch (error: any) { + console.log("utilization:", error?.name, error?.code ?? ""); + } + console.log("heap:", await outcome(() => worker.getHeapStatistics())); + console.log("cpu:", await outcome(() => worker.cpuUsage())); + console.log("exit:", code); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/post-exit-safe-methods.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/post-exit-safe-methods.ts new file mode 100644 index 0000000000..2c2aabebfb --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/post-exit-safe-methods.ts @@ -0,0 +1,30 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./natural-exit-worker.cjs"); +const stdin = worker.stdin; +const stdout = worker.stdout; +const stderr = worker.stderr; + +function outcome(fn: () => any): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +worker.on("exit", (code) => { + console.log("exit:", code, worker.threadId, worker.threadName); + console.log("postMessage:", outcome(() => worker.postMessage("after-exit"))); + console.log("ref:", outcome(() => worker.ref())); + console.log("unref:", outcome(() => worker.unref())); + console.log( + "stream identity:", + worker.stdin === stdin, + worker.stdout === stdout, + worker.stderr === stderr, + ); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/post-message-transfer.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/post-message-transfer.ts new file mode 100644 index 0000000000..155de50daf --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/post-message-transfer.ts @@ -0,0 +1,18 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./post-transfer-worker.cjs"); +const buffer = new ArrayBuffer(4); +const view = new Uint8Array(buffer); +view.set([2, 4, 6, 8]); + +worker.on("online", () => { + worker.postMessage({ view }, [buffer]); + console.log("source detached:", buffer.byteLength, view.byteLength); +}); +worker.on("message", (message: any) => { + console.log("worker received:", message?.brand, message?.length, message?.values); + worker.terminate(); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/post-transfer-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/post-transfer-worker.cjs new file mode 100644 index 0000000000..c857536255 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/post-transfer-worker.cjs @@ -0,0 +1,10 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.on("message", (message) => { + const view = message?.view; + parentPort.postMessage({ + brand: view instanceof Uint8Array, + length: view?.length, + values: typeof view?.join === "function" ? view.join(",") : "not-typed", + }); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-errors-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-errors-worker.cjs new file mode 100644 index 0000000000..2eddaa1e7a --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-errors-worker.cjs @@ -0,0 +1,7 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.once("message", (message) => { + if (message === "finish") { + parentPort.postMessage("finished"); + } +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-errors.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-errors.ts new file mode 100644 index 0000000000..108e23df10 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-errors.ts @@ -0,0 +1,30 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./postmessage-errors-worker.cjs"); + +function outcome(label: string, value: any, transfer?: readonly any[]) { + try { + worker.postMessage(value, transfer as any); + console.log(label, "ok"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } +} + +worker.on("online", () => { + outcome("function:", () => 1); + outcome("symbol:", Symbol("value")); + + const buffer = new ArrayBuffer(8); + outcome("rollback:", { buffer, invalid() {} }, [buffer]); + console.log("buffer retained:", buffer.byteLength); + + worker.postMessage("finish"); +}); +worker.on("message", (message) => { + console.log("message:", message); + worker.terminate().then((code) => console.log("terminate:", code)); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-missing.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-missing.ts new file mode 100644 index 0000000000..59927f8833 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-missing.ts @@ -0,0 +1,19 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./postmessage-errors-worker.cjs"); +worker.on("online", () => { + try { + (worker.postMessage as any)(); + console.log("post: ok"); + } catch (error: any) { + console.log("post:", error?.name, error?.code ?? ""); + } + worker.postMessage("finish"); +}); +worker.on("message", (message) => { + console.log("message:", message); + worker.terminate().then((code) => console.log("terminate:", code)); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-shared-array-buffer-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-shared-array-buffer-worker.cjs new file mode 100644 index 0000000000..6d6a7329cb --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-shared-array-buffer-worker.cjs @@ -0,0 +1,12 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.once("message", ({ shared }) => { + const view = new Uint8Array(shared); + const before = Array.from(view).join(","); + view[1] = 8; + parentPort.postMessage({ + brand: shared instanceof SharedArrayBuffer, + before, + after: Array.from(view).join(","), + }); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-shared-array-buffer.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-shared-array-buffer.ts new file mode 100644 index 0000000000..2398a2cb2f --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-shared-array-buffer.ts @@ -0,0 +1,16 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const shared = new SharedArrayBuffer(4); +const view = new Uint8Array(shared); +view.set([1, 2, 3, 4]); + +const worker = new Worker("./postmessage-shared-array-buffer-worker.cjs"); +worker.on("online", () => worker.postMessage({ shared })); +worker.on("message", (message) => { + console.log("worker:", message.brand, message.before, message.after); + console.log("parent:", Array.from(view).join(",")); + worker.terminate().then((code) => console.log("terminate:", code)); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-url-rejection.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-url-rejection.ts new file mode 100644 index 0000000000..8f6c5c2d61 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/postmessage-url-rejection.ts @@ -0,0 +1,28 @@ +import { Worker } from "node:worker_threads"; + +function outcome(fn: () => void): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const worker = new Worker( + ` + const { parentPort } = require("node:worker_threads"); + parentPort.on("message", (message) => parentPort.postMessage(message)); + `, + { eval: true }, +); +worker.on("online", () => { + const url = new URL("https://example.org/path?q=value"); + console.log("worker:", outcome(() => worker.postMessage(url))); + worker.postMessage("still-alive"); +}); +worker.on("message", (message) => { + console.log("worker message:", message); + worker.terminate().then((code) => console.log("terminate:", code)); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-descriptor-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-descriptor-worker.cjs new file mode 100644 index 0000000000..02be0b1515 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-descriptor-worker.cjs @@ -0,0 +1,24 @@ +const { parentPort } = require("node:worker_threads"); + +function define(label, descriptor) { + const key = `PERRY_WORKER_DESCRIPTOR_${label.toUpperCase().replace("-", "_")}`; + try { + Object.defineProperty(process.env, key, descriptor); + return `${label}:ok:${process.env[key]}:${ + Object.keys(process.env).includes(key) + }`; + } catch (error) { + return `${label}:${error.name}:${error.code || ""}`; + } +} + +parentPort.postMessage([ + define("missing-flags", { value: 42 }), + define("accessor", { get: () => "getter", configurable: true }), + define("valid", { + value: 42, + configurable: true, + writable: true, + enumerable: true, + }), +]); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-descriptors.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-descriptors.ts new file mode 100644 index 0000000000..8375f88ff3 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-descriptors.ts @@ -0,0 +1,12 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./process-env-descriptor-worker.cjs"); +worker.on("message", (value: any) => { + console.log("descriptors:", value.join("|")); +}); +worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-option-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-option-worker.cjs new file mode 100644 index 0000000000..b45e57bb34 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-option-worker.cjs @@ -0,0 +1,5 @@ +const { parentPort } = require("node:worker_threads"); + +const inherited = process.env.PERRY_PROCESS_ENV_OPTION; +process.env.PERRY_PROCESS_ENV_OPTION = "worker-change"; +parentPort.postMessage(`${inherited}:${process.env.PERRY_PROCESS_ENV_OPTION}`); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-option.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-option.ts new file mode 100644 index 0000000000..286e52be57 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/process-env-option.ts @@ -0,0 +1,32 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const previous = process.env.PERRY_PROCESS_ENV_OPTION; +process.env.PERRY_PROCESS_ENV_OPTION = "parent-value"; + +function restore() { + if (previous === undefined) { + delete process.env.PERRY_PROCESS_ENV_OPTION; + } else { + process.env.PERRY_PROCESS_ENV_OPTION = previous; + } +} + +try { + const worker = new Worker("./process-env-option-worker.cjs", { + env: process.env, + }); + + worker.on("message", (value) => console.log("worker:", value)); + worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); + }); + worker.on("exit", (code) => { + console.log("exit:", code, process.env.PERRY_PROCESS_ENV_OPTION); + restore(); + }); +} catch (error: any) { + console.log("construction:", error?.name, error?.code ?? ""); + restore(); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/process-unsupported-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/process-unsupported-worker.cjs new file mode 100644 index 0000000000..180f46fb8c --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/process-unsupported-worker.cjs @@ -0,0 +1,61 @@ +const { parentPort } = require("node:worker_threads"); + +function outcome(fn) { + try { + fn(); + return "ok"; + } catch (error) { + return `${error?.name}:${error?.code ?? ""}`; + } +} + +const originalTitle = process.title; +process.title = `${originalTitle} worker`; +const title = process.title === `${originalTitle} worker`; + +const originalDebugPort = process.debugPort; +process.debugPort = originalDebugPort + 1; +const debugPort = process.debugPort === originalDebugPort + 1; + +const currentMask = process.umask(); +const names = ["abort", "chdir", "send", "disconnect"]; +for ( + const name of [ + "setuid", + "seteuid", + "setgid", + "setegid", + "setgroups", + "initgroups", + ] +) { + if (typeof process[name] === "function") names.push(name); +} + +const stubs = {}; +for (const name of names) { + stubs[name] = { + disabled: process[name]?.disabled === true, + call: outcome(() => process[name]()), + }; +} + +const getters = {}; +for (const name of ["channel", "connected"]) { + getters[name] = outcome(() => process[name]); +} + +parentPort.postMessage({ + title, + debugPort, + stubs, + getters, + umask: outcome(() => process.umask(currentMask)), + internals: [ + "_startProfilerIdleNotifier", + "_stopProfilerIdleNotifier", + "_debugProcess", + "_debugPause", + "_debugEnd", + ].every((name) => !(name in process)), +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/process-unsupported.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/process-unsupported.ts new file mode 100644 index 0000000000..2f18f32b14 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/process-unsupported.ts @@ -0,0 +1,36 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const previousChannel = process.env.NODE_CHANNEL_FD; +process.env.NODE_CHANNEL_FD = "worker-parity"; + +function restore() { + if (previousChannel === undefined) { + delete process.env.NODE_CHANNEL_FD; + } else { + process.env.NODE_CHANNEL_FD = previousChannel; + } +} + +try { + const worker = new Worker("./process-unsupported-worker.cjs"); + worker.on("message", (message) => { + console.log("writable:", message.title, message.debugPort); + console.log("stubs:", JSON.stringify(message.stubs)); + console.log("getters:", JSON.stringify(message.getters)); + console.log("umask:", message.umask); + console.log("internals:", message.internals); + }); + worker.on( + "error", + (error: any) => console.log("error:", error?.name, error?.code ?? ""), + ); + worker.on("exit", (code) => { + console.log("exit:", code); + restore(); + }); +} catch (error: any) { + console.log("construction:", error?.name, error?.code ?? ""); + restore(); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/queued-port-receive-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/queued-port-receive-worker.cjs new file mode 100644 index 0000000000..1d617cca67 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/queued-port-receive-worker.cjs @@ -0,0 +1,18 @@ +const { + receiveMessageOnPort, + parentPort, + workerData, +} = require("node:worker_threads"); + +const packet = receiveMessageOnPort(workerData.port); +const empty = receiveMessageOnPort(workerData.port); +parentPort.postMessage({ + text: packet?.message?.text ?? "missing", + count: packet?.message?.count ?? "missing", + empty: empty === undefined, +}); +try { + workerData.port.close(); +} catch (error) { + parentPort.postMessage({ closeError: error?.name ?? "Error" }); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/queued-port-receive.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/queued-port-receive.ts new file mode 100644 index 0000000000..549e5d835e --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/queued-port-receive.ts @@ -0,0 +1,27 @@ +import { MessageChannel, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const { port1, port2 } = new MessageChannel(); +port1.postMessage({ text: "queued", count: 2 }); + +const worker = new Worker("./queued-port-receive-worker.cjs", { + workerData: { port: port2 }, + transferList: [port2], +}); + +worker.on("message", (message) => { + if (message.closeError) { + console.log("close error:", message.closeError); + return; + } + console.log("received:", message.text, message.count, message.empty); +}); +worker.on( + "error", + (error: any) => console.log("error:", error?.name, error?.code ?? ""), +); +worker.on("exit", (code) => { + console.log("exit:", code); + port1.close(); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/resource-limits-metadata-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/resource-limits-metadata-worker.cjs new file mode 100644 index 0000000000..0c0d8643f8 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/resource-limits-metadata-worker.cjs @@ -0,0 +1,3 @@ +const { parentPort, resourceLimits } = require("node:worker_threads"); + +parentPort.postMessage(resourceLimits); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/resource-limits-metadata.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/resource-limits-metadata.ts new file mode 100644 index 0000000000..fbbef3ebdd --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/resource-limits-metadata.ts @@ -0,0 +1,25 @@ +import { resourceLimits, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const requested = { + maxOldGenerationSizeMb: 16, + maxYoungGenerationSizeMb: 4, + codeRangeSizeMb: 16, + stackSizeMb: 1, +}; + +console.log("main:", JSON.stringify(resourceLimits)); +const worker = new Worker("./resource-limits-metadata-worker.cjs", { + resourceLimits: requested, +}); +console.log("parent live:", JSON.stringify(worker.resourceLimits)); +worker.on("message", (value) => { + console.log("worker:", JSON.stringify(value)); +}); +worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); +}); +worker.on("exit", (code) => { + console.log("exit:", code, JSON.stringify(worker.resourceLimits)); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-founder-a-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-founder-a-worker.cjs new file mode 100644 index 0000000000..e9971ec8fd --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-founder-a-worker.cjs @@ -0,0 +1,18 @@ +const { SHARE_ENV, Worker } = require("node:worker_threads"); + +const before = process.env.PERRY_FOUNDER_KEY; +const worker = new Worker("./share-env-founder-b-worker.cjs", { + env: SHARE_ENV, +}); + +worker.on("error", () => { + process.exitCode = 1; +}); +worker.on("exit", code => { + if ( + code !== 0 || before !== "from-A" || + process.env.PERRY_FOUNDER_KEY !== "from-A" + ) { + process.exitCode = 1; + } +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-founder-b-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-founder-b-worker.cjs new file mode 100644 index 0000000000..c2f606ca8a --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-founder-b-worker.cjs @@ -0,0 +1 @@ +if (process.env.PERRY_FOUNDER_KEY !== "from-A") process.exitCode = 2; diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-founder.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-founder.ts new file mode 100644 index 0000000000..69da5375a5 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-founder.ts @@ -0,0 +1,34 @@ +import { SHARE_ENV, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +function wait(worker: Worker): Promise { + return new Promise((resolve) => { + worker.on("error", () => {}); + worker.on("exit", resolve); + }); +} + +async function main() { + const previous = process.env.PERRY_FOUNDER_KEY; + process.env.PERRY_FOUNDER_KEY = "from-main"; + + try { + await wait( + new Worker("./constructor-validation-worker.cjs", { env: SHARE_ENV }), + ); + const code = await wait( + new Worker("./share-env-founder-a-worker.cjs", { + env: { PERRY_FOUNDER_KEY: "from-A" }, + }), + ); + console.log("founder:", code, process.env.PERRY_FOUNDER_KEY); + } finally { + if (previous === undefined) delete process.env.PERRY_FOUNDER_KEY; + else process.env.PERRY_FOUNDER_KEY = previous; + } +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-indexed-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-indexed-worker.cjs new file mode 100644 index 0000000000..6dc2b3863d --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-indexed-worker.cjs @@ -0,0 +1,11 @@ +const { parentPort } = require("node:worker_threads"); + +const initial = process.env[123]; +process.env[456] = "from-worker"; +delete process.env[7]; +parentPort.postMessage({ + initial, + keys: Object.keys(process.env).filter((key) => + key === "123" || key === "456" + ), +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-indexed.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-indexed.ts new file mode 100644 index 0000000000..cd5244d127 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-indexed.ts @@ -0,0 +1,14 @@ +import { SHARE_ENV, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); +process.env[123] = "from-main"; +process.env[7] = "delete-me"; + +const worker = new Worker("./share-env-indexed-worker.cjs", { env: SHARE_ENV }); +worker.on("message", (message: any) => { + console.log("worker:", message.initial, message.keys.join(",")); +}); +worker.on("exit", (code) => { + console.log("main:", process.env[123], process.env[456], process.env[7]); + console.log("exit:", code); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-sibling-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-sibling-worker.cjs new file mode 100644 index 0000000000..9ae9e93e6e --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-sibling-worker.cjs @@ -0,0 +1,22 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.on("message", (command) => { + if (command === "mutate") { + process.env.PERRY_SHARED_SIBLING = "worker-value"; + delete process.env.PERRY_SHARED_DELETE; + parentPort.postMessage({ + phase: "mutated", + value: process.env.PERRY_SHARED_SIBLING, + deleted: process.env.PERRY_SHARED_DELETE === undefined, + }); + return; + } + + parentPort.postMessage({ + phase: "inspected", + value: process.env.PERRY_SHARED_SIBLING, + deleted: process.env.PERRY_SHARED_DELETE === undefined, + enumerated: Object.keys(process.env).includes("PERRY_SHARED_SIBLING"), + }); +}); +parentPort.postMessage({ phase: "ready" }); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-siblings.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-siblings.ts new file mode 100644 index 0000000000..30e127b14a --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-siblings.ts @@ -0,0 +1,47 @@ +import { SHARE_ENV, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +process.env.PERRY_SHARED_SIBLING = "parent"; +process.env.PERRY_SHARED_DELETE = "delete-me"; + +function run( + command: "mutate" | "inspect", +): Promise<{ message: any; code: number }> { + return new Promise((resolve) => { + const worker = new Worker("./share-env-sibling-worker.cjs", { + env: SHARE_ENV, + }); + worker.on("message", async (message: any) => { + if (message.phase === "ready") { + worker.postMessage(command); + return; + } + const code = await worker.terminate(); + resolve({ message, code }); + }); + }); +} + +async function main() { + const first = await run("mutate"); + console.log("first:", first.message.value, first.message.deleted); + + const second = await run("inspect"); + console.log( + "second:", + second.message.value, + second.message.deleted, + second.message.enumerated, + ); + console.log( + "parent:", + process.env.PERRY_SHARED_SIBLING, + process.env.PERRY_SHARED_DELETE === undefined, + ); + console.log("terminate:", first.code, second.code); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree-a-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree-a-worker.cjs new file mode 100644 index 0000000000..7c120cf166 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree-a-worker.cjs @@ -0,0 +1,10 @@ +const { SHARE_ENV, Worker } = require("node:worker_threads"); + +process.env.PERRY_TREE_A = "a"; +const worker = new Worker("./share-env-tree-b-worker.cjs", { env: SHARE_ENV }); +worker.on("error", () => { + process.exitCode = 1; +}); +worker.on("exit", code => { + if (code !== 0) process.exitCode = 1; +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree-b-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree-b-worker.cjs new file mode 100644 index 0000000000..5da78dc8f8 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree-b-worker.cjs @@ -0,0 +1 @@ +process.env.PERRY_TREE_B = "b"; diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree-c-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree-c-worker.cjs new file mode 100644 index 0000000000..9288df119b --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree-c-worker.cjs @@ -0,0 +1,3 @@ +process.env.PERRY_TREE_C = "c"; +process.env.PERRY_TREE_C_SEES_B = process.env.PERRY_TREE_B ?? "missing"; +process.env.PERRY_TREE_C_SEES_MAIN = process.env.PERRY_TREE_MAIN ?? "missing"; diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree.ts new file mode 100644 index 0000000000..5461e09948 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-tree.ts @@ -0,0 +1,64 @@ +import { SHARE_ENV, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +function wait(worker: Worker): Promise { + return new Promise((resolve, reject) => { + worker.on("error", reject); + worker.on("exit", (code) => { + if (code === 0) resolve(); + else reject(new Error("exit:" + code)); + }); + }); +} + +async function main() { + const previousMain = process.env.PERRY_TREE_MAIN; + const previousA = process.env.PERRY_TREE_A; + const previousB = process.env.PERRY_TREE_B; + const previousC = process.env.PERRY_TREE_C; + const previousCSeesB = process.env.PERRY_TREE_C_SEES_B; + const previousCSeesMain = process.env.PERRY_TREE_C_SEES_MAIN; + process.env.PERRY_TREE_MAIN = "main"; + delete process.env.PERRY_TREE_A; + delete process.env.PERRY_TREE_B; + delete process.env.PERRY_TREE_C; + delete process.env.PERRY_TREE_C_SEES_B; + delete process.env.PERRY_TREE_C_SEES_MAIN; + try { + await wait(new Worker("./share-env-tree-a-worker.cjs")); + await wait( + new Worker("./share-env-tree-c-worker.cjs", { env: SHARE_ENV }), + ); + console.log( + "C observations:", + process.env.PERRY_TREE_C_SEES_B === "missing" + ? null + : process.env.PERRY_TREE_C_SEES_B ?? null, + process.env.PERRY_TREE_C_SEES_MAIN ?? null, + ); + console.log( + "main:", + process.env.PERRY_TREE_B ?? null, + process.env.PERRY_TREE_C ?? null, + ); + } finally { + for ( + const [key, value] of [ + ["PERRY_TREE_MAIN", previousMain], + ["PERRY_TREE_A", previousA], + ["PERRY_TREE_B", previousB], + ["PERRY_TREE_C", previousC], + ["PERRY_TREE_C_SEES_B", previousCSeesB], + ["PERRY_TREE_C_SEES_MAIN", previousCSeesMain], + ] as const + ) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } + } +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-worker.cjs new file mode 100644 index 0000000000..80e22c5aa0 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env-worker.cjs @@ -0,0 +1,13 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.postMessage({ + phase: "ready", + parent: process.env.PERRY_SHARED_PARENT, +}); +parentPort.once("message", () => { + process.env.PERRY_SHARED_WORKER = "worker-value"; + parentPort.postMessage({ + phase: "checked", + parent: process.env.PERRY_SHARED_PARENT, + }); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/share-env.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env.ts new file mode 100644 index 0000000000..29f211aafe --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/share-env.ts @@ -0,0 +1,21 @@ +import { SHARE_ENV, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +process.env.PERRY_SHARED_PARENT = "initial"; +delete process.env.PERRY_SHARED_WORKER; + +const worker = new Worker("./share-env-worker.cjs", { env: SHARE_ENV }); +worker.on("message", (message: any) => { + if (message.phase === "ready") { + console.log("worker initial:", message.parent); + process.env.PERRY_SHARED_PARENT = "updated"; + worker.postMessage("check"); + return; + } + + console.log("worker updated:", message.parent); + console.log("parent sees worker:", process.env.PERRY_SHARED_WORKER); + worker.terminate().then((code) => console.log("terminate:", code)); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/terminate-idempotent.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/terminate-idempotent.ts new file mode 100644 index 0000000000..7d19637a16 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/terminate-idempotent.ts @@ -0,0 +1,12 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./termination-worker.cjs"); +worker.on("online", async () => { + const first = worker.terminate(); + const second = worker.terminate(); + console.log("promise identity:", first === second); + console.log("results:", await first, await second); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/termination-ordering.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/termination-ordering.ts new file mode 100644 index 0000000000..c505e66fcb --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/termination-ordering.ts @@ -0,0 +1,29 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const events: string[] = []; +let exitCode: number | undefined; +let terminateCode: number | undefined; + +function finish() { + if (exitCode === undefined || terminateCode === undefined) return; + console.log("lifecycle:", events.join(",")); + console.log("codes:", exitCode, terminateCode); +} + +const worker = new Worker("./termination-worker.cjs"); +worker.on("online", () => events.push("online")); +worker.on("message", (message) => { + events.push(`message:${message}`); + worker.terminate().then((code) => { + events.push("terminate-resolved"); + terminateCode = code; + finish(); + }); +}); +worker.on("exit", (code) => { + events.push("exit"); + exitCode = code; + finish(); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/termination-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/termination-worker.cjs new file mode 100644 index 0000000000..86dee53f2c --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/termination-worker.cjs @@ -0,0 +1,4 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.on("message", () => {}); +parentPort.postMessage("ready"); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/thread-metadata-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/thread-metadata-worker.cjs new file mode 100644 index 0000000000..fcb6cc2d37 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/thread-metadata-worker.cjs @@ -0,0 +1,3 @@ +const { parentPort, threadId, threadName } = require("node:worker_threads"); + +parentPort.postMessage({ threadId, threadName }); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/thread-metadata.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/thread-metadata.ts new file mode 100644 index 0000000000..ea8e55fabb --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/thread-metadata.ts @@ -0,0 +1,22 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./thread-metadata-worker.cjs", { + name: "parity-worker", +}); + +console.log("parent initial:", worker.threadId > 0, worker.threadName); +worker.on("online", () => { + console.log("online:", worker.threadId > 0, worker.threadName); +}); +worker.on("message", (message) => { + console.log( + "worker values:", + message.threadId === worker.threadId, + message.threadName, + ); +}); +worker.on("exit", (code) => { + console.log("exit:", code, worker.threadId, worker.threadName); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/thread-name-defaults.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/thread-name-defaults.ts new file mode 100644 index 0000000000..5eacbaf56c --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/thread-name-defaults.ts @@ -0,0 +1,32 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const cases: Array<[string, boolean, any]> = [ + ["missing", false, undefined], + ["empty", true, ""], + ["false", true, false], + ["zero", true, 0], + ["null", true, null], + ["nan", true, Number.NaN], +]; + +async function run(label: string, includeName: boolean, name: any) { + const options = includeName ? { name } : {}; + const worker = new Worker("./thread-metadata-worker.cjs", options); + + const message = await new Promise((resolve, reject) => { + worker.once("message", resolve); + worker.once("error", reject); + }); + console.log(label, worker.threadName, message.threadName); + await new Promise((resolve) => worker.once("exit", () => resolve())); +} + +async function main() { + for (const entry of cases) await run(...entry); +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.code ?? "") +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/unreferenced-arraybuffer-transfer.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/unreferenced-arraybuffer-transfer.ts new file mode 100644 index 0000000000..feb113a66f --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/unreferenced-arraybuffer-transfer.ts @@ -0,0 +1,16 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const buffer = new ArrayBuffer(8); +new Uint8Array(buffer)[0] = 9; +const worker = new Worker("./natural-exit-worker.cjs", { + transferList: [buffer], +}); + +console.log("detached:", buffer.byteLength); +worker.on( + "error", + (error: any) => console.log("error:", error?.name, error?.code ?? ""), +); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/unsupported-paths.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/unsupported-paths.ts new file mode 100644 index 0000000000..5d5a8531f2 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/unsupported-paths.ts @@ -0,0 +1,25 @@ +import { Worker } from "node:worker_threads"; + +const values: Array<[string, any]> = [ + ["bare relative", "worker.cjs"], + ["http string", "https://example.com/worker.js"], + ["http URL", new URL("https://example.com/worker.js")], +]; + +async function main() { + for (const [label, value] of values) { + let worker: Worker | undefined; + try { + worker = new Worker(value); + console.log(label, "created"); + } catch (error: any) { + console.log(label, error?.name, error?.code ?? ""); + } finally { + await worker?.terminate().catch(() => {}); + } + } +} + +main().catch((error) => + console.log("unexpected:", error?.name, error?.message) +); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/url-eval-validation.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/url-eval-validation.ts new file mode 100644 index 0000000000..432eedb5e2 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/url-eval-validation.ts @@ -0,0 +1,18 @@ +import { Worker } from "node:worker_threads"; + +const filename = new URL( + "./internal-thread-worker.cjs", + import.meta.url, +); + +try { + const worker = new Worker(filename, { eval: true }); + console.log("construct: ok"); + worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); + }); + worker.on("exit", (code) => console.log("exit:", code)); + worker.terminate(); +} catch (error: any) { + console.log("construct:", error?.name, error?.code ?? ""); +} diff --git "a/test-parity/node-suite/worker_threads/worker-lifecycle/url-\317\200-worker.cjs" "b/test-parity/node-suite/worker_threads/worker-lifecycle/url-\317\200-worker.cjs" new file mode 100644 index 0000000000..dc63d606fe --- /dev/null +++ "b/test-parity/node-suite/worker_threads/worker-lifecycle/url-\317\200-worker.cjs" @@ -0,0 +1,4 @@ +const path = require("node:path"); +const { parentPort, workerData } = require("node:worker_threads"); + +parentPort.postMessage(`${workerData}:${path.basename(__filename)}`); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-buffers-after-exit.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-buffers-after-exit.ts new file mode 100644 index 0000000000..eb02f21e20 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-buffers-after-exit.ts @@ -0,0 +1,35 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +let arrayBuffer: any; +let sharedBuffer: any; +let detachedLength: any = "missing"; +const worker = new Worker("./worker-created-buffers-worker.cjs"); + +worker.on("message", (value: any) => { + if ("detachedLength" in value) { + detachedLength = value.detachedLength; + return; + } + arrayBuffer = value.arrayBuffer; + sharedBuffer = value.sharedBuffer; +}); +worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); +}); +worker.on("exit", (code) => { + const arrayValues = arrayBuffer instanceof ArrayBuffer + ? Array.from(new Uint8Array(arrayBuffer)).join(",") + : "missing"; + const sharedValues = sharedBuffer instanceof SharedArrayBuffer + ? Array.from(new Uint8Array(sharedBuffer)).join(",") + : "missing"; + console.log( + "brands:", + arrayBuffer instanceof ArrayBuffer, + sharedBuffer instanceof SharedArrayBuffer, + ); + console.log("values:", arrayValues, sharedValues, detachedLength); + console.log("exit:", code); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-buffers-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-buffers-worker.cjs new file mode 100644 index 0000000000..0efd4b6187 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-buffers-worker.cjs @@ -0,0 +1,9 @@ +const { parentPort } = require("node:worker_threads"); + +const arrayBuffer = new ArrayBuffer(4); +new Uint8Array(arrayBuffer).set([1, 2, 3, 4]); +const sharedBuffer = new SharedArrayBuffer(4); +new Uint8Array(sharedBuffer).set([5, 6, 7, 8]); + +parentPort.postMessage({ arrayBuffer, sharedBuffer }, [arrayBuffer]); +parentPort.postMessage({ detachedLength: arrayBuffer.byteLength }); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-port-after-exit.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-port-after-exit.ts new file mode 100644 index 0000000000..0a72540c56 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-port-after-exit.ts @@ -0,0 +1,23 @@ +import { MessagePort, receiveMessageOnPort, Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +let receivedPort: any; +const worker = new Worker("./worker-created-port-worker.cjs"); +worker.on("message", (value: any) => { + receivedPort = value?.port; +}); +worker.on("error", (error: any) => { + console.log("error:", error?.name, error?.code ?? ""); +}); +worker.on("exit", (code) => { + let message: any = "missing"; + try { + message = receiveMessageOnPort(receivedPort)?.message ?? "empty"; + } catch (error: any) { + message = `${error?.name}:${error?.code ?? ""}`; + } + console.log("port:", receivedPort instanceof MessagePort, message); + console.log("exit:", code); + receivedPort?.close?.(); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-port-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-port-worker.cjs new file mode 100644 index 0000000000..adcc8ccb93 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-created-port-worker.cjs @@ -0,0 +1,6 @@ +const { MessageChannel, parentPort } = require("node:worker_threads"); + +const { port1, port2 } = new MessageChannel(); +port2.postMessage("queued-before-exit"); +parentPort.postMessage({ port: port1 }, [port1]); +port2.close(); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/worker-eventemitter-surface.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-eventemitter-surface.ts new file mode 100644 index 0000000000..83c8bf4437 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-eventemitter-surface.ts @@ -0,0 +1,48 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./termination-worker.cjs"); +const names = [ + "addListener", + "emit", + "eventNames", + "getMaxListeners", + "listenerCount", + "listeners", + "off", + "on", + "once", + "prependListener", + "prependOnceListener", + "rawListeners", + "removeAllListeners", + "removeListener", + "setMaxListeners", +]; + +console.log( + "methods:", + names.map((name) => `${name}:${typeof (worker as any)[name]}`).join(","), +); +const eventNames = typeof (worker as any).eventNames === "function" + ? JSON.stringify((worker as any).eventNames()) + : "unsupported"; +console.log("event names:", eventNames); + +if ( + typeof (worker as any).getMaxListeners === "function" && + typeof (worker as any).setMaxListeners === "function" +) { + console.log( + "max listeners:", + typeof (worker as any).getMaxListeners(), + (worker as any).setMaxListeners(7) === worker, + ); + console.log("updated max:", (worker as any).getMaxListeners()); +} else { + console.log("max listeners: unsupported"); +} + +worker.terminate().then((code) => console.log("terminate:", code)); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/worker-ref-state.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-ref-state.ts new file mode 100644 index 0000000000..85f8ea4483 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/worker-ref-state.ts @@ -0,0 +1,12 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./termination-worker.cjs"); +worker.on("message", (message) => { + console.log("ready:", message); + console.log("unref return:", worker.unref() === worker); + console.log("ref return:", worker.ref() === worker); + worker.terminate().then((code) => console.log("terminate:", code)); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-alias-view-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-alias-view-worker.cjs new file mode 100644 index 0000000000..fbf234b3ef --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-alias-view-worker.cjs @@ -0,0 +1,14 @@ +const { parentPort, workerData } = require("node:worker_threads"); +const left = workerData?.left; +const right = workerData?.right; +const view = workerData?.view; +const buffer = workerData?.buffer; + +parentPort.postMessage({ + alias: left !== undefined && left === right, + viewBrand: view instanceof Uint16Array, + bufferBrand: buffer instanceof ArrayBuffer, + backing: view !== undefined && view.buffer === buffer, + value: view?.[0] ?? "missing", + lengths: [buffer?.byteLength ?? "missing", view?.byteLength ?? "missing"], +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-alias-view.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-alias-view.ts new file mode 100644 index 0000000000..77709979f6 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-alias-view.ts @@ -0,0 +1,34 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const shared = { value: 7 }; +const buffer = new ArrayBuffer(4); +const view = new Uint16Array(buffer); +view[0] = 0x1234; + +const worker = new Worker("./workerdata-alias-view-worker.cjs", { + workerData: { + left: shared, + right: shared, + buffer, + view, + }, +}); +console.log("parent:", buffer.byteLength, view.byteLength, view[0]); +worker.on("message", (message) => { + console.log( + "worker:", + message.alias, + message.viewBrand, + message.bufferBrand, + message.backing, + message.value, + message.lengths.join(","), + ); +}); +worker.on( + "error", + (error) => console.log("error:", error.name, (error as any).code ?? ""), +); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-builtins.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-builtins.ts new file mode 100644 index 0000000000..b8c8063a39 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-builtins.ts @@ -0,0 +1,36 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const worker = new Worker("./builtin-data-worker.cjs", { + workerData: { + date: new Date("2021-02-03T04:05:06.000Z"), + map: new Map([["key", 17]]), + set: new Set([2, 3]), + regexp: /data/i, + bigint: 12345678901234567890n, + error: new TypeError("worker-data-error"), + }, +}); + +worker.on("message", (message: any) => { + console.log( + "brands:", + message?.date, + message?.map, + message?.set, + message?.regexp, + message?.bigintType, + message?.error, + ); + console.log( + "values:", + message?.dateValue, + message?.mapValue, + message?.setValue, + message?.regexpValue, + message?.bigintValue, + message?.errorValue, + ); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-cycle-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-cycle-worker.cjs new file mode 100644 index 0000000000..33266dc2dd --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-cycle-worker.cjs @@ -0,0 +1,6 @@ +const { parentPort, workerData } = require("node:worker_threads"); +parentPort.postMessage({ + cycle: workerData.self === workerData, + nested: workerData.child.parent === workerData, + value: workerData.child.value, +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-cycle.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-cycle.ts new file mode 100644 index 0000000000..4dc66fd8de --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-cycle.ts @@ -0,0 +1,25 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const data: any = { child: { value: 9 } }; +data.self = data; +data.child.parent = data; + +try { + const worker = new Worker("./workerdata-cycle-worker.cjs", { + workerData: data, + }); + worker.on( + "message", + (message) => + console.log("cycle:", message.cycle, message.nested, message.value), + ); + worker.on( + "error", + (error) => console.log("error:", error.name, (error as any).code ?? ""), + ); + worker.on("exit", (code) => console.log("exit:", code)); +} catch (error) { + console.log("construct:", (error as any)?.name, (error as any)?.code ?? ""); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-defaults.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-defaults.ts new file mode 100644 index 0000000000..5729141de8 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-defaults.ts @@ -0,0 +1,31 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +function run(label: string, options?: { workerData?: any }): Promise { + return new Promise((resolve) => { + const worker = new Worker("./default-data-worker.cjs", options); + worker.on("message", (message: any) => { + console.log( + `${label}:`, + message?.isMainThread, + message?.type, + message?.isNull, + message?.value, + ); + }); + worker.on("exit", (code) => { + console.log(`${label} exit:`, code); + resolve(); + }); + }); +} + +async function main() { + await run("omitted"); + await run("undefined", { workerData: undefined }); + await run("null", { workerData: null }); + await run("object", { workerData: { value: 5 } }); +} + +main().catch((error) => console.log("unexpected:", error?.name, error?.message)); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-multiple-ports-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-multiple-ports-worker.cjs new file mode 100644 index 0000000000..44022a667f --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-multiple-ports-worker.cjs @@ -0,0 +1,20 @@ +const { + MessagePort, + parentPort, + workerData, +} = require("node:worker_threads"); + +const first = workerData?.first; +const second = workerData?.second; + +if (typeof first?.postMessage === "function") first.postMessage("first"); +if (typeof second?.postMessage === "function") second.postMessage("second"); + +parentPort.postMessage({ + firstBrand: first instanceof MessagePort, + secondBrand: second instanceof MessagePort, + distinct: first !== undefined && second !== undefined && first !== second, +}); + +first?.close?.(); +second?.close?.(); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-multiple-ports.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-multiple-ports.ts new file mode 100644 index 0000000000..bd59f9b408 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-multiple-ports.ts @@ -0,0 +1,41 @@ +import { + MessageChannel, + receiveMessageOnPort, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const first = new MessageChannel(); +const second = new MessageChannel(); +const worker = new Worker("./workerdata-multiple-ports-worker.cjs", { + workerData: { first: first.port1, second: second.port1 }, + transferList: [first.port1, second.port1], +}); +let summary: any; + +worker.on("message", (message) => { + summary = message; +}); +worker.on( + "error", + (error) => console.log("error:", error.name, (error as any).code ?? ""), +); +worker.on("exit", (code) => { + console.log( + "ports:", + summary?.firstBrand, + summary?.secondBrand, + summary?.distinct, + ); + console.log( + "delivery:", + receiveMessageOnPort(first.port2)?.message, + receiveMessageOnPort(second.port2)?.message, + ); + console.log("exit:", code); + first.port1.close(); + first.port2.close(); + second.port1.close(); + second.port2.close(); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-alias-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-alias-worker.cjs new file mode 100644 index 0000000000..25210508f4 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-alias-worker.cjs @@ -0,0 +1,19 @@ +const { + MessagePort, + parentPort, + workerData, +} = require("node:worker_threads"); + +const left = workerData?.left; +const right = workerData?.right; + +if (typeof left?.postMessage === "function") left.postMessage("left"); +if (typeof right?.postMessage === "function") right.postMessage("right"); + +parentPort.postMessage({ + leftBrand: left instanceof MessagePort, + rightBrand: right instanceof MessagePort, + alias: left !== undefined && left === right, +}); + +left?.close?.(); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-alias.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-alias.ts new file mode 100644 index 0000000000..d51adcd030 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-alias.ts @@ -0,0 +1,38 @@ +import { + MessageChannel, + receiveMessageOnPort, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const channel = new MessageChannel(); +const worker = new Worker("./workerdata-port-alias-worker.cjs", { + workerData: { left: channel.port1, right: channel.port1 }, + transferList: [channel.port1], +}); +let summary: any; + +worker.on("message", (message) => { + summary = message; +}); +worker.on( + "error", + (error) => console.log("error:", error.name, (error as any).code ?? ""), +); +worker.on("exit", (code) => { + console.log( + "port alias:", + summary?.leftBrand, + summary?.rightBrand, + summary?.alias, + ); + console.log( + "delivery:", + receiveMessageOnPort(channel.port2)?.message, + receiveMessageOnPort(channel.port2)?.message, + ); + console.log("exit:", code); + channel.port1.close(); + channel.port2.close(); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-container-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-container-worker.cjs new file mode 100644 index 0000000000..8c8818d341 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-container-worker.cjs @@ -0,0 +1,32 @@ +const { + MessagePort, + parentPort, + workerData, +} = require("node:worker_threads"); + +const kind = workerData?.kind; +const container = workerData?.container; +let port; +let extractionError = "none"; + +try { + if (kind === "map" && typeof container?.get === "function") { + port = container.get("port"); + } else if (kind === "set" && typeof container?.values === "function") { + const iterator = container.values(); + if (typeof iterator?.next === "function") port = iterator.next()?.value; + } +} catch (error) { + extractionError = error?.name ?? "unknown"; +} + +if (typeof port?.postMessage === "function") port.postMessage(kind); +parentPort.postMessage({ + kind, + containerBrand: kind === "map" + ? container instanceof Map + : container instanceof Set, + portBrand: port instanceof MessagePort, + extractionError, +}); +port?.close?.(); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-map.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-map.ts new file mode 100644 index 0000000000..7fed82e86f --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-map.ts @@ -0,0 +1,41 @@ +import { + MessageChannel, + receiveMessageOnPort, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const channel = new MessageChannel(); +try { + const worker = new Worker("./workerdata-port-container-worker.cjs", { + workerData: { + kind: "map", + container: new Map([["port", channel.port1]]), + }, + transferList: [channel.port1], + }); + worker.on("message", (message) => { + console.log( + "map:", + message.kind, + message.containerBrand, + message.portBrand, + message.extractionError, + ); + console.log("delivery:", receiveMessageOnPort(channel.port2)?.message); + }); + worker.on( + "error", + (error) => console.log("error:", error.name, (error as any).code ?? ""), + ); + worker.on("exit", (code) => { + console.log("exit:", code); + channel.port1.close(); + channel.port2.close(); + }); +} catch (error) { + console.log("construct:", (error as any)?.name, (error as any)?.code ?? ""); + channel.port1.close(); + channel.port2.close(); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-set.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-set.ts new file mode 100644 index 0000000000..839cd6288d --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-set.ts @@ -0,0 +1,41 @@ +import { + MessageChannel, + receiveMessageOnPort, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const channel = new MessageChannel(); +try { + const worker = new Worker("./workerdata-port-container-worker.cjs", { + workerData: { + kind: "set", + container: new Set([channel.port1]), + }, + transferList: [channel.port1], + }); + worker.on("message", (message) => { + console.log( + "set:", + message.kind, + message.containerBrand, + message.portBrand, + message.extractionError, + ); + console.log("delivery:", receiveMessageOnPort(channel.port2)?.message); + }); + worker.on( + "error", + (error) => console.log("error:", error.name, (error as any).code ?? ""), + ); + worker.on("exit", (code) => { + console.log("exit:", code); + channel.port1.close(); + channel.port2.close(); + }); +} catch (error) { + console.log("construct:", (error as any)?.name, (error as any)?.code ?? ""); + channel.port1.close(); + channel.port2.close(); +} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-surface-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-surface-worker.cjs new file mode 100644 index 0000000000..9967642696 --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-surface-worker.cjs @@ -0,0 +1,17 @@ +const { MessagePort, parentPort, workerData } = require("node:worker_threads"); + +const port = workerData?.port; +const methods = ["addListener", "off", "on", "once", "postMessage"] + .map((name) => `${name}:${typeof port?.[name]}`) + .join(","); + +if (typeof port?.postMessage === "function") { + port.postMessage("from-transferred-port"); +} +parentPort.postMessage({ + brand: port instanceof MessagePort, + methods, +}); +try { + port?.close?.(); +} catch {} diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-surface.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-surface.ts new file mode 100644 index 0000000000..5577a2f6ad --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-surface.ts @@ -0,0 +1,27 @@ +import { + MessageChannel, + receiveMessageOnPort, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const { port1, port2 } = new MessageChannel(); +const worker = new Worker("./workerdata-port-surface-worker.cjs", { + workerData: { port: port2 }, + transferList: [port2], +}); +let summary: any; + +worker.on("message", (message) => summary = message); +worker.on("exit", (code) => { + const packet = receiveMessageOnPort(port1); + console.log( + "surface:", + summary?.brand ?? "missing", + summary?.methods ?? "missing", + ); + console.log("peer:", packet ? packet.message : "missing"); + console.log("exit:", code); + port1.close(); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-transfer.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-transfer.ts new file mode 100644 index 0000000000..fd3fadc09e --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-transfer.ts @@ -0,0 +1,28 @@ +import { + MessageChannel, + receiveMessageOnPort, + Worker, +} from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const channel = new MessageChannel(); +const worker = new Worker("./workerdata-port-worker.cjs", { + workerData: { port: channel.port1 }, + transferList: [channel.port1], +}); + +worker.on("message", (message: any) => { + const packet = receiveMessageOnPort(channel.port2); + console.log("worker port:", message?.brand, message?.postMessage); + console.log("peer delivery:", packet ? packet.message : undefined); + channel.port1.postMessage("old-owner"); + const oldOwner = receiveMessageOnPort(channel.port2); + console.log("old owner detached:", oldOwner ? oldOwner.message : undefined); + worker.terminate(); +}); +worker.on("exit", (code) => { + console.log("exit:", code); + channel.port1.close(); + channel.port2.close(); +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-worker.cjs new file mode 100644 index 0000000000..6cd7fd048a --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-port-worker.cjs @@ -0,0 +1,14 @@ +const { + MessagePort, + parentPort, + workerData, +} = require("node:worker_threads"); + +const port = workerData?.port; +if (typeof port?.postMessage === "function") { + port.postMessage("from-worker"); +} +parentPort.postMessage({ + brand: port instanceof MessagePort, + postMessage: typeof port?.postMessage, +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-shared-array-buffer-worker.cjs b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-shared-array-buffer-worker.cjs new file mode 100644 index 0000000000..2e1e5b9b4e --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-shared-array-buffer-worker.cjs @@ -0,0 +1,10 @@ +const { parentPort, workerData } = require("node:worker_threads"); + +const view = new Uint8Array(workerData.shared); +const before = Array.from(view).join(","); +view[0] = 9; +parentPort.postMessage({ + brand: workerData.shared instanceof SharedArrayBuffer, + before, + after: Array.from(view).join(","), +}); diff --git a/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-shared-array-buffer.ts b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-shared-array-buffer.ts new file mode 100644 index 0000000000..9f92f030de --- /dev/null +++ b/test-parity/node-suite/worker_threads/worker-lifecycle/workerdata-shared-array-buffer.ts @@ -0,0 +1,16 @@ +import { Worker } from "node:worker_threads"; + +process.chdir("test-parity/node-suite/worker_threads/worker-lifecycle"); + +const shared = new SharedArrayBuffer(4); +const view = new Uint8Array(shared); +view.set([1, 2, 3, 4]); + +const worker = new Worker("./workerdata-shared-array-buffer-worker.cjs", { + workerData: { shared }, +}); +worker.on("message", (message) => { + console.log("worker:", message.brand, message.before, message.after); + console.log("parent shared:", Array.from(view).join(",")); +}); +worker.on("exit", (code) => console.log("exit:", code)); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 922c40b052..95216d0cc1 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -5,9 +5,9 @@ "note": "Deterministic modules are floored at full pass. Timing/racy modules (http2, net, stream, diagnostics_channel, fs-promises) carry a small margin below observed pass so ordinary flake does not false-alarm; the guard still catches real regressions, which are large (e.g. dns 6->0, http 19->9). node_suite_run.normalize() scrubs environment-variant tokens (console.time hrtime durations, stack-trace frame lines) symmetrically before the stdout compare, so console is floored at full pass (119) on its deterministic content. http is verified 19/19 in isolation but the full-suite harness flakes to 17 under port contention, so it is floored at 17 (flake margin, not a regression); a real http break is a much larger drop. Floors were refreshed from a clean node-26 run at 2810/2863 (98.1%), then the deterministic child_process floor was measured independently at 43/53 on Node 26.5.0. The overall summary is the sum of the committed per-module floors." }, "overall": { - "pass": 2818, - "total": 2890, - "pct": 97.5 + "pass": 2839, + "total": 3078, + "pct": 92.2 }, "modules": { "assert": { @@ -215,8 +215,8 @@ "total": 3 }, "worker_threads": { - "pass": 17, - "total": 17 + "pass": 38, + "total": 205 }, "zlib": { "pass": 58,