diff --git a/scripts/node_suite_run.py b/scripts/node_suite_run.py old mode 100755 new mode 100644 index a95778c343..2c43e98424 --- a/scripts/node_suite_run.py +++ b/scripts/node_suite_run.py @@ -69,7 +69,7 @@ def normalize(text: str) -> str: # Modules that must run one-at-a-time (port binding / process spawn / event-loop # or timer ordering). Parallelism corrupts their results. SLOW_MODULES = { - "http", "http2", "https", "net", "dgram", "tls", "cluster", "dns", + "http", "http2", "https", "net", "dgram", "tls", "cluster", "dns", "async_hooks", "stream", "child_process", "worker_threads", "inspector", "inspector-promises", "repl", "diagnostics_channel", "timers", "fetch", } diff --git a/test-parity/node-suite/async_hooks/README.md b/test-parity/node-suite/async_hooks/README.md new file mode 100644 index 0000000000..cd0fb9578d --- /dev/null +++ b/test-parity/node-suite/async_hooks/README.md @@ -0,0 +1,260 @@ +# `node:async_hooks` granular parity suite + +This directory tests deterministic public behavior from `node:async_hooks`. +Fixtures assert ID relationships rather than exact numeric IDs and use explicit +completion barriers for asynchronous work. + +## Upstream comparison + +The expansion was reviewed on 2026-07-16 against primary repository sources: + +- Node.js main at + [`34c28d5`](https://github.com/nodejs/node/tree/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/async-hooks), + especially the AsyncResource lifecycle, AsyncLocalStorage nesting, + enable/disable, promise, pre-hook Promise creation, late hook activation, + execution-resource identity, default trigger, concurrent HTTP/socket, and + async/await cases, plus its + [bind](https://github.com/nodejs/node/blob/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel/test-async-local-storage-bind.js) + and + [snapshot](https://github.com/nodejs/node/blob/34c28d5a69f4f00cd599adcbe57834435d3a683b/test/parallel/test-async-local-storage-snapshot.js) + selections. +- Deno main at + [`f8a17c8`](https://github.com/denoland/deno/blob/f8a17c8171569fa2870d740030aaa59c91fdf9ee/tests/unit_node/async_hooks_test.ts), + whose selected compatibility coverage independently emphasizes nesting, + enterWith, bind/snapshot, AsyncResource scope callbacks, and async API + propagation. +- Bun main at + [`5d350cc`](https://github.com/oven-sh/bun/tree/5d350cc17525a493fcb55b0a014f75af7c414580/test/js/node/async_hooks), + plus its selected Node compatibility cases for constructor behavior, + receiver preservation, context isolation, and exit cleanup. + +The correctness oracle remains the repository-pinned Node 26.5.0. + +The current expansion directly maps the deterministic public contracts from +Node's `test-async-hooks-disable-during-promise.js`, +`test-async-hooks-enable-during-promise.js`, +`test-async-hooks-promise-triggerid.js`, both +`test-promise.*-before-init-hooks.js` cases, `test-late-hook-enable.js`, +`test-nexttick-default-trigger.js`, `test-async-exec-resource-match.js`, +`test-async-hooks-correctly-switch-promise-hook.js`, +`test-async-hooks-close-during-destroy.js`, +`test-async-hooks-execution-async-resource-await.js`, +`test-async-local-storage-http-multiclients.js`, +`test-async-local-storage-socket.js`, +`test-eventemitter-asyncresource.js`, `test-async-wrap-trigger-id.js`, +`test-timers.setInterval.js`, `test-fsreqcallback-readFile.js`, +`test-getaddrinforeqwrap.js`, `test-getnameinforeqwrap.js`, +`test-querywrap.js`, `test-crypto-randomBytes.js`, and +`test-zlib.zlib-binding.deflate.js`, `test-immediate.js`, +`test-fseventwrap.js`, `test-statwatcher.js`, `test-udpwrap.js`, +`test-tcpwrap.js`, `test-shutdownwrap.js`, `test-graph.timeouts.js`, +`test-pipewrap.js`, `test-signalwrap.js`, and +`test-async-wrap-providers.js`, plus the `DIRHANDLE` selection in +`test-async-wrap-getasyncid.js`. The current fixtures additionally isolate +`test-timers-clearImmediate-als.js`, the AsyncLocalStorage branch of +`test-web-locks.js`, the resource back-reference from +`test-eventemitter-asyncresource.js`, and the bounded WORKER/MESSAGEPORT +relationships exercised by Node's worker provider selections. The provider +request fixtures also exercise the +`HASHREQUEST`, `CIPHERREQUEST`, and `SIGNREQUEST` paths used by Node's current +crypto implementation. Deno's selected bind/snapshot, nesting, enterWith, +resource-scope, and propagation contracts and Bun's async-context provider +matrix are represented by smaller single-boundary fixtures rather than copied +monolithic tests. + +For the latest 12 new fixtures and three enhanced fixtures, Node 26.5.0 produced +byte-identical output in three rounds. The additions cover the complete claimed +function/export metadata, module namespace descriptors and immutability, +constructor-call and inherited option-accessor behavior, accessor exception +cleanup, arbitrary `enterWith` store values, self-cleared immediates, Web Locks, +EventEmitterAsyncResource back-references, BLOBREADER, and DNSCHANNEL. Existing +HTTP, worker, and timers/promises cases now also cover native provider +lifecycles, scheduler wait/yield, and promise intervals. Perry matches the new +Web Locks and `enterWith` value matrices; the other ten new cases remain stable +diagnostics. Bun 1.2.18 matches the module surface, constructor calls, +self-cleared immediates, value matrix, and timers additions but still exposes a +no-op `createHook`. Deno 2.9.2 matches the module surface, constructor calls, +hook option access/lifecycle, self-cleared immediates, value matrix, +EventEmitterAsyncResource back-reference, and timers additions; its provider +selection remains different. These divergences are comparison evidence, not a +reason to weaken the Node oracle. + +For the preceding 12 fixtures, Node 26.5.0 produced byte-identical output in three +rounds. Deno 2.9.2 matches Node's import/namespace table, both subclass cases, +nested Timeout lifecycle, AsyncResource detached methods, and frozen +null-prototype provider table, while its native provider selection differs for +child pipes, signals, directory handles, and WebCrypto requests. Bun 1.2.18 +matches import identity/branding and AsyncLocalStorage subclass propagation, +but its current `createHook` is a no-op and its provider table remains mutable +with an ordinary object prototype. These divergences are recorded as comparison +evidence, not used to weaken the Node oracle. + +The current focused result is **78/193** and is recorded in +`node_suite_baseline.json`. The suite keeps every stable mismatch as a diagnostic +rather than removing unsupported cases: failures identify context loss, missing hook callbacks/resources, +lifecycle differences, validation gaps, or a compile/runtime boundary for the +specific provider named by the fixture. + +The 115 non-matching diagnostics are stable and grouped as follows: + +- hook delivery/configuration: custom and built-in provider lifecycle callbacks, + cancelled resource destruction and identity, simultaneous hooks, late + activation during timers/immediates/next ticks and Promise chains, + pre-created Promise relationships, mixed Promise hook shapes, destroy work + queued from a destroy callback, repeated interval and sibling-nextTick + resources, fs.readFile/fs-promises and DNS trigger/lifecycle resources, + filesystem watcher, DIRHANDLE, BLOBREADER, DNSCHANNEL, PROCESS/PIPE, SIGNAL, + WORKER/MESSAGEPORT, HTTP client/incoming, UDP/TCP/shutdown, + classic and WebCrypto request, randomBytes, and zlib resources, + `promiseResolve`, resource arguments, execution-resource mapping/metadata, + static-bind resource types, the async-wrap provider table prototype, and + `trackPromises` behavior/validation; +- scheduling/context: zlib, HTTP/HTTPS keep-alive reuse and concurrent clients, + net callback/data isolation, dgram, subprocess, worker, VM, dynamic import, + readline, events.on, and stream.finished boundaries; +- callback contract: several async crypto APIs invoke their callback before the + call returns, while prime callbacks do not settle; +- resource/storage semantics: AsyncResource and AsyncLocalStorage native-class + subclassing, constructor-call behavior, option getter access/exception + cleanup, detached-method receivers, reflected API metadata, module namespace + descriptors/immutability, self-cleared Immediate metadata, EventEmitterAsyncResource + back-references, snapshot receiver handling, top + execution-resource restoration, disable cleanup, caught async `exit()` + rejection routing, module namespace branding, and EventEmitterAsyncResource + prototype/getter brand behavior; and +- runtime: after a clean Perry compiler/runtime rebuild, the direct `node:tls` + fixture compiles but its local TLS connection does not settle within the + granular runner's 30-second execution limit. The same certificate fixture + passes the pinned Node oracle. + +## Coverage + +- `resource/`: construction/type and ID invariants, scope/receiver/arguments, + instance/static bind including inferred resource types and explicit receiver, + option getter order and inherited/null-prototype options, detached-method + receiver behavior, native-class subclass identity, + deterministic hook scope callbacks, and explicit destroy. +- `storage/`: run nesting and restoration, independent instances, enterWith, + exit and its async descendants, EventEmitter listener bleed/isolation, + multiple store value types, cross-instance exit isolation, disable/re-entry, + repeated disable, all primitive/object `enterWith` store values, self-cleared + Immediate context/handle identity, detached/foreign method receivers, + native-class subclass propagation, and promise-boundary behavior. +- `static/`: AsyncLocalStorage bind/snapshot and AsyncResource.bind context, + empty and populated captures, receiver, argument, return-value, re-entry, and + restoration behavior. +- `propagation/`: controlled concurrent promises, catch/finally, thenables, + thenables returned from async functions and handlers, async iterators, + dynamic import, local fetch, Web Locks, VM scheduling, queueMicrotask, nested nextTick, + immediate, ref/unref, interval, and timer propagation with awaited or + callback-driven completion barriers. +- `integrations/`: individual fs access, mutation, metadata, descriptor, + directory, watch, promises, and stream callbacks; crypto random, KDF, key, + key-pair, and prime callbacks; all major zlib callback/stream families; + Readable/Writable/Transform/finished, timers/promises timeout/immediate/ + interval plus scheduler wait/yield, util.promisify, + util.promisify.custom, EventEmitter, and EventEmitterAsyncResource lifecycle + plus prototype-brand behavior selected from Bun's async-context matrix and + Node's provider tests. +- `hooks/`: enable/disable/re-enable, simultaneous observers, enabling and + disabling observers from `init`, `before`, and `after` or while callbacks and + Promise chains are active, mixed Promise hook shapes, pre-created and + async/await Promise trigger chains, execution-resource identity and writable + metadata propagation, default next-tick triggers, re-entrant destroy queuing, `trackPromises`, + `promiseResolve`, resource arguments, cancelled timer/immediate destruction, + deterministic timer/interval/immediate/microtask/nextTick, nested Timeout + ancestry, callback and promise fs, filesystem watcher and directory handles, + classic/WebCrypto requests, PBKDF2/randomBytes, zlib, BLOBREADER, DNS plus + DNSCHANNEL, child-process PROCESS/PIPE, signal registration, UDP, and local + TCP lifecycles; sibling, + fs.readFile, and provider trigger ancestry; async-wrap provider-table + immutability; and throwing scoped callbacks. +- `providers/`: DNS, child processes, HTTP and HTTPS including keep-alive agent + and concurrent-client isolation, HTTP execution-resource mapping, TLS, net + including concurrent data, dual accept/connect context isolation, and + `getConnections`; dgram including dual send/receive context isolation; DNS + `lookupService`; workers, readline, events.on, and stream async iterators with + local endpoints, ephemeral ports, and explicit close/exit barriers. The HTTP + and worker cases also assert HTTPCLIENTREQUEST/HTTPINCOMINGMESSAGE and + WORKER/MESSAGEPORT execution-resource mapping and balanced lifecycles. This + directory runs in the sequential lane. +- `validation/`: synchronous throw propagation and cleanup of storage and + execution-resource state, constructor-call behavior, option-accessor + exception short-circuiting, plus hook-sensitive empty resource type behavior. + The pre-existing root fixtures retain detailed callback, constructor, and + hook-option argument validation. +- root module fixtures: bare/prefixed/default import identity, complete claimed + export/function metadata, and ESM namespace branding/descriptors/immutability + are isolated so a correct export identity does not hide namespace mismatches. + +## Remaining slow, redundant, or environment-sensitive categories + +The following current upstream selections are not counted as coverage. Each is +kept out for a concrete reason rather than to cap the suite size: + +- Exact numeric async IDs are runtime-specific; only relationships and + restoration invariants are asserted here. +- GC-driven destroy delivery, weak-reference collection, destroy-vs-scheduler + priority, recursive hooks, deep stacks, stress/leak probes, and process + shutdown require slow or timing-sensitive runners. +- Cross-worker storage inheritance is not a Node contract; the retained worker + cases instead check the parent-side `online`, `message`, and `exit` provider + callbacks plus bounded WORKER/MESSAGEPORT lifecycles with an explicitly + terminated local worker. A separate direct MessageChannel lifecycle probe was + rejected because Bun 1.2.18 kept the process alive past 30 seconds even after + listeners were removed and both ports were unrefed and explicitly closed. +- Uncaught exception and unhandled rejection routing mutates process-global + handlers and belongs in a dedicated isolation fixture. +- Bun's DNS CNAME/MX/TXT/reverse cases depend on external resolver state. Local + `lookup`, `resolve4`, and the Node lifecycle selections cover the stable DNS + provider boundary without making network availability an oracle input. +- Bun's crypto cipher/hash/sign/randomUUID selections perform their work + synchronously and only check a following `setImmediate`; that scheduling + behavior is already isolated by the immediate propagation and hook fixtures. +- Node's HTTP parser/socket reuse graphs, exhaustive provider topology, signal + delivery/re-registration, local-domain PIPECONNECT/PIPESERVER graphs, forced + WRITEWRAP backpressure, TTY, process-shutdown, and inspector/trace-event cases + assert native implementation details or depend on a separate transport + boundary. The retained bounded cases cover child stdio PROCESS/PIPE resources, + signal registration/removal, and the stable loopback TCP relationships + selected by Node's own tests. A local-domain socket probe currently fails in + Perry with `Can't assign requested address (os error 49)`, while the forced + 8 MiB WRITEWRAP probe leaves its write callback unsettled past 30 seconds. +- Node's HTTP/2 ALS selection cannot yet reach an async-context callback in + Perry because its local plaintext client never emits `connect`; it belongs + after the underlying `node:http2` provider can complete a loopback session. +- Bun's ReadableStream `cancel` selection expects captured context while Node + 26.5.0 reports `undefined`. Its `pull` selection matches Node, but activates a + separate Web Streams feature whose cold Perry build exceeds the granular + runner's compile budget; both belong with Web Streams compatibility rather + than this Node module suite. +- Bun's cipher-stream context selection cannot currently reach its ALS + assertions because Perry's base `createCipheriv` stream never emits data/end + and exits with unsettled top-level await; it belongs after crypto stream event + support is functional. +- Node's ARGON2REQUEST and prime-request providers are not retained yet: the + corresponding Perry async `argon2`, `checkPrime`, and `generatePrime` + callbacks do not settle, so provider assertions cannot be reached without + first fixing the separate `node:crypto` callback boundary. +- Node's `UDPSENDWRAP` selection requires the internal + `--test-udp-no-try-send` flag to disable the synchronous fast path. Without + that non-public runner flag Node 26.5.0 emits no send resource, so the portable + suite retains UDP socket lifecycle and dgram callback-context coverage only. +- A current Node-main test expects the removed `AsyncResource.domain` getter, + but the pinned Node 26.5.0 oracle has no such prototype getter; that + post-oracle surface is not counted. +- Node 26.5.0 supports AsyncLocalStorage `defaultValue`, `name`, and `withScope`, + but Perry's API manifest does not claim those constructor/property/scope + surfaces, so they are not counted here. + +Raw hook coverage remains deliberately bounded. User-created AsyncResource +cases control init/before/callback/after/destroy directly. Selected built-in +provider cases are retained only where the pinned Node implementation or suite +supplies a stable contract and the operation is local and deterministic: +interval and nextTick, +fs.readFile and fs promises/watchers, Blob reads, DNS channel plus +lookup/lookupService/localhost query, directory handles, randomBytes and other +classic/WebCrypto requests, zlib, child stdio, bounded worker and HTTP +providers, signal registration, UDP socket, and a bounded loopback TCP matrix. +All use explicit completion barriers and assert relationships rather than raw +IDs; GC timing and exhaustive native topology graphs remain excluded. diff --git a/test-parity/node-suite/async_hooks/api-function-metadata.ts b/test-parity/node-suite/async_hooks/api-function-metadata.ts new file mode 100644 index 0000000000..f57bae1ce0 --- /dev/null +++ b/test-parity/node-suite/async_hooks/api-function-metadata.ts @@ -0,0 +1,53 @@ +import { + AsyncLocalStorage, + AsyncResource, + createHook, + executionAsyncId, + executionAsyncResource, + triggerAsyncId, +} from "node:async_hooks"; + +function metadata(entries: Array<[string, unknown]>) { + return entries + .map(([label, value]) => + typeof value === "function" + ? `${label}:${value.name}/${value.length}` + : `${label}:missing`, + ) + .join("|"); +} + +console.log( + "module function metadata:", + metadata([ + ["createHook", createHook], + ["executionAsyncId", executionAsyncId], + ["executionAsyncResource", executionAsyncResource], + ["triggerAsyncId", triggerAsyncId], + ]), +); +console.log( + "storage function metadata:", + metadata([ + ["constructor", AsyncLocalStorage], + ["bind", AsyncLocalStorage.bind], + ["snapshot", AsyncLocalStorage.snapshot], + ["run", AsyncLocalStorage.prototype.run], + ["getStore", AsyncLocalStorage.prototype.getStore], + ["enterWith", AsyncLocalStorage.prototype.enterWith], + ["exit", AsyncLocalStorage.prototype.exit], + ["disable", AsyncLocalStorage.prototype.disable], + ]), +); +console.log( + "resource function metadata:", + metadata([ + ["constructor", AsyncResource], + ["staticBind", AsyncResource.bind], + ["asyncId", AsyncResource.prototype.asyncId], + ["triggerAsyncId", AsyncResource.prototype.triggerAsyncId], + ["emitDestroy", AsyncResource.prototype.emitDestroy], + ["runInAsyncScope", AsyncResource.prototype.runInAsyncScope], + ["bind", AsyncResource.prototype.bind], + ]), +); diff --git a/test-parity/node-suite/async_hooks/hooks/async-await-trigger-chain.ts b/test-parity/node-suite/async_hooks/hooks/async-await-trigger-chain.ts new file mode 100644 index 0000000000..7529eca772 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/async-await-trigger-chain.ts @@ -0,0 +1,51 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +const triggers = new Map(); +const resources = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "PROMISE") { + triggers.set(asyncId, triggerAsyncId); + resources.set(asyncId, resource); + } + }, +}).enable(); + +let firstContinuationId = -1; +let secondContinuationId = -1; +async function exercise() { + await null; + firstContinuationId = executionAsyncId(); + await Promise.resolve("ready"); + secondContinuationId = executionAsyncId(); +} + +await exercise(); +function descendsFrom(asyncId: number, ancestorId: number) { + const visited = new Set(); + let current = asyncId; + while (current > 0 && !visited.has(current)) { + if (current === ancestorId) return true; + visited.add(current); + current = triggers.get(current) ?? -1; + } + return false; +} + +console.log( + "async-await continuation ids:", + firstContinuationId > 0, + secondContinuationId > 0, + firstContinuationId !== secondContinuationId, +); +console.log( + "async-await resources supplied:", + resources.has(firstContinuationId), + resources.has(secondContinuationId), +); +console.log( + "async-await trigger chain:", + (triggers.get(firstContinuationId) ?? -1) > 0, + descendsFrom(secondContinuationId, firstContinuationId), +); +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/hooks/async-wrap-providers-table.ts b/test-parity/node-suite/async_hooks/hooks/async-wrap-providers-table.ts new file mode 100644 index 0000000000..610e27f0f9 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/async-wrap-providers-table.ts @@ -0,0 +1,30 @@ +import { asyncWrapProviders } from "node:async_hooks"; + +const keys = Object.keys(asyncWrapProviders); +const values = keys.map((key) => (asyncWrapProviders as any)[key]); +const first = keys[0]; +const original = (asyncWrapProviders as any)[first]; +let mutation = "ok"; +try { + (asyncWrapProviders as any)[first] = original + 1; +} catch (error: any) { + mutation = `${error.name}:${error.code || "no-code"}`; +} +console.log( + "provider table shape:", + typeof asyncWrapProviders, + Object.getPrototypeOf(asyncWrapProviders) === null, + Object.isFrozen(asyncWrapProviders), +); +console.log( + "provider table values:", + keys.length > 0, + values.every((value) => Number.isInteger(value) && value >= 0), + new Set(values).size === values.length, + asyncWrapProviders.NONE === 0, +); +console.log( + "provider table mutation:", + mutation, + (asyncWrapProviders as any)[first] === original, +); diff --git a/test-parity/node-suite/async_hooks/hooks/cancelled-immediate-destroy.ts b/test-parity/node-suite/async_hooks/hooks/cancelled-immediate-destroy.ts new file mode 100644 index 0000000000..fca1c2fe19 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/cancelled-immediate-destroy.ts @@ -0,0 +1,34 @@ +import { createHook } from "node:async_hooks"; + +let target = -1; +let observedResource: object | undefined; +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, _triggerAsyncId, resource) { + if (type === "Immediate" && target === -1) { + target = asyncId; + observedResource = resource; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === target) events.push("before"); + }, + after(asyncId) { + if (asyncId === target) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === target) events.push("destroy"); + }, +}).enable(); + +const immediate = setImmediate(() => events.push("callback")); +clearImmediate(immediate); +await new Promise((resolve) => setTimeout(resolve, 0)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +console.log( + "cancelled immediate resource identity:", + observedResource === immediate, +); +console.log("cancelled immediate lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/cancelled-timeout-destroy.ts b/test-parity/node-suite/async_hooks/hooks/cancelled-timeout-destroy.ts new file mode 100644 index 0000000000..8a53879998 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/cancelled-timeout-destroy.ts @@ -0,0 +1,34 @@ +import { createHook } from "node:async_hooks"; + +let target = -1; +let observedResource: object | undefined; +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, _triggerAsyncId, resource) { + if (type === "Timeout" && target === -1) { + target = asyncId; + observedResource = resource; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === target) events.push("before"); + }, + after(asyncId) { + if (asyncId === target) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === target) events.push("destroy"); + }, +}).enable(); + +const timeout = setTimeout(() => events.push("callback"), 1000); +clearTimeout(timeout); +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +console.log( + "cancelled timeout resource identity:", + observedResource === timeout, +); +console.log("cancelled timeout lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/destroy-enqueues-destroy.ts b/test-parity/node-suite/async_hooks/hooks/destroy-enqueues-destroy.ts new file mode 100644 index 0000000000..d0cce57d4c --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/destroy-enqueues-destroy.ts @@ -0,0 +1,32 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +const targetIds = new Set(); +const destroyed: number[] = []; +let secondResource!: AsyncResource; +const hook = createHook({ + init(asyncId, type) { + if (type === "DestroyQueueResource") targetIds.add(asyncId); + }, + destroy(asyncId) { + if (!targetIds.has(asyncId)) return; + destroyed.push(asyncId); + if (destroyed.length === 1) secondResource.emitDestroy(); + }, +}).enable(); + +const firstResource = new AsyncResource("DestroyQueueResource"); +secondResource = new AsyncResource("DestroyQueueResource"); +const firstId = firstResource.asyncId(); +const secondId = secondResource.asyncId(); +firstResource.emitDestroy(); + +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +console.log( + "destroy queue identities:", + targetIds.size === 2, + destroyed[0] === firstId, + destroyed[1] === secondId, +); +console.log("destroy queue counts:", destroyed.length, new Set(destroyed).size); +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/hooks/disable-during-promise.ts b/test-parity/node-suite/async_hooks/hooks/disable-during-promise.ts new file mode 100644 index 0000000000..e324cd12a6 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/disable-during-promise.ts @@ -0,0 +1,32 @@ +import { createHook } from "node:async_hooks"; + +const promiseIds = new Set(); +let initCount = 0; +let beforeCount = 0; +let afterCount = 0; +const hook = createHook({ + init(asyncId, type) { + if (type === "PROMISE") { + promiseIds.add(asyncId); + initCount++; + } + }, + before(asyncId) { + if (promiseIds.has(asyncId)) beforeCount++; + }, + after(asyncId) { + if (promiseIds.has(asyncId)) afterCount++; + }, +}).enable(); + +Promise.resolve(1).then(() => { + hook.disable(); + Promise.resolve(42).then(() => {}); + process.nextTick(() => {}); +}); + +setImmediate(() => { + console.log("disable during promise init:", initCount); + console.log("disable during promise before:", beforeCount); + console.log("disable during promise after:", afterCount); +}); diff --git a/test-parity/node-suite/async_hooks/hooks/disable-in-init.ts b/test-parity/node-suite/async_hooks/hooks/disable-in-init.ts new file mode 100644 index 0000000000..8ed3979395 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/disable-in-init.ts @@ -0,0 +1,40 @@ +import { createHook } from "node:async_hooks"; +import { access } from "node:fs"; + +let outerInits = 0; +let nestedInits = 0; +let scheduled = false; +let pending = 1; +let finish!: () => void; +const completed = new Promise((resolve) => { + finish = resolve; +}); +function done(error: NodeJS.ErrnoException | null) { + if (error) throw error; + if (--pending === 0) finish(); +} + +const outer = createHook({ + init(_asyncId, type) { + if (type !== "FSREQCALLBACK") return; + outerInits++; + nested.disable(); + if (!scheduled) { + scheduled = true; + pending++; + access(import.meta.filename, done); + } + }, +}).enable(); +const nested = createHook({ + init(_asyncId, type) { + if (type === "FSREQCALLBACK") nestedInits++; + }, +}).enable(); + +access(import.meta.filename, done); +await completed; +outer.disable(); +nested.disable(); +console.log("disable in init outer:", outerInits); +console.log("disable in init nested:", nestedInits); diff --git a/test-parity/node-suite/async_hooks/hooks/enable-before-promise-resolve.ts b/test-parity/node-suite/async_hooks/hooks/enable-before-promise-resolve.ts new file mode 100644 index 0000000000..07a855a41e --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/enable-before-promise-resolve.ts @@ -0,0 +1,35 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +let resolverExecutionId = -1; +let continuationExecutionId = -1; +const afterIds = new Set(); +const hook = createHook({ + after(asyncId) { + afterIds.add(asyncId); + }, +}); + +const promise = new Promise((resolve) => { + setTimeout(() => { + resolverExecutionId = executionAsyncId(); + hook.enable(); + resolve(); + }, 0); +}); + +await promise.then(() => { + continuationExecutionId = executionAsyncId(); +}); +await new Promise((resolve) => setImmediate(resolve)); + +console.log( + "enabled-before-resolve continuation:", + resolverExecutionId > 0, + continuationExecutionId > 0, + continuationExecutionId !== resolverExecutionId, +); +console.log( + "enabled-before-resolve after:", + afterIds.has(continuationExecutionId), +); +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/hooks/enable-disable.ts b/test-parity/node-suite/async_hooks/hooks/enable-disable.ts new file mode 100644 index 0000000000..84c58a9093 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/enable-disable.ts @@ -0,0 +1,23 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +let initCount = 0; +const hook = createHook({ + init(asyncId, type) { + if (type === "ParityEnableDisable") initCount++; + }, +}); + +console.log("first enable identity:", hook.enable() === hook); +console.log("second enable identity:", hook.enable() === hook); +new AsyncResource("ParityEnableDisable").emitDestroy(); +console.log("enabled init count:", initCount); + +console.log("first disable identity:", hook.disable() === hook); +console.log("second disable identity:", hook.disable() === hook); +new AsyncResource("ParityEnableDisable").emitDestroy(); +console.log("disabled init count:", initCount); + +hook.enable(); +new AsyncResource("ParityEnableDisable").emitDestroy(); +hook.disable(); +console.log("re-enabled init count:", initCount); diff --git a/test-parity/node-suite/async_hooks/hooks/enable-during-promise.ts b/test-parity/node-suite/async_hooks/hooks/enable-during-promise.ts new file mode 100644 index 0000000000..a92c223fbb --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/enable-during-promise.ts @@ -0,0 +1,30 @@ +import { createHook } from "node:async_hooks"; + +Promise.resolve().then(() => { + const tickIds = new Set(); + let initCount = 0; + let beforeCount = 0; + let afterCount = 0; + const hook = createHook({ + init(asyncId, type) { + if (type === "TickObject") { + tickIds.add(asyncId); + initCount++; + } + }, + before(asyncId) { + if (tickIds.has(asyncId)) beforeCount++; + }, + after(asyncId) { + if (tickIds.has(asyncId)) afterCount++; + }, + }).enable(); + + process.nextTick(() => {}); + setImmediate(() => { + hook.disable(); + console.log("enable during promise tick init:", initCount); + console.log("enable during promise tick before:", beforeCount); + console.log("enable during promise tick after:", afterCount); + }); +}); diff --git a/test-parity/node-suite/async_hooks/hooks/enable-in-init.ts b/test-parity/node-suite/async_hooks/hooks/enable-in-init.ts new file mode 100644 index 0000000000..56a662bf9c --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/enable-in-init.ts @@ -0,0 +1,40 @@ +import { createHook } from "node:async_hooks"; +import { access } from "node:fs"; + +let outerInits = 0; +let nestedInits = 0; +let scheduled = false; +let pending = 1; +let finish!: () => void; +const completed = new Promise((resolve) => { + finish = resolve; +}); +function done(error: NodeJS.ErrnoException | null) { + if (error) throw error; + if (--pending === 0) finish(); +} + +const nested = createHook({ + init(_asyncId, type) { + if (type === "FSREQCALLBACK") nestedInits++; + }, +}); +const outer = createHook({ + init(_asyncId, type) { + if (type !== "FSREQCALLBACK") return; + outerInits++; + nested.enable(); + if (!scheduled) { + scheduled = true; + pending++; + access(import.meta.filename, done); + } + }, +}).enable(); + +access(import.meta.filename, done); +await completed; +outer.disable(); +nested.disable(); +console.log("enable in init outer:", outerInits); +console.log("enable in init nested:", nestedInits); diff --git a/test-parity/node-suite/async_hooks/hooks/execution-resource-mapping.ts b/test-parity/node-suite/async_hooks/hooks/execution-resource-mapping.ts new file mode 100644 index 0000000000..51a2011768 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/execution-resource-mapping.ts @@ -0,0 +1,43 @@ +import { + AsyncResource, + createHook, + executionAsyncResource, +} from "node:async_hooks"; + +const resources = new Map(); +let targetId = -1; +let beforeMatches = false; +let callbackMatches = false; +let afterMatches = false; +const hook = createHook({ + init(asyncId, _type, _triggerAsyncId, resource) { + resources.set(asyncId, resource); + }, + before(asyncId) { + if (asyncId === targetId) { + beforeMatches = executionAsyncResource() === resources.get(asyncId); + } + }, + after(asyncId) { + if (asyncId === targetId) { + afterMatches = executionAsyncResource() === resources.get(asyncId); + } + }, +}).enable(); + +const resource = new AsyncResource("MappingResource"); +targetId = resource.asyncId(); +const initMatches = resources.get(targetId) === resource; +resource.runInAsyncScope(() => { + callbackMatches = executionAsyncResource() === resource; +}); + +console.log( + "execution resource mapping:", + initMatches, + beforeMatches, + callbackMatches, + afterMatches, +); +resource.emitDestroy(); +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/hooks/execution-resource-property-propagation.ts b/test-parity/node-suite/async_hooks/hooks/execution-resource-property-propagation.ts new file mode 100644 index 0000000000..721d2c2377 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/execution-resource-property-propagation.ts @@ -0,0 +1,36 @@ +import { + AsyncResource, + createHook, + executionAsyncResource, +} from "node:async_hooks"; + +const context = Symbol("context"); +type ContextResource = object & { [context]?: string }; +const hook = createHook({ + init(_asyncId, _type, _triggerAsyncId, resource) { + const current = executionAsyncResource() as ContextResource; + (resource as ContextResource)[context] = current[context]; + }, +}).enable(); + +async function exercise(label: string) { + await new Promise((resolve) => setImmediate(resolve)); + await Promise.resolve(); + return (executionAsyncResource() as ContextResource)[context]; +} + +async function start(label: string) { + const resource = new AsyncResource(`Property-${label}`) as AsyncResource & + ContextResource; + resource[context] = label; + const promise = resource.runInAsyncScope(() => exercise(label)); + try { + return await promise; + } finally { + resource.emitDestroy(); + } +} + +const values = await Promise.all([start("first"), start("second")]); +console.log("execution resource propagated values:", values.join(",")); +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/hooks/fs-readfile-trigger-chain.ts b/test-parity/node-suite/async_hooks/hooks/fs-readfile-trigger-chain.ts new file mode 100644 index 0000000000..0393adcae5 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/fs-readfile-trigger-chain.ts @@ -0,0 +1,69 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { readFile, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const path = join(tmpdir(), "perry-async-hooks-readfile-chain.txt"); +rmSync(path, { force: true }); +writeFileSync(path, "read-file-chain"); +const parentId = executionAsyncId(); +type Entry = { + asyncId: number; + triggerAsyncId: number; + before: number; + after: number; + destroy: number; +}; +const entries: Entry[] = []; +const byId = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type !== "FSREQCALLBACK") return; + const entry = { asyncId, triggerAsyncId, before: 0, after: 0, destroy: 0 }; + entries.push(entry); + byId.set(asyncId, entry); + }, + before(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.before++; + }, + after(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.after++; + }, + destroy(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.destroy++; + }, +}).enable(); + +let result = ""; +try { + result = await new Promise((resolve, reject) => { + readFile(path, "utf8", (error, data) => + error ? reject(error) : resolve(data), + ); + }); + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} finally { + hook.disable(); + rmSync(path, { force: true }); +} + +console.log("readFile hook result/count:", result, entries.length); +console.log( + "readFile hook trigger chain:", + entries.length > 0 && + entries.every((entry, index) => + index === 0 + ? entry.triggerAsyncId === parentId + : entry.triggerAsyncId === entries[index - 1].asyncId, + ), +); +console.log( + "readFile hook lifecycles:", + entries + .map((entry) => `${entry.before}/${entry.after}/${entry.destroy}`) + .join(","), +); diff --git a/test-parity/node-suite/async_hooks/hooks/immediate-nested-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/immediate-nested-lifecycle.ts new file mode 100644 index 0000000000..29ca48b5ae --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/immediate-nested-lifecycle.ts @@ -0,0 +1,55 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +const parentId = executionAsyncId(); +type Entry = { + asyncId: number; + triggerAsyncId: number; + resource: object; + events: string[]; +}; +const entries: Entry[] = []; +const byId = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type !== "Immediate" || entries.length >= 2) return; + const entry = { asyncId, triggerAsyncId, resource, events: ["init"] }; + entries.push(entry); + byId.set(asyncId, entry); + }, + before(asyncId) { + byId.get(asyncId)?.events.push("before"); + }, + after(asyncId) { + byId.get(asyncId)?.events.push("after"); + }, + destroy(asyncId) { + byId.get(asyncId)?.events.push("destroy"); + }, +}).enable(); + +let firstHandle: ReturnType | undefined; +let secondHandle: ReturnType | undefined; +await new Promise((resolve) => { + firstHandle = setImmediate(() => { + secondHandle = setImmediate(resolve); + }); +}); +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); + +console.log("nested immediate count:", entries.length); +console.log( + "nested immediate resources:", + entries.length === 2 && entries[0].resource === firstHandle, + entries.length === 2 && entries[1].resource === secondHandle, +); +console.log( + "nested immediate triggers:", + entries.length === 2 && entries[0].triggerAsyncId === parentId, + entries.length === 2 && entries[1].triggerAsyncId === entries[0].asyncId, +); +console.log( + "nested immediate lifecycles:", + entries.map((entry) => entry.events.join(">")).join(","), +); diff --git a/test-parity/node-suite/async_hooks/hooks/interval-repeat-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/interval-repeat-lifecycle.ts new file mode 100644 index 0000000000..d68fa214b9 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/interval-repeat-lifecycle.ts @@ -0,0 +1,50 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +const parentId = executionAsyncId(); +let targetId = -1; +let targetTriggerId = -1; +let targetResource: object | undefined; +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "Timeout" && targetId === -1) { + targetId = asyncId; + targetTriggerId = triggerAsyncId; + targetResource = resource; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === targetId) events.push("before"); + }, + after(asyncId) { + if (asyncId === targetId) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === targetId) events.push("destroy"); + }, +}).enable(); + +let interval: ReturnType; +let calls = 0; +await new Promise((resolve) => { + interval = setInterval(() => { + calls++; + if (calls === 2) { + clearInterval(interval); + resolve(); + } + }, 1); +}); +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); + +console.log( + "interval resource relationship:", + targetId > 0, + targetTriggerId === parentId, + targetResource === interval, +); +console.log("interval repeated calls:", calls); +console.log("interval repeated lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/late-enable-immediate.ts b/test-parity/node-suite/async_hooks/hooks/late-enable-immediate.ts new file mode 100644 index 0000000000..941c91507e --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/late-enable-immediate.ts @@ -0,0 +1,24 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +let targetId = -1; +let afterSeen = false; +let destroySeen = false; +const hook = createHook({ + after(asyncId) { + if (asyncId === targetId) afterSeen = true; + }, + destroy(asyncId) { + if (asyncId === targetId) destroySeen = true; + }, +}); + +setImmediate(() => { + targetId = executionAsyncId(); + hook.enable(); + setImmediate(() => + setImmediate(() => { + console.log("late immediate after/destroy:", afterSeen, destroySeen); + hook.disable(); + }), + ); +}); diff --git a/test-parity/node-suite/async_hooks/hooks/late-enable-nexttick.ts b/test-parity/node-suite/async_hooks/hooks/late-enable-nexttick.ts new file mode 100644 index 0000000000..2f989db712 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/late-enable-nexttick.ts @@ -0,0 +1,24 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +let targetId = -1; +let afterSeen = false; +let destroySeen = false; +const hook = createHook({ + after(asyncId) { + if (asyncId === targetId) afterSeen = true; + }, + destroy(asyncId) { + if (asyncId === targetId) destroySeen = true; + }, +}); + +process.nextTick(() => { + targetId = executionAsyncId(); + hook.enable(); + setImmediate(() => + setImmediate(() => { + console.log("late nextTick after/destroy:", afterSeen, destroySeen); + hook.disable(); + }), + ); +}); diff --git a/test-parity/node-suite/async_hooks/hooks/late-enable-timer.ts b/test-parity/node-suite/async_hooks/hooks/late-enable-timer.ts new file mode 100644 index 0000000000..a0ad2e8f87 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/late-enable-timer.ts @@ -0,0 +1,24 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +let targetId = -1; +let afterSeen = false; +let destroySeen = false; +const hook = createHook({ + after(asyncId) { + if (asyncId === targetId) afterSeen = true; + }, + destroy(asyncId) { + if (asyncId === targetId) destroySeen = true; + }, +}); + +setTimeout(() => { + targetId = executionAsyncId(); + hook.enable(); + setImmediate(() => + setImmediate(() => { + console.log("late timer after/destroy:", afterSeen, destroySeen); + hook.disable(); + }), + ); +}, 0); diff --git a/test-parity/node-suite/async_hooks/hooks/multiple-hooks.ts b/test-parity/node-suite/async_hooks/hooks/multiple-hooks.ts new file mode 100644 index 0000000000..95cd8a8f31 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/multiple-hooks.ts @@ -0,0 +1,42 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +const firstEvents: string[] = []; +const secondEvents: string[] = []; +let targetId = -1; + +function createTrackingHook(events: string[]) { + return createHook({ + init(asyncId, type) { + if (type === "ParityMultipleHooks") { + targetId = asyncId; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === targetId) events.push("before"); + }, + after(asyncId) { + if (asyncId === targetId) events.push("after"); + }, + }).enable(); +} + +const first = createTrackingHook(firstEvents); +const second = createTrackingHook(secondEvents); +const resource = new AsyncResource("ParityMultipleHooks"); + +resource.runInAsyncScope(() => { + console.log("both hooks callback"); +}); +console.log("first hook events:", firstEvents.join(">")); +console.log("second hook events:", secondEvents.join(">")); + +first.disable(); +resource.runInAsyncScope(() => { + console.log("second hook callback"); +}); +console.log("first hook after disable:", firstEvents.join(">")); +console.log("second hook after first disable:", secondEvents.join(">")); + +second.disable(); +resource.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/hooks/multiple-promise-hook-shapes.ts b/test-parity/node-suite/async_hooks/hooks/multiple-promise-hook-shapes.ts new file mode 100644 index 0000000000..9c343c05b6 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/multiple-promise-hook-shapes.ts @@ -0,0 +1,64 @@ +import { createHook } from "node:async_hooks"; + +type Lifecycle = { + init: number; + before: number; + after: number; + resolve: number; +}; +const ids = new WeakMap(); +const lifecycle = new Map(); +const firstHook = createHook({ + init(asyncId, type, _triggerAsyncId, resource) { + if (type !== "PROMISE") return; + ids.set(resource, asyncId); + lifecycle.set(asyncId, { init: 1, before: 0, after: 0, resolve: 0 }); + }, + before(asyncId) { + const state = lifecycle.get(asyncId); + if (state) state.before++; + }, + after(asyncId) { + const state = lifecycle.get(asyncId); + if (state) state.after++; + }, + promiseResolve(asyncId) { + const state = lifecycle.get(asyncId); + if (state) state.resolve++; + }, +}).enable(); +const secondHook = createHook({ + init() {}, + destroy() {}, +}).enable(); + +const parent = Promise.resolve(41); +const child = parent.then((value) => value + 1); +const value = await child; +await new Promise((resolve) => setImmediate(resolve)); + +const parentId = ids.get(parent) ?? -1; +const childId = ids.get(child) ?? -1; +const parentState = lifecycle.get(parentId); +const childState = lifecycle.get(childId); +console.log( + "multiple promise hook identities:", + parentId > 0, + childId > 0, + parentId !== childId, +); +console.log( + "multiple promise parent lifecycle:", + parentState?.init === 1, + parentState?.resolve === 1, +); +console.log( + "multiple promise child lifecycle:", + childState?.init === 1, + childState?.before === 1, + childState?.after === 1, + childState?.resolve === 1, + value, +); +firstHook.disable(); +secondHook.disable(); diff --git a/test-parity/node-suite/async_hooks/hooks/mutate-during-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/mutate-during-lifecycle.ts new file mode 100644 index 0000000000..187082ed44 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/mutate-during-lifecycle.ts @@ -0,0 +1,75 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +let targetId = -1; +let phase = 0; +const firstEvents: string[] = []; +const secondEvents: string[] = []; +const thirdEvents: string[] = []; + +const first = createHook({ + init(asyncId, type) { + if (type === "ParityHookMutation") { + targetId = asyncId; + firstEvents.push("init"); + } + }, + before(asyncId) { + if (asyncId === targetId) firstEvents.push(`before-${phase}`); + }, + after(asyncId) { + if (asyncId === targetId) firstEvents.push(`after-${phase}`); + }, + destroy(asyncId) { + if (asyncId === targetId) firstEvents.push("destroy"); + }, +}); +const second = createHook({ + init(asyncId, type) { + if (type === "ParityHookMutation") secondEvents.push("init"); + }, + before(asyncId) { + if (asyncId !== targetId) return; + secondEvents.push(`before-${phase}`); + if (phase === 2) first.disable(); + }, + after(asyncId) { + if (asyncId === targetId) secondEvents.push(`after-${phase}`); + }, + destroy(asyncId) { + if (asyncId === targetId) secondEvents.push("destroy"); + }, +}); +const third = createHook({ + init(asyncId, type) { + if (type === "ParityHookMutation") thirdEvents.push("init"); + }, + before(asyncId) { + if (asyncId !== targetId) return; + thirdEvents.push(`before-${phase}`); + if (phase === 1) second.enable(); + }, + after(asyncId) { + if (asyncId !== targetId) return; + thirdEvents.push(`after-${phase}`); + if (phase === 2) third.disable(); + }, + destroy(asyncId) { + if (asyncId === targetId) thirdEvents.push("destroy"); + }, +}); + +first.enable(); +third.enable(); +const resource = new AsyncResource("ParityHookMutation"); +phase = 1; +resource.runInAsyncScope(() => {}); +phase = 2; +resource.runInAsyncScope(() => {}); +resource.emitDestroy(); +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +second.disable(); + +console.log("mutated first hook:", firstEvents.join(">")); +console.log("mutated second hook:", secondEvents.join(">")); +console.log("mutated third hook:", thirdEvents.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/nested-timeout-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/nested-timeout-lifecycle.ts new file mode 100644 index 0000000000..dd4478b5a3 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/nested-timeout-lifecycle.ts @@ -0,0 +1,72 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +type Activity = { + id: number; + trigger: number; + events: string[]; + resource: object; +}; +const activities: Activity[] = []; +const byId = new Map(); +const root = executionAsyncId(); +let accepting = true; +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (accepting && type === "Timeout") { + const activity = { + id: asyncId, + trigger: triggerAsyncId, + events: ["init"], + resource, + }; + activities.push(activity); + byId.set(asyncId, activity); + } + }, + before(asyncId) { + byId.get(asyncId)?.events.push("before"); + }, + after(asyncId) { + byId.get(asyncId)?.events.push("after"); + }, + destroy(asyncId) { + byId.get(asyncId)?.events.push("destroy"); + }, +}).enable(); + +const handles: object[] = []; +await new Promise((resolve) => { + handles.push( + setTimeout(() => { + handles.push( + setTimeout(() => { + handles.push( + setTimeout(() => { + accepting = false; + resolve(); + }, 0), + ); + }, 0), + ); + }, 0), + ); +}); +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +console.log("timeout count:", activities.length); +console.log( + "timeout resources:", + activities.length === 3 && + activities.every((a, i) => a.resource === handles[i]), +); +console.log( + "timeout trigger chain:", + activities[0]?.trigger === root, + activities[1]?.trigger === activities[0]?.id, + activities[2]?.trigger === activities[1]?.id, +); +console.log( + "timeout lifecycles:", + activities.map((a) => a.events.join(">")).join("|"), +); diff --git a/test-parity/node-suite/async_hooks/hooks/nexttick-default-trigger.ts b/test-parity/node-suite/async_hooks/hooks/nexttick-default-trigger.ts new file mode 100644 index 0000000000..9a1bd94b4d --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/nexttick-default-trigger.ts @@ -0,0 +1,42 @@ +import { createHook, executionAsyncId, triggerAsyncId } from "node:async_hooks"; + +const parentId = executionAsyncId(); +let tickId = -1; +let initTrigger = -1; +let beforeCount = 0; +let afterCount = 0; +let destroyCount = 0; +const hook = createHook({ + init(asyncId, type, triggerId) { + if (type === "TickObject" && tickId === -1) { + tickId = asyncId; + initTrigger = triggerId; + } + }, + before(asyncId) { + if (asyncId === tickId) beforeCount++; + }, + after(asyncId) { + if (asyncId === tickId) afterCount++; + }, + destroy(asyncId) { + if (asyncId === tickId) destroyCount++; + }, +}).enable(); + +process.nextTick(() => { + console.log( + "nextTick trigger relationships:", + tickId > 0, + initTrigger === parentId, + triggerAsyncId() === parentId, + executionAsyncId() === tickId, + ); +}); + +setImmediate(() => + setImmediate(() => { + console.log("nextTick lifecycle:", beforeCount, afterCount, destroyCount); + hook.disable(); + }), +); diff --git a/test-parity/node-suite/async_hooks/hooks/nexttick-sibling-trigger.ts b/test-parity/node-suite/async_hooks/hooks/nexttick-sibling-trigger.ts new file mode 100644 index 0000000000..221cb92d93 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/nexttick-sibling-trigger.ts @@ -0,0 +1,42 @@ +import { createHook, executionAsyncId, triggerAsyncId } from "node:async_hooks"; + +let outerId = -1; +const children: Array<{ asyncId: number; triggerAsyncId: number }> = []; +const callbackChecks: boolean[] = []; +const hook = createHook({ + init(asyncId, type, parentId) { + if (type !== "TickObject") return; + if (outerId === -1) { + outerId = asyncId; + } else if (parentId === outerId) { + children.push({ asyncId, triggerAsyncId: parentId }); + } + }, +}).enable(); + +await new Promise((resolve) => { + process.nextTick(() => { + let completed = 0; + for (let index = 0; index < 2; index++) { + process.nextTick(() => { + const child = children[index]; + callbackChecks.push( + !!child && + executionAsyncId() === child.asyncId && + triggerAsyncId() === outerId, + ); + if (++completed === 2) resolve(); + }); + } + }); +}); +hook.disable(); + +console.log("nextTick sibling count:", children.length); +console.log( + "nextTick sibling relationships:", + children.length === 2 && + children.every((child) => child.triggerAsyncId === outerId), + children.length === 2 && children[0].asyncId !== children[1].asyncId, + callbackChecks.join(","), +); diff --git a/test-parity/node-suite/async_hooks/hooks/options-accessors.ts b/test-parity/node-suite/async_hooks/hooks/options-accessors.ts new file mode 100644 index 0000000000..5854bc54d0 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/options-accessors.ts @@ -0,0 +1,35 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +const accesses: string[] = []; +const events: string[] = []; +let target = -1; +const prototype = {}; +for (const name of ["init", "before", "after", "destroy", "promiseResolve"]) { + Object.defineProperty(prototype, name, { + get() { + accesses.push(name); + if (name === "init") { + return (asyncId: number, type: string) => { + if (type === "AccessorHookResource") { + target = asyncId; + events.push("init"); + } + }; + } + return (asyncId: number) => { + if (asyncId === target) events.push(name); + }; + }, + }); +} + +const hook = createHook(Object.create(prototype)).enable(); +const resource = new AsyncResource("AccessorHookResource"); +resource.runInAsyncScope(() => events.push("callback")); +resource.emitDestroy(); +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); + +console.log("hook option accessor order:", accesses.join(",")); +console.log("inherited hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/precreated-promise-chain.ts b/test-parity/node-suite/async_hooks/hooks/precreated-promise-chain.ts new file mode 100644 index 0000000000..50be198432 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/precreated-promise-chain.ts @@ -0,0 +1,50 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +let firstExecutionId = -1; +let secondExecutionId = -1; +const beforeIds = new Set(); +const afterIds = new Set(); +let initCount = 0; + +const parent = Promise.resolve(1); +const first = parent.then((value) => { + firstExecutionId = executionAsyncId(); + return value + 1; +}); +first.then((value) => { + secondExecutionId = executionAsyncId(); + return value + 1; +}); +const barrier = setTimeout(() => { + hook.disable(); + console.log("precreated promise init count:", initCount); + console.log( + "precreated promise ids valid:", + firstExecutionId > 0, + secondExecutionId > 0, + firstExecutionId !== secondExecutionId, + ); + console.log( + "precreated promise before delivery:", + beforeIds.has(firstExecutionId), + beforeIds.has(secondExecutionId), + ); + console.log( + "precreated promise after delivery:", + afterIds.has(firstExecutionId), + afterIds.has(secondExecutionId), + ); +}, 0); + +const hook = createHook({ + init() { + initCount++; + }, + before(asyncId) { + beforeIds.add(asyncId); + }, + after(asyncId) { + afterIds.add(asyncId); + }, +}).enable(); +void barrier; diff --git a/test-parity/node-suite/async_hooks/hooks/promise-created-before-enable.ts b/test-parity/node-suite/async_hooks/hooks/promise-created-before-enable.ts new file mode 100644 index 0000000000..1fc70133ef --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/promise-created-before-enable.ts @@ -0,0 +1,49 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +type PromiseInfo = { asyncId: number; triggerAsyncId: number }; +const parent = Promise.resolve("parent"); +const resources = new Map(); +const observedIds = new Set(); +const beforeIds = new Set(); +const afterIds = new Set(); +let executionMatches = false; + +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "PROMISE") { + resources.set(resource, { asyncId, triggerAsyncId }); + observedIds.add(asyncId); + } + }, + before(asyncId) { + beforeIds.add(asyncId); + }, + after(asyncId) { + afterIds.add(asyncId); + }, +}).enable(); + +const child = parent.then((value) => { + executionMatches = executionAsyncId() === resources.get(child)?.asyncId; + return `${value}:child`; +}); +const value = await child; +const childInfo = resources.get(child); +hook.disable(); + +console.log("late hook child supplied:", !!childInfo); +console.log( + "late hook parent init absent:", + !observedIds.has(childInfo?.triggerAsyncId ?? -1), +); +console.log( + "late hook trigger positive:", + (childInfo?.triggerAsyncId ?? -1) > 0, +); +console.log("late hook execution relation:", executionMatches); +console.log( + "late hook child lifecycle:", + beforeIds.has(childInfo?.asyncId ?? -1), + afterIds.has(childInfo?.asyncId ?? -1), +); +console.log("late hook child value:", value); diff --git a/test-parity/node-suite/async_hooks/hooks/promise-enable-disable-selection.ts b/test-parity/node-suite/async_hooks/hooks/promise-enable-disable-selection.ts new file mode 100644 index 0000000000..60f9a8c5d9 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/promise-enable-disable-selection.ts @@ -0,0 +1,27 @@ +import { createHook } from "node:async_hooks"; + +const observed = new WeakSet(); +const hook = createHook({ + init(_asyncId, type, _triggerAsyncId, resource) { + if (type === "PROMISE") observed.add(resource); + }, +}); + +hook.disable(); +const disabledFirst = Promise.resolve("disabled-first"); +hook.enable(); +const enabledFirst = Promise.resolve("enabled-first"); +hook.disable(); +const disabledSecond = Promise.resolve("disabled-second"); +hook.enable(); +const enabledSecond = Promise.resolve("enabled-second"); +hook.disable(); + +await Promise.all([disabledFirst, enabledFirst, disabledSecond, enabledSecond]); +console.log( + "promise enable selection:", + observed.has(disabledFirst), + observed.has(enabledFirst), + observed.has(disabledSecond), + observed.has(enabledSecond), +); diff --git a/test-parity/node-suite/async_hooks/hooks/promise-resolve.ts b/test-parity/node-suite/async_hooks/hooks/promise-resolve.ts new file mode 100644 index 0000000000..891c62c4e7 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/promise-resolve.ts @@ -0,0 +1,23 @@ +import { createHook } from "node:async_hooks"; + +let target = -1; +let observedResource: object | undefined; +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, _triggerAsyncId, resource) { + if (type === "PROMISE" && target === -1) { + target = asyncId; + observedResource = resource; + events.push("init"); + } + }, + promiseResolve(asyncId) { + if (asyncId === target) events.push("resolve"); + }, +}).enable(); + +const promise = Promise.resolve(42); +await promise; +hook.disable(); +console.log("promise hook resource supplied:", observedResource === promise); +console.log("promise hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/promise-trigger-chain.ts b/test-parity/node-suite/async_hooks/hooks/promise-trigger-chain.ts new file mode 100644 index 0000000000..cdf76cc126 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/promise-trigger-chain.ts @@ -0,0 +1,47 @@ +import { createHook, executionAsyncId, triggerAsyncId } from "node:async_hooks"; + +type PromiseInfo = { asyncId: number; triggerAsyncId: number }; +const resources = new Map(); +const beforeIds = new Set(); +const afterIds = new Set(); +let callbackExecutionMatches = false; +let callbackTriggerMatches = false; + +const hook = createHook({ + init(asyncId, type, triggerId, resource) { + if (type === "PROMISE") { + resources.set(resource, { asyncId, triggerAsyncId: triggerId }); + } + }, + before(asyncId) { + beforeIds.add(asyncId); + }, + after(asyncId) { + afterIds.add(asyncId); + }, +}).enable(); + +const parent = Promise.resolve(42); +const child = parent.then((value) => { + const parentInfo = resources.get(parent); + const childInfo = resources.get(child); + callbackExecutionMatches = executionAsyncId() === childInfo?.asyncId; + callbackTriggerMatches = + triggerAsyncId() === parentInfo?.asyncId && + childInfo?.triggerAsyncId === parentInfo?.asyncId; + return value + 1; +}); +const value = await child; +const parentInfo = resources.get(parent); +const childInfo = resources.get(child); +hook.disable(); + +console.log("promise chain resources supplied:", !!parentInfo, !!childInfo); +console.log("promise chain trigger relation:", callbackTriggerMatches); +console.log("promise chain execution relation:", callbackExecutionMatches); +console.log( + "promise chain lifecycle:", + beforeIds.has(childInfo?.asyncId ?? -1), + afterIds.has(childInfo?.asyncId ?? -1), +); +console.log("promise chain value:", value); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-blob-reader-lifecycles.ts b/test-parity/node-suite/async_hooks/hooks/provider-blob-reader-lifecycles.ts new file mode 100644 index 0000000000..c4db650237 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-blob-reader-lifecycles.ts @@ -0,0 +1,54 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +type Entry = { + id: number; + trigger: number; + resource: object; + before: number; + after: number; +}; +const entries: Entry[] = []; +const byId = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type !== "BLOBREADER") return; + const entry = { + id: asyncId, + trigger: triggerAsyncId, + resource, + before: 0, + after: 0, + }; + entries.push(entry); + byId.set(asyncId, entry); + }, + before(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.before++; + }, + after(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.after++; + }, +}).enable(); + +const parent = executionAsyncId(); +const blob = new Blob(["blob-reader"]); +const [text, buffer] = await Promise.all([blob.text(), blob.arrayBuffer()]); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); + +console.log("blob reader results:", text, buffer.byteLength); +console.log( + "blob reader resources:", + entries.length, + entries.length === 2 && entries.every((entry) => entry.id > 0), + entries.length === 2 && + new Set(entries.map((entry) => entry.resource)).size === 2, +); +console.log( + "blob reader relationships:", + entries.length === 2 && entries.every((entry) => entry.trigger === parent), + entries.length === 2 && + entries.every((entry) => entry.before > 0 && entry.before === entry.after), +); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-child-process-lifecycles.ts b/test-parity/node-suite/async_hooks/hooks/provider-child-process-lifecycles.ts new file mode 100644 index 0000000000..8ede44495e --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-child-process-lifecycles.ts @@ -0,0 +1,73 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { spawn } from "node:child_process"; + +type Activity = { id: number; trigger: number; events: string[] }; +const root = executionAsyncId(); +const activities = new Map(); +const byId = new Map(); +for (const type of ["PROCESSWRAP", "PIPEWRAP"]) activities.set(type, []); +let accepting = true; +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (!accepting) return; + const list = activities.get(type); + if (!list) return; + const activity = { id: asyncId, trigger: triggerAsyncId, events: ["init"] }; + list.push(activity); + byId.set(asyncId, activity); + }, + before(asyncId) { + byId.get(asyncId)?.events.push("before"); + }, + after(asyncId) { + byId.get(asyncId)?.events.push("after"); + }, + destroy(asyncId) { + byId.get(asyncId)?.events.push("destroy"); + }, +}).enable(); + +const child = spawn("/bin/sh", ["-c", "printf ok"]); +accepting = false; +child.stdin.end(); +let stdout = ""; +child.stdout.setEncoding("utf8"); +child.stdout.on("data", (chunk) => { + stdout += chunk; +}); +let exitCode: number | null = null; +const completion = new Promise((resolve, reject) => { + child.once("error", reject); + child.once("exit", (code) => { + exitCode = code; + }); + child.once("close", () => resolve()); +}); +await completion; +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +const processes = activities.get("PROCESSWRAP")!; +const pipes = activities.get("PIPEWRAP")!; +console.log("child result:", exitCode, stdout); +console.log("child resources:", processes.length, pipes.length); +console.log( + "child root triggers:", + processes.length === 1 && processes.every((a) => a.trigger === root), + pipes.length === 3 && pipes.every((a) => a.trigger === root), +); +console.log( + "process callbacks:", + processes.length === 1 && + processes.every( + (a) => a.events.includes("before") && a.events.includes("after"), + ), +); +console.log( + "pipe balanced callbacks:", + pipes.length === 3 && + pipes.every( + (a) => + a.events.filter((e) => e === "before").length === + a.events.filter((e) => e === "after").length, + ), +); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-crypto-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-crypto-lifecycle.ts new file mode 100644 index 0000000000..bcfbc13ff6 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-crypto-lifecycle.ts @@ -0,0 +1,35 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { pbkdf2 } from "node:crypto"; + +let target = -1; +let triggerMatches = false; +const root = executionAsyncId(); +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type === "PBKDF2REQUEST" && target === -1) { + target = asyncId; + triggerMatches = triggerAsyncId === root; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === target) events.push("before"); + }, + after(asyncId) { + if (asyncId === target) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === target) events.push("destroy"); + }, +}).enable(); + +await new Promise((resolve, reject) => + pbkdf2("password", "salt", 1, 8, "sha256", (error) => + error ? reject(error) : resolve(), + ), +); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +console.log("crypto hook trigger root:", triggerMatches); +console.log("crypto hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-crypto-request-lifecycles.ts b/test-parity/node-suite/async_hooks/hooks/provider-crypto-request-lifecycles.ts new file mode 100644 index 0000000000..6292ce0cb0 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-crypto-request-lifecycles.ts @@ -0,0 +1,123 @@ +import { + createHook, + executionAsyncId, + executionAsyncResource, +} from "node:async_hooks"; +import { generateKey, generateKeyPair, hkdf, scrypt } from "node:crypto"; + +const types = [ + "DERIVEBITSREQUEST", + "SCRYPTREQUEST", + "KEYGENREQUEST", + "KEYPAIRGENREQUEST", +] as const; +type Type = (typeof types)[number]; +type Entry = { + asyncId: number; + triggerAsyncId: number; + resource: object; + before: number; + after: number; + destroy: number; +}; +const entries = new Map(); +const byId = new Map(); +const expectedParents = new Map(); +const callbackChecks = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (!types.includes(type as Type)) return; + const entry = { + asyncId, + triggerAsyncId, + resource, + before: 0, + after: 0, + destroy: 0, + }; + const selected = entries.get(type as Type) || []; + selected.push(entry); + entries.set(type as Type, selected); + byId.set(asyncId, entry); + }, + before(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.before++; + }, + after(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.after++; + }, + destroy(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.destroy++; + }, +}).enable(); + +function remember(type: Type) { + expectedParents.set(type, executionAsyncId()); +} +function callbackMatches(type: Type) { + const entry = entries.get(type)?.[0]; + callbackChecks.set( + type, + !!entry && + executionAsyncId() === entry.asyncId && + executionAsyncResource() === entry.resource, + ); +} + +try { + remember("DERIVEBITSREQUEST"); + await new Promise((resolve, reject) => { + hkdf("sha256", "key", "salt", "info", 8, (error) => { + callbackMatches("DERIVEBITSREQUEST"); + error ? reject(error) : resolve(); + }); + }); + + remember("SCRYPTREQUEST"); + await new Promise((resolve, reject) => { + scrypt("password", "salt", 16, (error) => { + callbackMatches("SCRYPTREQUEST"); + error ? reject(error) : resolve(); + }); + }); + + remember("KEYGENREQUEST"); + await new Promise((resolve, reject) => { + generateKey("hmac", { length: 128 }, (error) => { + callbackMatches("KEYGENREQUEST"); + error ? reject(error) : resolve(); + }); + }); + + remember("KEYPAIRGENREQUEST"); + await new Promise((resolve, reject) => { + generateKeyPair("ed25519", {}, (error) => { + callbackMatches("KEYPAIRGENREQUEST"); + error ? reject(error) : resolve(); + }); + }); + + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} finally { + hook.disable(); +} + +for (const type of types) { + const selected = entries.get(type) || []; + console.log( + `${type} crypto lifecycle:`, + selected.length, + selected.length === 1 && + selected.every( + (entry) => entry.triggerAsyncId === expectedParents.get(type), + ), + callbackChecks.get(type) === true, + selected + .map((entry) => `${entry.before}/${entry.after}/${entry.destroy}`) + .join(","), + ); +} diff --git a/test-parity/node-suite/async_hooks/hooks/provider-dirhandle-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-dirhandle-lifecycle.ts new file mode 100644 index 0000000000..d25980eca7 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-dirhandle-lifecycle.ts @@ -0,0 +1,54 @@ +import { createHook } from "node:async_hooks"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { opendir, type Dir } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +type Entry = { id: number; events: string[]; resource: object }; +const entries: Entry[] = []; +const byId = new Map(); +const path = join(tmpdir(), `perry-async-hooks-dirhandle-${process.pid}`); +rmSync(path, { recursive: true, force: true }); +mkdirSync(path); +writeFileSync(join(path, "entry.txt"), "entry"); +const hook = createHook({ + init(asyncId, type, _triggerAsyncId, resource) { + if (type !== "DIRHANDLE") return; + const entry = { id: asyncId, events: ["init"], resource }; + entries.push(entry); + byId.set(asyncId, entry); + }, + before(asyncId) { + byId.get(asyncId)?.events.push("before"); + }, + after(asyncId) { + byId.get(asyncId)?.events.push("after"); + }, + destroy(asyncId) { + byId.get(asyncId)?.events.push("destroy"); + }, +}).enable(); +let directory: Dir | undefined; +let opened = false; +try { + directory = await opendir(path); + opened = true; + await directory.close(); + directory = undefined; + await new Promise((resolve) => setImmediate(resolve)); +} finally { + if (directory) await directory.close(); + hook.disable(); + rmSync(path, { recursive: true, force: true }); +} +console.log("dirhandle operation:", opened); +console.log( + "dirhandle resources:", + entries.length, + entries.length === 1 && entries[0].id > 0, + entries.length === 1 && typeof entries[0].resource === "object", +); +console.log( + "dirhandle events:", + entries.map((entry) => entry.events.join(">")).join("|"), +); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-dns-channel-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-dns-channel-lifecycle.ts new file mode 100644 index 0000000000..5911a2549d --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-dns-channel-lifecycle.ts @@ -0,0 +1,27 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { Resolver } from "node:dns"; + +type Entry = { id: number; trigger: number; resource: object }; +const entries: Entry[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "DNSCHANNEL") { + entries.push({ id: asyncId, trigger: triggerAsyncId, resource }); + } + }, +}).enable(); + +const parent = executionAsyncId(); +const resolver = new Resolver(); +resolver.setServers(["127.0.0.1"]); +resolver.cancel(); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); + +console.log("dns channel count:", entries.length); +console.log( + "dns channel relationship:", + entries.length === 1 && entries[0].id > 0, + entries.length === 1 && entries[0].trigger === parent, + entries.length === 1 && typeof entries[0].resource === "object", +); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-dns-lookup-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-dns-lookup-lifecycle.ts new file mode 100644 index 0000000000..66d7c5ad90 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-dns-lookup-lifecycle.ts @@ -0,0 +1,62 @@ +import { + createHook, + executionAsyncId, + executionAsyncResource, +} from "node:async_hooks"; +import { lookup } from "node:dns"; + +const parentId = executionAsyncId(); +let targetId = -1; +let targetTriggerId = -1; +let targetResource: object | undefined; +let callbackExecutionMatches = false; +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "GETADDRINFOREQWRAP" && targetId === -1) { + targetId = asyncId; + targetTriggerId = triggerAsyncId; + targetResource = resource; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === targetId) events.push("before"); + }, + after(asyncId) { + if (asyncId === targetId) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === targetId) events.push("destroy"); + }, +}).enable(); + +let callbackInvoked = false; +try { + await new Promise((resolve) => { + lookup("localhost", (error, address) => { + callbackInvoked = true; + callbackExecutionMatches = + executionAsyncId() === targetId && + executionAsyncResource() === targetResource; + console.log( + "dns lookup hook result shape:", + error !== null || typeof address === "string", + ); + resolve(); + }); + }); + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} finally { + hook.disable(); +} + +console.log( + "dns lookup hook relationship:", + callbackInvoked, + targetId > 0, + targetTriggerId === parentId, + callbackExecutionMatches, +); +console.log("dns lookup hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-dns-lookup-service-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-dns-lookup-service-lifecycle.ts new file mode 100644 index 0000000000..5b84dc9a7f --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-dns-lookup-service-lifecycle.ts @@ -0,0 +1,63 @@ +import { + createHook, + executionAsyncId, + executionAsyncResource, +} from "node:async_hooks"; +import { lookupService } from "node:dns"; + +const parentId = executionAsyncId(); +let targetId = -1; +let targetTriggerId = -1; +let targetResource: object | undefined; +let callbackExecutionMatches = false; +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "GETNAMEINFOREQWRAP" && targetId === -1) { + targetId = asyncId; + targetTriggerId = triggerAsyncId; + targetResource = resource; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === targetId) events.push("before"); + }, + after(asyncId) { + if (asyncId === targetId) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === targetId) events.push("destroy"); + }, +}).enable(); + +let callbackInvoked = false; +try { + await new Promise((resolve) => { + lookupService("127.0.0.1", 80, (error, hostname, service) => { + callbackInvoked = true; + callbackExecutionMatches = + executionAsyncId() === targetId && + executionAsyncResource() === targetResource; + console.log( + "lookupService hook result shape:", + error !== null || + (typeof hostname === "string" && typeof service === "string"), + ); + resolve(); + }); + }); + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} finally { + hook.disable(); +} + +console.log( + "lookupService hook relationship:", + callbackInvoked, + targetId > 0, + targetTriggerId === parentId, + callbackExecutionMatches, +); +console.log("lookupService hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-dns-query-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-dns-query-lifecycle.ts new file mode 100644 index 0000000000..700405bdf1 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-dns-query-lifecycle.ts @@ -0,0 +1,62 @@ +import { + createHook, + executionAsyncId, + executionAsyncResource, +} from "node:async_hooks"; +import { resolve4 } from "node:dns"; + +const parentId = executionAsyncId(); +let targetId = -1; +let targetTriggerId = -1; +let targetResource: object | undefined; +let callbackExecutionMatches = false; +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "QUERYWRAP" && targetId === -1) { + targetId = asyncId; + targetTriggerId = triggerAsyncId; + targetResource = resource; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === targetId) events.push("before"); + }, + after(asyncId) { + if (asyncId === targetId) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === targetId) events.push("destroy"); + }, +}).enable(); + +let callbackInvoked = false; +try { + await new Promise((resolve) => { + resolve4("localhost", (error, addresses) => { + callbackInvoked = true; + callbackExecutionMatches = + executionAsyncId() === targetId && + executionAsyncResource() === targetResource; + console.log( + "dns query hook result shape:", + error !== null || Array.isArray(addresses), + ); + resolve(); + }); + }); + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} finally { + hook.disable(); +} + +console.log( + "dns query hook relationship:", + callbackInvoked, + targetId > 0, + targetTriggerId === parentId, + callbackExecutionMatches, +); +console.log("dns query hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-fs-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-fs-lifecycle.ts new file mode 100644 index 0000000000..b5ee7c9b17 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-fs-lifecycle.ts @@ -0,0 +1,33 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { access } from "node:fs"; + +let target = -1; +let triggerMatches = false; +const root = executionAsyncId(); +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type === "FSREQCALLBACK" && target === -1) { + target = asyncId; + triggerMatches = triggerAsyncId === root; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === target) events.push("before"); + }, + after(asyncId) { + if (asyncId === target) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === target) events.push("destroy"); + }, +}).enable(); + +await new Promise((resolve, reject) => + access(import.meta.filename, (error) => (error ? reject(error) : resolve())), +); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +console.log("fs hook trigger root:", triggerMatches); +console.log("fs hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-fs-promises-lifecycles.ts b/test-parity/node-suite/async_hooks/hooks/provider-fs-promises-lifecycles.ts new file mode 100644 index 0000000000..9d1d3d0590 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-fs-promises-lifecycles.ts @@ -0,0 +1,70 @@ +import { createHook } from "node:async_hooks"; +import { rmSync, writeFileSync } from "node:fs"; +import { open, type FileHandle } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const path = join(tmpdir(), "perry-async-hooks-fs-promises-lifecycle.txt"); +rmSync(path, { force: true }); +writeFileSync(path, "abc"); +const tracked = new Set(["FSREQPROMISE", "FILEHANDLE", "FILEHANDLECLOSEREQ"]); +type Entry = { + asyncId: number; + type: string; + before: number; + after: number; + destroy: number; +}; +const entries: Entry[] = []; +const byId = new Map(); +const hook = createHook({ + init(asyncId, type) { + if (!tracked.has(type)) return; + const entry = { asyncId, type, before: 0, after: 0, destroy: 0 }; + entries.push(entry); + byId.set(asyncId, entry); + }, + before(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.before++; + }, + after(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.after++; + }, + destroy(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.destroy++; + }, +}).enable(); + +let handle: FileHandle | undefined; +let content = ""; +let size = -1; +try { + handle = await open(path, "r"); + const buffer = Buffer.alloc(3); + await handle.read(buffer, 0, 3, 0); + content = buffer.toString(); + size = (await handle.stat()).size; + await handle.close(); + handle = undefined; + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} finally { + if (handle) await handle.close(); + hook.disable(); + rmSync(path, { force: true }); +} + +console.log("fs promises hook result:", content, size); +for (const type of ["FSREQPROMISE", "FILEHANDLE", "FILEHANDLECLOSEREQ"]) { + const selected = entries.filter((entry) => entry.type === type); + console.log( + `${type} fs promises lifecycle:`, + selected.length, + selected + .map((entry) => `${entry.before}/${entry.after}/${entry.destroy}`) + .join(","), + ); +} diff --git a/test-parity/node-suite/async_hooks/hooks/provider-fs-watcher-lifecycles.ts b/test-parity/node-suite/async_hooks/hooks/provider-fs-watcher-lifecycles.ts new file mode 100644 index 0000000000..4aa3edb7a7 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-fs-watcher-lifecycles.ts @@ -0,0 +1,72 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { rmSync, unwatchFile, watch, watchFile, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const path = join(tmpdir(), "perry-async-hooks-watcher-lifecycles.txt"); +rmSync(path, { force: true }); +writeFileSync(path, "watchers"); +const parentId = executionAsyncId(); +type Entry = { + asyncId: number; + type: string; + triggerAsyncId: number; + before: number; + after: number; + destroy: number; +}; +const entries: Entry[] = []; +const byId = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type !== "FSEVENTWRAP" && type !== "STATWATCHER") return; + const entry = { + asyncId, + type, + triggerAsyncId, + before: 0, + after: 0, + destroy: 0, + }; + entries.push(entry); + byId.set(asyncId, entry); + }, + before(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.before++; + }, + after(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.after++; + }, + destroy(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.destroy++; + }, +}).enable(); + +try { + const eventWatcher = watch(path, () => {}); + eventWatcher.close(); + watchFile(path, { interval: 20 }, () => {}); + unwatchFile(path); + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} finally { + hook.disable(); + unwatchFile(path); + rmSync(path, { force: true }); +} + +for (const type of ["FSEVENTWRAP", "STATWATCHER"]) { + const selected = entries.filter((entry) => entry.type === type); + console.log( + `${type} watcher lifecycle:`, + selected.length, + selected.length === 1 && + selected.every((entry) => entry.triggerAsyncId === parentId), + selected + .map((entry) => `${entry.before}/${entry.after}/${entry.destroy}`) + .join(","), + ); +} diff --git a/test-parity/node-suite/async_hooks/hooks/provider-microtask-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-microtask-lifecycle.ts new file mode 100644 index 0000000000..3bd2979821 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-microtask-lifecycle.ts @@ -0,0 +1,30 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +let target = -1; +let triggerMatches = false; +const root = executionAsyncId(); +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type === "Microtask" && target === -1) { + target = asyncId; + triggerMatches = triggerAsyncId === root; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === target) events.push("before"); + }, + after(asyncId) { + if (asyncId === target) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === target) events.push("destroy"); + }, +}).enable(); + +await new Promise((resolve) => queueMicrotask(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +console.log("microtask hook trigger root:", triggerMatches); +console.log("microtask hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-net-lifecycle-matrix.ts b/test-parity/node-suite/async_hooks/hooks/provider-net-lifecycle-matrix.ts new file mode 100644 index 0000000000..a21369293f --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-net-lifecycle-matrix.ts @@ -0,0 +1,116 @@ +import { createHook } from "node:async_hooks"; +import { connect, createServer, type Socket } from "node:net"; + +const tracked = new Set([ + "TCPSERVERWRAP", + "TCPWRAP", + "TCPCONNECTWRAP", + "SHUTDOWNWRAP", +]); +type Entry = { + asyncId: number; + type: string; + triggerAsyncId: number; + before: number; + after: number; + destroy: number; +}; +const entries: Entry[] = []; +const byId = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (!tracked.has(type)) return; + const entry = { + asyncId, + type, + triggerAsyncId, + before: 0, + after: 0, + destroy: 0, + }; + entries.push(entry); + byId.set(asyncId, entry); + }, + before(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.before++; + }, + after(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.after++; + }, + destroy(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.destroy++; + }, +}).enable(); + +const serverSockets = new Set(); +const server = createServer((socket) => { + serverSockets.add(socket); + socket.once("close", () => serverSockets.delete(socket)); + socket.end("ok"); +}); +let client: ReturnType | undefined; +try { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + reject(new Error("missing net address")); + return; + } + client = connect(address.port, "127.0.0.1"); + client.once("error", reject); + client.resume(); + client.once("end", resolve); + }); + }); +} finally { + client?.destroy(); + for (const socket of serverSockets) socket.destroy(); + if (server.listening) { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); + hook.disable(); +} + +const serverEntry = entries.find((entry) => entry.type === "TCPSERVERWRAP"); +const tcpEntries = entries.filter((entry) => entry.type === "TCPWRAP"); +const connectEntries = entries.filter( + (entry) => entry.type === "TCPCONNECTWRAP", +); +const shutdownEntries = entries.filter( + (entry) => entry.type === "SHUTDOWNWRAP", +); +const tcpIds = new Set(tcpEntries.map((entry) => entry.asyncId)); +console.log( + "net hook provider counts:", + serverEntry ? 1 : 0, + tcpEntries.length, + connectEntries.length, + shutdownEntries.length, +); +console.log( + "net hook provider ancestry:", + !!serverEntry && + tcpEntries.length === 2 && + tcpEntries.some((entry) => entry.triggerAsyncId === serverEntry.asyncId), + connectEntries.length === 1 && + connectEntries.every((entry) => tcpIds.has(entry.triggerAsyncId)), + shutdownEntries.length === 2 && + shutdownEntries.every((entry) => tcpIds.has(entry.triggerAsyncId)), +); +console.log( + "net hook provider lifecycles:", + entries.length > 0 && + entries.every( + (entry) => + entry.before > 0 && entry.before === entry.after && entry.destroy === 1, + ), +); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-nexttick-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-nexttick-lifecycle.ts new file mode 100644 index 0000000000..ff3fe631f8 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-nexttick-lifecycle.ts @@ -0,0 +1,30 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +let target = -1; +let triggerMatches = false; +const root = executionAsyncId(); +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type === "TickObject" && target === -1) { + target = asyncId; + triggerMatches = triggerAsyncId === root; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === target) events.push("before"); + }, + after(asyncId) { + if (asyncId === target) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === target) events.push("destroy"); + }, +}).enable(); + +await new Promise((resolve) => process.nextTick(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +console.log("nextTick hook trigger root:", triggerMatches); +console.log("nextTick hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-pbkdf2-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-pbkdf2-lifecycle.ts new file mode 100644 index 0000000000..a606ebd467 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-pbkdf2-lifecycle.ts @@ -0,0 +1,48 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { pbkdf2 } from "node:crypto"; + +let requestId = -1; +let triggerId = -1; +let beforeCount = 0; +let afterCount = 0; +let destroyCount = 0; +const parentId = executionAsyncId(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type === "PBKDF2REQUEST") { + requestId = asyncId; + triggerId = triggerAsyncId; + } + }, + before(asyncId) { + if (asyncId === requestId) beforeCount++; + }, + after(asyncId) { + if (asyncId === requestId) afterCount++; + }, + destroy(asyncId) { + if (asyncId === requestId) destroyCount++; + }, +}).enable(); + +const keyLength = await new Promise((resolve, reject) => { + pbkdf2("password", "salt", 1, 20, "sha256", (error, key) => { + if (error) return reject(error); + console.log( + "pbkdf2 callback lifecycle:", + requestId > 0, + triggerId === parentId, + beforeCount, + ); + resolve(key.length); + }); +}); + +await new Promise((resolve) => setImmediate(resolve)); +console.log( + "pbkdf2 completion lifecycle:", + keyLength, + afterCount, + destroyCount, +); +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-random-bytes-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-random-bytes-lifecycle.ts new file mode 100644 index 0000000000..625d9ba5a7 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-random-bytes-lifecycle.ts @@ -0,0 +1,57 @@ +import { + createHook, + executionAsyncId, + executionAsyncResource, +} from "node:async_hooks"; +import { randomBytes } from "node:crypto"; + +const parentId = executionAsyncId(); +let targetId = -1; +let targetTriggerId = -1; +let targetResource: object | undefined; +let callbackExecutionMatches = false; +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "RANDOMBYTESREQUEST" && targetId === -1) { + targetId = asyncId; + targetTriggerId = triggerAsyncId; + targetResource = resource; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === targetId) events.push("before"); + }, + after(asyncId) { + if (asyncId === targetId) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === targetId) events.push("destroy"); + }, +}).enable(); + +let length = -1; +try { + length = await new Promise((resolve, reject) => { + randomBytes(16, (error, data) => { + callbackExecutionMatches = + executionAsyncId() === targetId && + executionAsyncResource() === targetResource; + error ? reject(error) : resolve(data.length); + }); + }); + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} finally { + hook.disable(); +} + +console.log( + "randomBytes hook relationship:", + length, + targetId > 0, + targetTriggerId === parentId, + callbackExecutionMatches, +); +console.log("randomBytes hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-signal-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-signal-lifecycle.ts new file mode 100644 index 0000000000..acc0f4b552 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-signal-lifecycle.ts @@ -0,0 +1,39 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +type Activity = { id: number; trigger: number; events: string[] }; +const root = executionAsyncId(); +const activities: Activity[] = []; +const byId = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type !== "SIGNALWRAP") return; + const activity = { id: asyncId, trigger: triggerAsyncId, events: ["init"] }; + activities.push(activity); + byId.set(asyncId, activity); + }, + before(asyncId) { + byId.get(asyncId)?.events.push("before"); + }, + after(asyncId) { + byId.get(asyncId)?.events.push("after"); + }, + destroy(asyncId) { + byId.get(asyncId)?.events.push("destroy"); + }, +}).enable(); +const signal = process.platform === "win32" ? "SIGTERM" : "SIGUSR2"; +const listener = () => {}; +process.on(signal, listener); +process.removeListener(signal, listener); +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +console.log("signal resources:", activities.length); +console.log( + "signal root trigger:", + activities.length === 1 && activities[0]?.trigger === root, +); +console.log( + "signal lifecycle:", + activities.map((a) => a.events.join(">")).join("|"), +); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-timer-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-timer-lifecycle.ts new file mode 100644 index 0000000000..3b65fa9e21 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-timer-lifecycle.ts @@ -0,0 +1,31 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; + +let target = -1; +let triggerMatches = false; +const root = executionAsyncId(); +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type === "Timeout" && target === -1) { + target = asyncId; + triggerMatches = triggerAsyncId === root; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === target) events.push("before"); + }, + after(asyncId) { + if (asyncId === target) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === target) events.push("destroy"); + }, +}).enable(); + +await new Promise((resolve) => setTimeout(resolve, 0)); +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +console.log("timer hook trigger root:", triggerMatches); +console.log("timer hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-udp-socket-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-udp-socket-lifecycle.ts new file mode 100644 index 0000000000..749f0fbcbc --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-udp-socket-lifecycle.ts @@ -0,0 +1,47 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { createSocket } from "node:dgram"; + +const parentId = executionAsyncId(); +let targetId = -1; +let targetTriggerId = -1; +let resourceIsObject = false; +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "UDPWRAP" && targetId === -1) { + targetId = asyncId; + targetTriggerId = triggerAsyncId; + resourceIsObject = typeof resource === "object" && resource !== null; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === targetId) events.push("before"); + }, + after(asyncId) { + if (asyncId === targetId) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === targetId) events.push("destroy"); + }, +}).enable(); + +const socket = createSocket("udp4"); +try { + await new Promise((resolve) => socket.close(resolve)); + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} finally { + hook.disable(); + try { + socket.close(); + } catch {} +} + +console.log( + "udp socket resource:", + targetId > 0, + targetTriggerId === parentId, + resourceIsObject, +); +console.log("udp socket lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/provider-webcrypto-request-lifecycles.ts b/test-parity/node-suite/async_hooks/hooks/provider-webcrypto-request-lifecycles.ts new file mode 100644 index 0000000000..7984cfb8b1 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-webcrypto-request-lifecycles.ts @@ -0,0 +1,89 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { webcrypto } from "node:crypto"; + +const { subtle } = webcrypto; +const aesKey = await subtle.generateKey( + { name: "AES-GCM", length: 128 }, + false, + ["encrypt"], +); +const hmacKey = await subtle.generateKey( + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign", "verify"], +); +const data = new TextEncoder().encode("async hooks webcrypto"); +const types = ["HASHREQUEST", "CIPHERREQUEST", "SIGNREQUEST"] as const; +type Type = (typeof types)[number]; +type Entry = { + id: number; + trigger: number; + before: number; + after: number; + destroy: number; +}; +const entries = new Map(); +const byId = new Map(); +const parents = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (!types.includes(type as Type)) return; + const entry = { + id: asyncId, + trigger: triggerAsyncId, + before: 0, + after: 0, + destroy: 0, + }; + const selected = entries.get(type as Type) || []; + selected.push(entry); + entries.set(type as Type, selected); + byId.set(asyncId, entry); + }, + before(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.before++; + }, + after(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.after++; + }, + destroy(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.destroy++; + }, +}).enable(); + +function remember(type: Type) { + const selected = parents.get(type) || []; + selected.push(executionAsyncId()); + parents.set(type, selected); +} +remember("HASHREQUEST"); +await subtle.digest("SHA-256", data); +remember("CIPHERREQUEST"); +await subtle.encrypt({ name: "AES-GCM", iv: new Uint8Array(12) }, aesKey, data); +remember("SIGNREQUEST"); +const signature = await subtle.sign("HMAC", hmacKey, data); +remember("SIGNREQUEST"); +const verified = await subtle.verify("HMAC", hmacKey, signature, data); +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); + +console.log("webcrypto verified:", verified); +for (const type of types) { + const selected = entries.get(type) || []; + const expectedParents = parents.get(type) || []; + console.log( + `${type} lifecycle:`, + selected.length, + selected.length === expectedParents.length && + selected.every( + (entry, index) => entry.trigger === expectedParents[index], + ), + selected + .map((entry) => `${entry.before}/${entry.after}/${entry.destroy}`) + .join(","), + ); +} diff --git a/test-parity/node-suite/async_hooks/hooks/provider-zlib-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/provider-zlib-lifecycle.ts new file mode 100644 index 0000000000..f28d932ac7 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/provider-zlib-lifecycle.ts @@ -0,0 +1,46 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { deflate } from "node:zlib"; + +const parentId = executionAsyncId(); +let targetId = -1; +let targetTriggerId = -1; +const events: string[] = []; +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type === "ZLIB" && targetId === -1) { + targetId = asyncId; + targetTriggerId = triggerAsyncId; + events.push("init"); + } + }, + before(asyncId) { + if (asyncId === targetId) events.push("before"); + }, + after(asyncId) { + if (asyncId === targetId) events.push("after"); + }, + destroy(asyncId) { + if (asyncId === targetId) events.push("destroy"); + }, +}).enable(); + +let length = -1; +try { + length = await new Promise((resolve, reject) => { + deflate("zlib-hook-payload", (error, data) => { + error ? reject(error) : resolve(data.length); + }); + }); + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); +} finally { + hook.disable(); +} + +console.log( + "zlib hook relationship:", + length > 0, + targetId > 0, + targetTriggerId === parentId, +); +console.log("zlib hook lifecycle:", events.join(">")); diff --git a/test-parity/node-suite/async_hooks/hooks/resource-argument.ts b/test-parity/node-suite/async_hooks/hooks/resource-argument.ts new file mode 100644 index 0000000000..f17e32974a --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/resource-argument.ts @@ -0,0 +1,21 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +let observedResource: unknown; +let observedType = ""; + +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "ParityResourceArgument") { + observedType = type; + observedResource = resource; + } + }, +}).enable(); + +const resource = new AsyncResource("ParityResourceArgument"); +console.log("resource argument type:", observedType); +console.log("resource argument identity:", observedResource === resource); +console.log("resource argument object:", typeof observedResource); + +hook.disable(); +resource.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/hooks/scheduler-resource-identity.ts b/test-parity/node-suite/async_hooks/hooks/scheduler-resource-identity.ts new file mode 100644 index 0000000000..2b45782e95 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/scheduler-resource-identity.ts @@ -0,0 +1,78 @@ +import { createHook, executionAsyncResource } from "node:async_hooks"; + +type ResourceInfo = { + asyncId: number; + triggerAsyncId: number; + resource: object; +}; +const resources = new Map(); +const executionMatches = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if ( + (type === "Timeout" || + type === "Immediate" || + type === "Microtask" || + type === "TickObject") && + !resources.has(type) + ) { + resources.set(type, { asyncId, triggerAsyncId, resource }); + } + }, +}).enable(); + +const timeout = setTimeout(() => { + executionMatches.set( + "Timeout", + executionAsyncResource() === resources.get("Timeout")?.resource, + ); + const immediate = setImmediate(() => { + executionMatches.set( + "Immediate", + executionAsyncResource() === resources.get("Immediate")?.resource, + ); + queueMicrotask(() => { + executionMatches.set( + "Microtask", + executionAsyncResource() === resources.get("Microtask")?.resource, + ); + process.nextTick(() => { + executionMatches.set( + "TickObject", + executionAsyncResource() === resources.get("TickObject")?.resource, + ); + setImmediate(() => { + hook.disable(); + const timeoutInfo = resources.get("Timeout"); + const immediateInfo = resources.get("Immediate"); + const microtaskInfo = resources.get("Microtask"); + const tickInfo = resources.get("TickObject"); + console.log( + "scheduler handles match resources:", + timeoutInfo?.resource === timeout, + immediateInfo?.resource === immediate, + ); + console.log( + "scheduler execution resources:", + executionMatches.get("Timeout"), + executionMatches.get("Immediate"), + executionMatches.get("Microtask"), + executionMatches.get("TickObject"), + ); + console.log( + "scheduler trigger chain:", + !!immediateInfo && + !!timeoutInfo && + immediateInfo.triggerAsyncId === timeoutInfo.asyncId, + !!microtaskInfo && + !!immediateInfo && + microtaskInfo.triggerAsyncId === immediateInfo.asyncId, + !!tickInfo && + !!microtaskInfo && + tickInfo.triggerAsyncId === microtaskInfo.asyncId, + ); + }); + }); + }); + }); +}, 0); diff --git a/test-parity/node-suite/async_hooks/hooks/scope-throw-lifecycle.ts b/test-parity/node-suite/async_hooks/hooks/scope-throw-lifecycle.ts new file mode 100644 index 0000000000..c392fcbe72 --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/scope-throw-lifecycle.ts @@ -0,0 +1,28 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +const resource = new AsyncResource("ParityScopeThrow"); +const targetId = resource.asyncId(); +const events: string[] = []; + +const hook = createHook({ + before(asyncId) { + if (asyncId === targetId) events.push("before"); + }, + after(asyncId) { + if (asyncId === targetId) events.push("after"); + }, +}).enable(); + +try { + resource.runInAsyncScope(() => { + events.push("callback"); + throw new Error("expected"); + }); +} catch (error) { + console.log("scope throw error:", (error as Error).message); +} + +console.log("scope throw events:", events.join(">")); + +hook.disable(); +resource.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/hooks/track-promises.ts b/test-parity/node-suite/async_hooks/hooks/track-promises.ts new file mode 100644 index 0000000000..78cdabc05c --- /dev/null +++ b/test-parity/node-suite/async_hooks/hooks/track-promises.ts @@ -0,0 +1,36 @@ +import { createHook } from "node:async_hooks"; + +let defaultResource: object | undefined; +const defaultHook = createHook({ + init(_asyncId, type, _triggerAsyncId, resource) { + if (type === "PROMISE") defaultResource = resource; + }, +}).enable(); +const defaultPromise = Promise.resolve("default"); +defaultHook.disable(); +console.log( + "trackPromises default resource:", + defaultResource === defaultPromise, +); + +let explicitCount = 0; +const explicitHook = createHook({ + init(_asyncId, type) { + if (type === "PROMISE") explicitCount++; + }, + trackPromises: true, +}).enable(); +Promise.resolve("explicit"); +explicitHook.disable(); +console.log("trackPromises true init count:", explicitCount); + +let disabledCount = 0; +const disabledHook = createHook({ + init(_asyncId, type) { + if (type === "PROMISE") disabledCount++; + }, + trackPromises: false, +}).enable(); +Promise.resolve("disabled"); +disabledHook.disable(); +console.log("trackPromises false init count:", disabledCount); diff --git a/test-parity/node-suite/async_hooks/integrations/crypto-derive.ts b/test-parity/node-suite/async_hooks/integrations/crypto-derive.ts new file mode 100644 index 0000000000..46503082c2 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/crypto-derive.ts @@ -0,0 +1,31 @@ +import { pbkdf2, scrypt } from "node:crypto"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const pbkdf2Length = await storage.run( + "pbkdf2", + () => + new Promise((resolve, reject) => { + pbkdf2("password", "salt", 2, 16, "sha256", (error, key) => { + console.log("pbkdf2 store:", storage.getStore()); + if (error) return reject(error); + resolve(key.length); + }); + }), +); +console.log("pbkdf2 length:", pbkdf2Length); + +const scryptLength = await storage.run( + "scrypt", + () => + new Promise((resolve, reject) => { + scrypt("password", "salt", 16, (error, key) => { + console.log("scrypt store:", storage.getStore()); + if (error) return reject(error); + resolve(key.length); + }); + }), +); +console.log("scrypt length:", scryptLength); +console.log("derive outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/crypto-generate-key-pair.ts b/test-parity/node-suite/async_hooks/integrations/crypto-generate-key-pair.ts new file mode 100644 index 0000000000..e1fcad2cc1 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/crypto-generate-key-pair.ts @@ -0,0 +1,22 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { generateKeyPair } from "node:crypto"; + +const storage = new AsyncLocalStorage(); +const result = await storage.run( + "generate-key-pair", + () => + new Promise((resolve, reject) => { + const returned = generateKeyPair( + "rsa", + { modulusLength: 512 }, + (error, publicKey, privateKey) => { + console.log("generateKeyPair store:", storage.getStore()); + if (error) return reject(error); + resolve(`${publicKey.type}:${privateKey.type}`); + }, + ); + console.log("generateKeyPair return undefined:", returned === undefined); + }), +); +console.log("generateKeyPair result:", result); +console.log("generateKeyPair outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/crypto-generate-key.ts b/test-parity/node-suite/async_hooks/integrations/crypto-generate-key.ts new file mode 100644 index 0000000000..6d862b0544 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/crypto-generate-key.ts @@ -0,0 +1,18 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { generateKey } from "node:crypto"; + +const storage = new AsyncLocalStorage(); +const result = await storage.run( + "generate-key", + () => + new Promise((resolve, reject) => { + const returned = generateKey("hmac", { length: 64 }, (error, key) => { + console.log("generateKey store:", storage.getStore()); + if (error) return reject(error); + resolve(`${key.type}:${key.symmetricKeySize}`); + }); + console.log("generateKey return undefined:", returned === undefined); + }), +); +console.log("generateKey result:", result); +console.log("generateKey outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/crypto-hkdf.ts b/test-parity/node-suite/async_hooks/integrations/crypto-hkdf.ts new file mode 100644 index 0000000000..2ebe5eaf05 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/crypto-hkdf.ts @@ -0,0 +1,25 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { hkdf } from "node:crypto"; + +const storage = new AsyncLocalStorage(); +const length = await storage.run( + "hkdf", + () => + new Promise((resolve, reject) => { + const returned = hkdf( + "sha256", + "key", + "salt", + "info", + 16, + (error, derived) => { + console.log("hkdf store:", storage.getStore()); + if (error) return reject(error); + resolve(derived.byteLength); + }, + ); + console.log("hkdf return undefined:", returned === undefined); + }), +); +console.log("hkdf length:", length); +console.log("hkdf outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/crypto-prime.ts b/test-parity/node-suite/async_hooks/integrations/crypto-prime.ts new file mode 100644 index 0000000000..8deb32d289 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/crypto-prime.ts @@ -0,0 +1,24 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { checkPrime, generatePrime } from "node:crypto"; + +const storage = new AsyncLocalStorage(); +const checked = await storage.run( + "generate-prime", + () => + new Promise((resolve, reject) => { + const generated = generatePrime(32, (generateError, prime) => { + console.log("generatePrime store:", storage.getStore()); + if (generateError) return reject(generateError); + storage.enterWith("check-prime"); + const checked = checkPrime(prime, (checkError, value) => { + console.log("checkPrime store:", storage.getStore()); + if (checkError) return reject(checkError); + resolve(value); + }); + console.log("checkPrime return undefined:", checked === undefined); + }); + console.log("generatePrime return undefined:", generated === undefined); + }), +); +console.log("prime checked:", checked); +console.log("prime outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/crypto-random-bytes.ts b/test-parity/node-suite/async_hooks/integrations/crypto-random-bytes.ts new file mode 100644 index 0000000000..c64ac4b6af --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/crypto-random-bytes.ts @@ -0,0 +1,19 @@ +import { randomBytes } from "node:crypto"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const length = await storage.run( + "random-bytes", + () => + new Promise((resolve, reject) => { + randomBytes(16, (error, bytes) => { + console.log("randomBytes store:", storage.getStore()); + if (error) return reject(error); + resolve(bytes.length); + }); + }), +); + +console.log("randomBytes length:", length); +console.log("randomBytes outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/crypto-random-fill.ts b/test-parity/node-suite/async_hooks/integrations/crypto-random-fill.ts new file mode 100644 index 0000000000..043e60083c --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/crypto-random-fill.ts @@ -0,0 +1,21 @@ +import { randomFill } from "node:crypto"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const target = new Uint8Array(12); + +const result = await storage.run( + "random-fill", + () => + new Promise((resolve, reject) => { + randomFill(target, 2, 6, (error, value) => { + console.log("randomFill store:", storage.getStore()); + if (error) return reject(error); + resolve(value); + }); + }), +); + +console.log("randomFill identity:", result === target); +console.log("randomFill length:", result.length); +console.log("randomFill outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/crypto-random-int.ts b/test-parity/node-suite/async_hooks/integrations/crypto-random-int.ts new file mode 100644 index 0000000000..c7c0279c59 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/crypto-random-int.ts @@ -0,0 +1,18 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { randomInt } from "node:crypto"; + +const storage = new AsyncLocalStorage(); +const result = await storage.run( + "random-int", + () => + new Promise((resolve, reject) => { + const returned = randomInt(10, 20, (error, value) => { + console.log("randomInt store:", storage.getStore()); + if (error) return reject(error); + resolve(value >= 10 && value < 20); + }); + console.log("randomInt return undefined:", returned === undefined); + }), +); +console.log("randomInt in range:", result); +console.log("randomInt outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/event-emitter-async-resource.ts b/test-parity/node-suite/async_hooks/integrations/event-emitter-async-resource.ts new file mode 100644 index 0000000000..d2b9cb50fa --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/event-emitter-async-resource.ts @@ -0,0 +1,51 @@ +import { AsyncLocalStorage, createHook } from "node:async_hooks"; +import { EventEmitterAsyncResource } from "node:events"; + +const storage = new AsyncLocalStorage(); +let target = -1; +let observedResource: object | undefined; +const lifecycle: string[] = []; +let emitter!: EventEmitterAsyncResource; +const hook = createHook({ + init(asyncId, type, _triggerAsyncId, resource) { + if (type === "ParityEventEmitter") { + target = asyncId; + observedResource = resource; + lifecycle.push("init"); + } + }, + before(asyncId) { + if (asyncId === target) lifecycle.push("before"); + }, + after(asyncId) { + if (asyncId === target) lifecycle.push("after"); + }, + destroy(asyncId) { + if (asyncId === target) lifecycle.push("destroy"); + }, +}).enable(); + +emitter = storage.run( + "constructed", + () => new EventEmitterAsyncResource({ name: "ParityEventEmitter" }), +); +const stores: string[] = []; +emitter.on("event", (value) => { + stores.push(`${storage.getStore()}:${value}`); +}); +storage.run("emitting", () => emitter.emit("event", "payload")); +emitter.emitDestroy(); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); +console.log("event async resource id positive:", emitter.asyncId > 0); +console.log( + "event async resource trigger nonnegative:", + emitter.triggerAsyncId >= 0, +); +console.log("event async resource listener stores:", stores.join(",")); +console.log("event async resource lifecycle:", lifecycle.join(">")); +console.log( + "event async resource supplied:", + observedResource === emitter.asyncResource, +); +console.log("event async resource outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/event-emitter-back-reference.ts b/test-parity/node-suite/async_hooks/integrations/event-emitter-back-reference.ts new file mode 100644 index 0000000000..a47a2a2b3a --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/event-emitter-back-reference.ts @@ -0,0 +1,12 @@ +import { EventEmitterAsyncResource } from "node:events"; + +const emitter = new EventEmitterAsyncResource({ + name: "ParityEventEmitterBackReference", +}); +const resource = emitter.asyncResource; +console.log( + "event resource back reference:", + typeof resource, + resource.eventEmitter === emitter, +); +emitter.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/integrations/event-emitter-brand-validation.ts b/test-parity/node-suite/async_hooks/integrations/event-emitter-brand-validation.ts new file mode 100644 index 0000000000..32f31b3cca --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/event-emitter-brand-validation.ts @@ -0,0 +1,30 @@ +import { EventEmitterAsyncResource } from "node:events"; + +const prototype = EventEmitterAsyncResource.prototype; +console.log( + "event resource prototype surface:", + typeof prototype.emit, + typeof prototype.emitDestroy, + "asyncId" in prototype, + "triggerAsyncId" in prototype, + "asyncResource" in prototype, +); + +function probe(label: string, operation: () => unknown) { + try { + operation(); + console.log(label, "no-throw"); + } catch (error: any) { + console.log( + label, + error.name, + /private member/.test(String(error.message)), + ); + } +} + +probe("event resource emit brand:", () => prototype.emit()); +probe("event resource destroy brand:", () => prototype.emitDestroy()); +probe("event resource asyncId brand:", () => prototype.asyncId); +probe("event resource triggerAsyncId brand:", () => prototype.triggerAsyncId); +probe("event resource asyncResource brand:", () => prototype.asyncResource); diff --git a/test-parity/node-suite/async_hooks/integrations/event-emitter-resource-names.ts b/test-parity/node-suite/async_hooks/integrations/event-emitter-resource-names.ts new file mode 100644 index 0000000000..bf85914635 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/event-emitter-resource-names.ts @@ -0,0 +1,51 @@ +import { createHook, executionAsyncId } from "node:async_hooks"; +import { EventEmitterAsyncResource } from "node:events"; + +type InitInfo = { triggerAsyncId: number; resource: object }; +const observed = new Map(); +const tracked = new Set(["NamedEmitter", "ExplicitString", "ExplicitOptions"]); +const hook = createHook({ + init(_asyncId, type, triggerAsyncId, resource) { + if (tracked.has(type)) observed.set(type, { triggerAsyncId, resource }); + }, +}).enable(); + +class NamedEmitter extends EventEmitterAsyncResource {} +const parentId = executionAsyncId(); +const defaultEmitter = new NamedEmitter(); +const stringEmitter = new NamedEmitter("ExplicitString"); +const optionsEmitter = new NamedEmitter({ + name: "ExplicitOptions", + triggerAsyncId: 73, +}); + +const executionMatches: boolean[] = []; +for (const emitter of [defaultEmitter, stringEmitter, optionsEmitter]) { + emitter.on("event", () => { + executionMatches.push(executionAsyncId() === emitter.asyncId); + }); + emitter.emit("event"); +} + +console.log( + "event resource names observed:", + [...observed.keys()].sort().join(","), +); +console.log( + "event resource default triggers:", + observed.get("NamedEmitter")?.triggerAsyncId === parentId, + observed.get("ExplicitString")?.triggerAsyncId === parentId, + observed.get("ExplicitOptions")?.triggerAsyncId === 73, +); +console.log( + "event resource objects supplied:", + observed.get("NamedEmitter")?.resource === defaultEmitter.asyncResource, + observed.get("ExplicitString")?.resource === stringEmitter.asyncResource, + observed.get("ExplicitOptions")?.resource === optionsEmitter.asyncResource, +); +console.log("event resource execution ids:", executionMatches.join(",")); + +for (const emitter of [defaultEmitter, stringEmitter, optionsEmitter]) { + emitter.emitDestroy(); +} +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/integrations/events-emitter.ts b/test-parity/node-suite/async_hooks/integrations/events-emitter.ts new file mode 100644 index 0000000000..5f6409530d --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/events-emitter.ts @@ -0,0 +1,32 @@ +import { EventEmitter } from "node:events"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +await storage.run( + "events", + () => + new Promise((resolve) => { + const emitter = new EventEmitter(); + emitter.on("sync", (value) => { + console.log("event sync store:", storage.getStore(), value); + }); + emitter.once("once", () => { + console.log("event once store:", storage.getStore()); + }); + emitter.on("async", async () => { + await Promise.resolve(); + console.log("event async store:", storage.getStore()); + resolve(); + }); + + process.nextTick(() => { + emitter.emit("sync", "value"); + emitter.emit("once"); + emitter.emit("once"); + emitter.emit("async"); + }); + }), +); + +console.log("events outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-access.ts b/test-parity/node-suite/async_hooks/integrations/fs-access.ts new file mode 100644 index 0000000000..ac005ea8d2 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-access.ts @@ -0,0 +1,21 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { access, constants, writeFileSync, unlinkSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const path = "/tmp/perry-async-hooks-fs-access.txt"; +writeFileSync(path, "access"); +try { + await storage.run( + "fs-access", + () => + new Promise((resolve, reject) => { + access(path, constants.R_OK, (error) => { + console.log("fs.access store:", storage.getStore()); + if (error) return reject(error); + resolve(); + }); + }), + ); +} finally { + unlinkSync(path); +} +console.log("fs.access outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-append-file.ts b/test-parity/node-suite/async_hooks/integrations/fs-append-file.ts new file mode 100644 index 0000000000..70defac60e --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-append-file.ts @@ -0,0 +1,19 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { appendFile, readFileSync, writeFileSync, unlinkSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const path = "/tmp/perry-async-hooks-fs-append.txt"; +writeFileSync(path, "a"); +await storage.run( + "fs-append", + () => + new Promise((resolve, reject) => { + appendFile(path, "b", (error) => { + console.log("fs.appendFile store:", storage.getStore()); + if (error) return reject(error); + resolve(); + }); + }), +); +console.log("fs.appendFile content:", readFileSync(path, "utf8")); +unlinkSync(path); +console.log("fs.appendFile outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-callback-chain.ts b/test-parity/node-suite/async_hooks/integrations/fs-callback-chain.ts new file mode 100644 index 0000000000..968ac64a47 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-callback-chain.ts @@ -0,0 +1,33 @@ +import { unlinkSync, writeFile, readFile, unlink } from "node:fs"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const path = "/tmp/perry-async-hooks-fs-callback.txt"; + +try { + unlinkSync(path); +} catch {} + +await storage.run( + "fs-callback", + () => + new Promise((resolve, reject) => { + writeFile(path, "payload", (writeError) => { + console.log("writeFile store:", storage.getStore()); + if (writeError) return reject(writeError); + + readFile(path, "utf8", (readError, data) => { + console.log("readFile store:", storage.getStore(), data); + if (readError) return reject(readError); + + unlink(path, (unlinkError) => { + console.log("unlink store:", storage.getStore()); + if (unlinkError) return reject(unlinkError); + resolve(); + }); + }); + }); + }), +); + +console.log("fs callback outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-copy-file.ts b/test-parity/node-suite/async_hooks/integrations/fs-copy-file.ts new file mode 100644 index 0000000000..8cc1f5539f --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-copy-file.ts @@ -0,0 +1,23 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { copyFile, readFileSync, writeFileSync, rmSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const source = "/tmp/perry-async-hooks-fs-copy-source.txt"; +const target = "/tmp/perry-async-hooks-fs-copy-target.txt"; +rmSync(source, { force: true }); +rmSync(target, { force: true }); +writeFileSync(source, "copy"); +await storage.run( + "fs-copy", + () => + new Promise((resolve, reject) => { + copyFile(source, target, (error) => { + console.log("fs.copyFile store:", storage.getStore()); + if (error) return reject(error); + resolve(); + }); + }), +); +console.log("fs.copyFile content:", readFileSync(target, "utf8")); +rmSync(source, { force: true }); +rmSync(target, { force: true }); +console.log("fs.copyFile outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-directory.ts b/test-parity/node-suite/async_hooks/integrations/fs-directory.ts new file mode 100644 index 0000000000..9a97f5193c --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-directory.ts @@ -0,0 +1,33 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { mkdir, readdir, realpath, realpathSync, rmdir, rmSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const path = `${realpathSync("/tmp")}/perry-async-hooks-fs-directory`; +rmSync(path, { recursive: true, force: true }); +await storage.run( + "fs-directory", + () => + new Promise((resolve, reject) => { + mkdir(path, (mkdirError) => { + console.log("fs.mkdir store:", storage.getStore()); + if (mkdirError) return reject(mkdirError); + readdir(path, (readdirError, entries) => { + console.log("fs.readdir store:", storage.getStore(), entries.length); + if (readdirError) return reject(readdirError); + realpath(path, (realpathError, resolved) => { + console.log( + "fs.realpath store:", + storage.getStore(), + resolved === path, + ); + if (realpathError) return reject(realpathError); + rmdir(path, (rmdirError) => { + console.log("fs.rmdir store:", storage.getStore()); + if (rmdirError) return reject(rmdirError); + resolve(); + }); + }); + }); + }); + }), +); +console.log("fs.directory outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-fd-operations.ts b/test-parity/node-suite/async_hooks/integrations/fs-fd-operations.ts new file mode 100644 index 0000000000..142b813d69 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-fd-operations.ts @@ -0,0 +1,36 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { close, fstat, open, read, writeFileSync, unlinkSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const path = "/tmp/perry-async-hooks-fs-fd.txt"; +writeFileSync(path, "fd-data"); +await storage.run( + "fs-fd", + () => + new Promise((resolve, reject) => { + open(path, "r", (openError, fd) => { + console.log("fs.open store:", storage.getStore()); + if (openError) return reject(openError); + fstat(fd, (statError, stats) => { + console.log("fs.fstat store:", storage.getStore(), stats.size); + if (statError) return reject(statError); + const buffer = Buffer.alloc(2); + read(fd, buffer, 0, 2, 0, (readError, bytesRead) => { + console.log( + "fs.read store:", + storage.getStore(), + bytesRead, + String(buffer), + ); + if (readError) return reject(readError); + close(fd, (closeError) => { + console.log("fs.close store:", storage.getStore()); + if (closeError) return reject(closeError); + resolve(); + }); + }); + }); + }); + }), +); +unlinkSync(path); +console.log("fs.fd outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-metadata.ts b/test-parity/node-suite/async_hooks/integrations/fs-metadata.ts new file mode 100644 index 0000000000..d216998dca --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-metadata.ts @@ -0,0 +1,30 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { chmod, lstat, stat, writeFileSync, unlinkSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const path = "/tmp/perry-async-hooks-fs-metadata.txt"; +writeFileSync(path, "meta"); +await storage.run( + "fs-metadata", + () => + new Promise((resolve, reject) => { + chmod(path, 0o600, (chmodError) => { + console.log("fs.chmod store:", storage.getStore()); + if (chmodError) return reject(chmodError); + lstat(path, (lstatError, linkStats) => { + console.log( + "fs.lstat store:", + storage.getStore(), + linkStats.isFile(), + ); + if (lstatError) return reject(lstatError); + stat(path, (statError, stats) => { + console.log("fs.stat store:", storage.getStore(), stats.size); + if (statError) return reject(statError); + resolve(); + }); + }); + }); + }), +); +unlinkSync(path); +console.log("fs.metadata outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-mkdtemp.ts b/test-parity/node-suite/async_hooks/integrations/fs-mkdtemp.ts new file mode 100644 index 0000000000..fecc8903f0 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-mkdtemp.ts @@ -0,0 +1,20 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { mkdtemp, rmSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const made = await storage.run( + "fs-mkdtemp", + () => + new Promise((resolve, reject) => { + mkdtemp("/tmp/perry-async-hooks-mkdtemp-", (error, path) => { + console.log("fs.mkdtemp store:", storage.getStore()); + if (error) return reject(error); + resolve(path); + }); + }), +); +console.log( + "fs.mkdtemp prefix:", + made.startsWith("/tmp/perry-async-hooks-mkdtemp-"), +); +rmSync(made, { recursive: true, force: true }); +console.log("fs.mkdtemp outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-promises-chain.ts b/test-parity/node-suite/async_hooks/integrations/fs-promises-chain.ts new file mode 100644 index 0000000000..cc196173c0 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-promises-chain.ts @@ -0,0 +1,23 @@ +import { unlinkSync } from "node:fs"; +import { readFile, unlink, writeFile } from "node:fs/promises"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const path = "/tmp/perry-async-hooks-fs-promises.txt"; + +try { + unlinkSync(path); +} catch {} + +const result = await storage.run("fs-promises", async () => { + await writeFile(path, "promise-payload"); + console.log("writeFile promise store:", storage.getStore()); + const data = await readFile(path, "utf8"); + console.log("readFile promise store:", storage.getStore(), data); + await unlink(path); + console.log("unlink promise store:", storage.getStore()); + return "fs-promises-result"; +}); + +console.log("fs promises result:", result); +console.log("fs promises outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-rename.ts b/test-parity/node-suite/async_hooks/integrations/fs-rename.ts new file mode 100644 index 0000000000..a57843996a --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-rename.ts @@ -0,0 +1,22 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { rename, readFileSync, writeFileSync, rmSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const source = "/tmp/perry-async-hooks-fs-rename-source.txt"; +const target = "/tmp/perry-async-hooks-fs-rename-target.txt"; +rmSync(source, { force: true }); +rmSync(target, { force: true }); +writeFileSync(source, "rename"); +await storage.run( + "fs-rename", + () => + new Promise((resolve, reject) => { + rename(source, target, (error) => { + console.log("fs.rename store:", storage.getStore()); + if (error) return reject(error); + resolve(); + }); + }), +); +console.log("fs.rename content:", readFileSync(target, "utf8")); +rmSync(target, { force: true }); +console.log("fs.rename outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-streams.ts b/test-parity/node-suite/async_hooks/integrations/fs-streams.ts new file mode 100644 index 0000000000..15a77744a0 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-streams.ts @@ -0,0 +1,40 @@ +import { createReadStream, createWriteStream, unlinkSync } from "node:fs"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const path = "/tmp/perry-async-hooks-fs-streams.txt"; + +try { + unlinkSync(path); +} catch {} + +await storage.run( + "fs-streams", + () => + new Promise((resolve, reject) => { + const writer = createWriteStream(path); + writer.on("error", reject); + writer.on("finish", () => { + console.log("write stream finish store:", storage.getStore()); + const chunks: string[] = []; + const reader = createReadStream(path, { encoding: "utf8" }); + reader.on("error", reject); + reader.on("data", (chunk) => { + console.log("read stream data store:", storage.getStore()); + chunks.push(String(chunk)); + }); + reader.on("end", () => { + console.log( + "read stream end store:", + storage.getStore(), + chunks.join(""), + ); + unlinkSync(path); + resolve(); + }); + }); + writer.end("stream-payload"); + }), +); + +console.log("fs streams outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-truncate.ts b/test-parity/node-suite/async_hooks/integrations/fs-truncate.ts new file mode 100644 index 0000000000..485908e672 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-truncate.ts @@ -0,0 +1,19 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { statSync, truncate, writeFileSync, unlinkSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const path = "/tmp/perry-async-hooks-fs-truncate.txt"; +writeFileSync(path, "truncate"); +await storage.run( + "fs-truncate", + () => + new Promise((resolve, reject) => { + truncate(path, 3, (error) => { + console.log("fs.truncate store:", storage.getStore()); + if (error) return reject(error); + resolve(); + }); + }), +); +console.log("fs.truncate size:", statSync(path).size); +unlinkSync(path); +console.log("fs.truncate outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-watch-file.ts b/test-parity/node-suite/async_hooks/integrations/fs-watch-file.ts new file mode 100644 index 0000000000..0b1d467ff3 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-watch-file.ts @@ -0,0 +1,24 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { unwatchFile, watchFile, writeFileSync, unlinkSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const path = "/tmp/perry-async-hooks-fs-watch-file.txt"; +writeFileSync(path, "before"); +await storage.run( + "fs-watch-file", + () => + new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + unwatchFile(path); + reject(new Error("fs.watchFile timeout")); + }, 2000); + watchFile(path, { interval: 20 }, () => { + console.log("fs.watchFile store:", storage.getStore()); + clearTimeout(timeout); + unwatchFile(path); + resolve(); + }); + setTimeout(() => writeFileSync(path, "after"), 50); + }), +); +unlinkSync(path); +console.log("fs.watchFile outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/fs-watch.ts b/test-parity/node-suite/async_hooks/integrations/fs-watch.ts new file mode 100644 index 0000000000..531218463c --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/fs-watch.ts @@ -0,0 +1,30 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { mkdirSync, rmSync, watch, writeFileSync } from "node:fs"; +const storage = new AsyncLocalStorage(); +const directory = "/tmp/perry-async-hooks-fs-watch"; +const path = `${directory}/value.txt`; +rmSync(directory, { recursive: true, force: true }); +mkdirSync(directory); +writeFileSync(path, "before"); +await storage.run( + "fs-watch", + () => + new Promise((resolve, reject) => { + let settled = false; + const timeout = setTimeout(() => { + watcher.close(); + reject(new Error("fs.watch timeout")); + }, 2000); + const watcher = watch(directory, () => { + if (settled) return; + settled = true; + console.log("fs.watch store:", storage.getStore()); + clearTimeout(timeout); + watcher.close(); + resolve(); + }); + setImmediate(() => writeFileSync(path, "after")); + }), +); +rmSync(directory, { recursive: true, force: true }); +console.log("fs.watch outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/stream-finished.ts b/test-parity/node-suite/async_hooks/integrations/stream-finished.ts new file mode 100644 index 0000000000..7b9d2791c1 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/stream-finished.ts @@ -0,0 +1,21 @@ +import { PassThrough, finished } from "node:stream"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +await storage.run( + "finished", + () => + new Promise((resolve, reject) => { + const stream = new PassThrough(); + stream.resume(); + finished(stream, (error) => { + console.log("finished callback store:", storage.getStore()); + if (error) return reject(error); + resolve(); + }); + stream.end("payload"); + }), +); + +console.log("finished outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/stream-readable.ts b/test-parity/node-suite/async_hooks/integrations/stream-readable.ts new file mode 100644 index 0000000000..4a497b4c97 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/stream-readable.ts @@ -0,0 +1,32 @@ +import { Readable } from "node:stream"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const output = await storage.run( + "readable", + () => + new Promise((resolve, reject) => { + const chunks: string[] = []; + const readable = new Readable({ + read() { + console.log("read method store:", storage.getStore()); + this.push("a"); + this.push("b"); + this.push(null); + }, + }); + readable.on("error", reject); + readable.on("data", (chunk) => { + console.log("readable data store:", storage.getStore()); + chunks.push(String(chunk)); + }); + readable.on("end", () => { + console.log("readable end store:", storage.getStore()); + resolve(chunks.join("")); + }); + }), +); + +console.log("readable result:", output); +console.log("readable outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/stream-transform.ts b/test-parity/node-suite/async_hooks/integrations/stream-transform.ts new file mode 100644 index 0000000000..93a66fd7ed --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/stream-transform.ts @@ -0,0 +1,31 @@ +import { Transform } from "node:stream"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const output = await storage.run( + "transform", + () => + new Promise((resolve, reject) => { + const chunks: string[] = []; + const transform = new Transform({ + transform(chunk, encoding, callback) { + console.log("transform method store:", storage.getStore()); + callback(null, String(chunk).toUpperCase()); + }, + }); + transform.on("error", reject); + transform.on("data", (chunk) => { + console.log("transform data store:", storage.getStore()); + chunks.push(String(chunk)); + }); + transform.on("end", () => { + console.log("transform end store:", storage.getStore()); + resolve(chunks.join("")); + }); + transform.end("payload"); + }), +); + +console.log("transform result:", output); +console.log("transform outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/stream-writable.ts b/test-parity/node-suite/async_hooks/integrations/stream-writable.ts new file mode 100644 index 0000000000..361e1a02bd --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/stream-writable.ts @@ -0,0 +1,33 @@ +import { Writable } from "node:stream"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const writes = await storage.run( + "writable", + () => + new Promise((resolve, reject) => { + let count = 0; + const writable = new Writable({ + write(chunk, encoding, callback) { + count++; + console.log( + "writable write store:", + storage.getStore(), + String(chunk), + ); + callback(); + }, + }); + writable.on("error", reject); + writable.on("finish", () => { + console.log("writable finish store:", storage.getStore()); + resolve(count); + }); + writable.write("first"); + writable.end("second"); + }), +); + +console.log("writable count:", writes); +console.log("writable outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/timers-promises.ts b/test-parity/node-suite/async_hooks/integrations/timers-promises.ts new file mode 100644 index 0000000000..ce031464b5 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/timers-promises.ts @@ -0,0 +1,36 @@ +import { + setImmediate as immediate, + setInterval as interval, + setTimeout as delay, + scheduler, +} from "node:timers/promises"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const result = await storage.run("timers-promises", async () => { + const timeoutValue = await delay(1, "timeout-value"); + console.log( + "timers promise timeout store:", + storage.getStore(), + timeoutValue, + ); + const immediateValue = await immediate("immediate-value"); + console.log( + "timers promise immediate store:", + storage.getStore(), + immediateValue, + ); + await scheduler.yield(); + console.log("timers scheduler yield store:", storage.getStore()); + await scheduler.wait(1); + console.log("timers scheduler wait store:", storage.getStore()); + for await (const value of interval(1, "interval-value")) { + console.log("timers promise interval store:", storage.getStore(), value); + break; + } + return "timers-result"; +}); + +console.log("timers promise result:", result); +console.log("timers promise outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/util-promisify-custom.ts b/test-parity/node-suite/async_hooks/integrations/util-promisify-custom.ts new file mode 100644 index 0000000000..b684709ee6 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/util-promisify-custom.ts @@ -0,0 +1,29 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { promisify } from "node:util"; + +const storage = new AsyncLocalStorage(); +function callbackOperation( + _value: number, + _callback: (error: null, result: number) => void, +) { + throw new Error("default callback implementation must not run"); +} + +const customImplementation = async (value: number) => { + console.log("custom promisify start store:", storage.getStore()); + await new Promise((resolve) => setImmediate(resolve)); + console.log("custom promisify continuation store:", storage.getStore()); + return value * 3; +}; +callbackOperation[promisify.custom] = customImplementation; +const promisedOperation = promisify(callbackOperation); + +const result = await storage.run("custom-promisify", () => + promisedOperation(14), +); +console.log( + "custom promisify identity/result:", + promisedOperation === customImplementation, + result, +); +console.log("custom promisify outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/util-promisify.ts b/test-parity/node-suite/async_hooks/integrations/util-promisify.ts new file mode 100644 index 0000000000..249ae6deb2 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/util-promisify.ts @@ -0,0 +1,24 @@ +import { promisify } from "node:util"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +function callbackOperation( + value: number, + callback: (error: null, result: number) => void, +) { + process.nextTick(() => { + console.log("promisify callback store:", storage.getStore()); + callback(null, value * 2); + }); +} + +const promisedOperation = promisify(callbackOperation); +const result = await storage.run("promisify", async () => { + const value = await promisedOperation(21); + console.log("promisify continuation store:", storage.getStore()); + return value; +}); + +console.log("promisify result:", result); +console.log("promisify outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/zlib-brotli.ts b/test-parity/node-suite/async_hooks/integrations/zlib-brotli.ts new file mode 100644 index 0000000000..1e3a5e8007 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/zlib-brotli.ts @@ -0,0 +1,31 @@ +import { brotliCompress, brotliDecompress } from "node:zlib"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const compressed = await storage.run( + "brotli-compress", + () => + new Promise((resolve, reject) => { + brotliCompress("brotli-payload", (error, data) => { + console.log("brotli compress store:", storage.getStore()); + if (error) return reject(error); + resolve(data); + }); + }), +); + +const restored = await storage.run( + "brotli-decompress", + () => + new Promise((resolve, reject) => { + brotliDecompress(compressed, (error, data) => { + console.log("brotli decompress store:", storage.getStore()); + if (error) return reject(error); + resolve(data); + }); + }), +); + +console.log("brotli result:", restored.toString()); +console.log("brotli outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/zlib-create-gzip.ts b/test-parity/node-suite/async_hooks/integrations/zlib-create-gzip.ts new file mode 100644 index 0000000000..b606d73cf3 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/zlib-create-gzip.ts @@ -0,0 +1,26 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { createGzip } from "node:zlib"; + +const storage = new AsyncLocalStorage(); +const events = await storage.run( + "gzip-stream", + () => + new Promise((resolve, reject) => { + const seen: string[] = []; + const gzip = createGzip(); + gzip.on("data", (chunk) => { + seen.push(`data:${storage.getStore()}:${chunk.length > 0}`); + }); + gzip.on("finish", () => { + seen.push(`finish:${storage.getStore()}`); + }); + gzip.on("end", () => { + seen.push(`end:${storage.getStore()}`); + resolve(seen); + }); + gzip.on("error", reject); + gzip.end("gzip-stream-payload"); + }), +); +console.log("gzip stream events:", events.join("|")); +console.log("gzip stream outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/zlib-deflate-inflate.ts b/test-parity/node-suite/async_hooks/integrations/zlib-deflate-inflate.ts new file mode 100644 index 0000000000..0b1c67e4dd --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/zlib-deflate-inflate.ts @@ -0,0 +1,28 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { deflate, inflate } from "node:zlib"; + +const storage = new AsyncLocalStorage(); +const compressed = await storage.run( + "deflate", + () => + new Promise((resolve, reject) => { + deflate("deflate-payload", (error, data) => { + console.log("deflate store:", storage.getStore()); + if (error) return reject(error); + resolve(data); + }); + }), +); +const restored = await storage.run( + "inflate", + () => + new Promise((resolve, reject) => { + inflate(compressed, (error, data) => { + console.log("inflate store:", storage.getStore()); + if (error) return reject(error); + resolve(data); + }); + }), +); +console.log("inflate result:", restored.toString()); +console.log("deflate outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/integrations/zlib-gzip-gunzip.ts b/test-parity/node-suite/async_hooks/integrations/zlib-gzip-gunzip.ts new file mode 100644 index 0000000000..82782cf029 --- /dev/null +++ b/test-parity/node-suite/async_hooks/integrations/zlib-gzip-gunzip.ts @@ -0,0 +1,31 @@ +import { gzip, gunzip } from "node:zlib"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const compressed = await storage.run( + "gzip", + () => + new Promise((resolve, reject) => { + gzip("compressed-payload", (error, data) => { + console.log("gzip store:", storage.getStore()); + if (error) return reject(error); + resolve(data); + }); + }), +); + +const restored = await storage.run( + "gunzip", + () => + new Promise((resolve, reject) => { + gunzip(compressed, (error, data) => { + console.log("gunzip store:", storage.getStore()); + if (error) return reject(error); + resolve(data); + }); + }), +); + +console.log("gunzip result:", restored.toString()); +console.log("zlib outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/module-export-surface.ts b/test-parity/node-suite/async_hooks/module-export-surface.ts new file mode 100644 index 0000000000..19719fd01b --- /dev/null +++ b/test-parity/node-suite/async_hooks/module-export-surface.ts @@ -0,0 +1,42 @@ +import asyncHooksDefault from "node:async_hooks"; +import * as asyncHooks from "node:async_hooks"; + +const expected = [ + "AsyncLocalStorage", + "AsyncResource", + "asyncWrapProviders", + "createHook", + "default", + "executionAsyncId", + "executionAsyncResource", + "triggerAsyncId", +]; +const keys = Object.keys(asyncHooks); +console.log("module export keys:", keys.join(",")); +console.log( + "module export set:", + keys.length === expected.length && + [...keys].sort().join(",") === [...expected].sort().join(","), +); +console.log( + "module export descriptors:", + expected.every((name) => { + const descriptor = Object.getOwnPropertyDescriptor(asyncHooks, name); + return ( + !!descriptor && + descriptor.enumerable === true && + descriptor.configurable === false && + descriptor.writable === true + ); + }), +); +const originalCreateHook = asyncHooks.createHook; +console.log( + "module namespace immutable:", + Reflect.set(asyncHooks, "createHook", null), + asyncHooks.createHook === originalCreateHook, +); +console.log( + "default export keys:", + Object.keys(asyncHooksDefault).sort().join(","), +); diff --git a/test-parity/node-suite/async_hooks/module-identity.ts b/test-parity/node-suite/async_hooks/module-identity.ts new file mode 100644 index 0000000000..df3120de12 --- /dev/null +++ b/test-parity/node-suite/async_hooks/module-identity.ts @@ -0,0 +1,21 @@ +import asyncHooksDefault from "async_hooks"; +import * as bare from "async_hooks"; +import * as prefixed from "node:async_hooks"; + +const names = [ + "AsyncLocalStorage", + "AsyncResource", + "createHook", + "executionAsyncId", + "executionAsyncResource", + "triggerAsyncId", + "asyncWrapProviders", +] as const; +console.log( + "specifier identities:", + names.map((name) => bare[name] === prefixed[name]).join(","), +); +console.log( + "default identities:", + names.map((name) => asyncHooksDefault[name] === prefixed[name]).join(","), +); diff --git a/test-parity/node-suite/async_hooks/module-namespace-brand.ts b/test-parity/node-suite/async_hooks/module-namespace-brand.ts new file mode 100644 index 0000000000..67e9174fa1 --- /dev/null +++ b/test-parity/node-suite/async_hooks/module-namespace-brand.ts @@ -0,0 +1,13 @@ +import * as bare from "async_hooks"; +import * as prefixed from "node:async_hooks"; + +console.log( + "namespace tags:", + Object.prototype.toString.call(bare), + Object.prototype.toString.call(prefixed), +); +console.log( + "namespace toStringTag:", + bare[Symbol.toStringTag], + prefixed[Symbol.toStringTag], +); diff --git a/test-parity/node-suite/async_hooks/propagation/async-iterator.ts b/test-parity/node-suite/async_hooks/propagation/async-iterator.ts new file mode 100644 index 0000000000..d9ce053a57 --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/async-iterator.ts @@ -0,0 +1,23 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +async function* values() { + console.log("generator start:", storage.getStore()); + yield 1; + await Promise.resolve(); + console.log("generator continuation:", storage.getStore()); + yield 2; +} + +const seen = await storage.run("iterator", async () => { + const output: number[] = []; + for await (const value of values()) { + console.log("iterator value:", value, storage.getStore()); + output.push(value); + } + return output.join(","); +}); + +console.log("iterator result:", seen); +console.log("iterator outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/concurrent-promises.ts b/test-parity/node-suite/async_hooks/propagation/concurrent-promises.ts new file mode 100644 index 0000000000..ea9d6ea1d4 --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/concurrent-promises.ts @@ -0,0 +1,39 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +function deferred() { + let resolve!: () => void; + const promise = new Promise((done) => { + resolve = done; + }); + return { promise, resolve }; +} + +const firstGate = deferred(); +const secondGate = deferred(); +const thirdGate = deferred(); + +const first = storage.run("first", async () => { + await firstGate.promise; + console.log("first continuation:", storage.getStore()); + return "first-result"; +}); +const second = storage.run("second", async () => { + await secondGate.promise; + console.log("second continuation:", storage.getStore()); + return "second-result"; +}); +const third = storage.run("third", async () => { + await thirdGate.promise; + console.log("third continuation:", storage.getStore()); + return "third-result"; +}); + +secondGate.resolve(); +console.log("second result:", await second); +firstGate.resolve(); +console.log("first result:", await first); +thirdGate.resolve(); +console.log("third result:", await third); +console.log("concurrent outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/disable-pending.ts b/test-parity/node-suite/async_hooks/propagation/disable-pending.ts new file mode 100644 index 0000000000..9a26606839 --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/disable-pending.ts @@ -0,0 +1,28 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +let tickDone!: Promise; +let timerDone!: Promise; + +storage.run("pending", () => { + tickDone = new Promise((resolve) => { + process.nextTick(() => { + console.log("disabled nextTick:", String(storage.getStore())); + resolve(); + }); + }); + + timerDone = new Promise((resolve) => { + setTimeout(() => { + console.log("disabled timer:", String(storage.getStore())); + resolve(); + }, 0); + }); + + storage.disable(); + console.log("disabled synchronously:", String(storage.getStore())); +}); + +await Promise.all([tickDone, timerDone]); +console.log("disabled pending complete:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/dynamic-import.ts b/test-parity/node-suite/async_hooks/propagation/dynamic-import.ts new file mode 100644 index 0000000000..7c183c106c --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/dynamic-import.ts @@ -0,0 +1,17 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +(globalThis as any).parityDynamicImportStore = () => storage.getStore(); +const source = + "data:text/javascript,export const store = globalThis.parityDynamicImportStore()"; +let observed: string | undefined; +try { + observed = await storage.run("dynamic-import", async () => { + const namespace = await import(source); + return namespace.store; + }); +} finally { + delete (globalThis as any).parityDynamicImportStore; +} +console.log("dynamic import store:", observed); +console.log("dynamic import outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/fetch-local.ts b/test-parity/node-suite/async_hooks/propagation/fetch-local.ts new file mode 100644 index 0000000000..14964f8204 --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/fetch-local.ts @@ -0,0 +1,38 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { createServer } from "node:http"; + +const storage = new AsyncLocalStorage(); +const server = createServer((_request, response) => response.end("fetch-ok")); +let thenStore: string | undefined; +let afterAwaitStore: string | undefined; +let body = ""; + +try { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("missing fetch address"); + + await storage.run("fetch-context", async () => { + const response = await fetch(`http://127.0.0.1:${address.port}`).then( + (value) => { + thenStore = storage.getStore(); + return value; + }, + ); + body = await response.text(); + afterAwaitStore = storage.getStore(); + }); +} finally { + if (server.listening) { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } +} + +console.log("fetch stores:", thenStore, afterAwaitStore); +console.log("fetch body/outside:", body, String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/immediate-interval.ts b/test-parity/node-suite/async_hooks/propagation/immediate-interval.ts new file mode 100644 index 0000000000..24244c346d --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/immediate-interval.ts @@ -0,0 +1,32 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +await storage.run( + "immediate", + () => + new Promise((resolve) => { + setImmediate(() => { + console.log("immediate store:", storage.getStore()); + resolve(); + }); + }), +); + +await storage.run( + "interval", + () => + new Promise((resolve) => { + let count = 0; + const interval = setInterval(() => { + count++; + console.log("interval store:", count, storage.getStore()); + if (count === 2) { + clearInterval(interval); + resolve(); + } + }, 1); + }), +); + +console.log("timer variants outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/nested-nexttick.ts b/test-parity/node-suite/async_hooks/propagation/nested-nexttick.ts new file mode 100644 index 0000000000..e470bb95b0 --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/nested-nexttick.ts @@ -0,0 +1,18 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const stores = await storage.run( + "nested-tick", + () => + new Promise((resolve) => { + process.nextTick(() => { + const seen = [String(storage.getStore())]; + process.nextTick(() => { + seen.push(String(storage.getStore())); + resolve(seen); + }); + }); + }), +); +console.log("nested nextTick stores:", stores.join(",")); +console.log("nested nextTick outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/nexttick-timer.ts b/test-parity/node-suite/async_hooks/propagation/nexttick-timer.ts new file mode 100644 index 0000000000..45587dbadc --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/nexttick-timer.ts @@ -0,0 +1,25 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const completed = storage.run( + "scheduled", + () => + new Promise((resolve) => { + process.nextTick(() => { + console.log("nextTick store:", storage.getStore()); + + setTimeout(() => { + console.log("timer store:", storage.getStore()); + resolve("done"); + }, 0); + }); + }), +); + +storage.enterWith("caller"); +console.log("caller store:", storage.getStore()); +console.log("barrier result:", await completed); +console.log("caller restored:", storage.getStore()); + +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/propagation/promise-catch-finally.ts b/test-parity/node-suite/async_hooks/propagation/promise-catch-finally.ts new file mode 100644 index 0000000000..ba4d757fad --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/promise-catch-finally.ts @@ -0,0 +1,17 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const result = await storage.run("chain", () => + Promise.reject(new Error("expected")) + .catch((error) => { + console.log("catch store:", storage.getStore(), error.message); + return "recovered"; + }) + .finally(() => { + console.log("finally store:", storage.getStore()); + }), +); + +console.log("chain result:", result); +console.log("chain outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/promise-microtask.ts b/test-parity/node-suite/async_hooks/propagation/promise-microtask.ts new file mode 100644 index 0000000000..8fd4fa49ac --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/promise-microtask.ts @@ -0,0 +1,26 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +async function main() { + const returned = await storage.run("promise", async () => { + console.log("promise start:", storage.getStore()); + await Promise.resolve(); + console.log("promise continuation:", storage.getStore()); + + await new Promise((resolve) => { + queueMicrotask(() => { + console.log("queueMicrotask callback:", storage.getStore()); + resolve(); + }); + }); + + console.log("after queueMicrotask:", storage.getStore()); + return "completed"; + }); + + console.log("async return:", returned); + console.log("outside after await:", String(storage.getStore())); +} + +await main(); diff --git a/test-parity/node-suite/async_hooks/propagation/thenable-return-paths.ts b/test-parity/node-suite/async_hooks/propagation/thenable-return-paths.ts new file mode 100644 index 0000000000..b5e8f3c8e9 --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/thenable-return-paths.ts @@ -0,0 +1,41 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +function makeThenable(label: string, stores: string[]) { + return { + then(resolve: (value: string) => void) { + stores.push(storage.getStore() ?? "missing"); + setImmediate(() => { + stores.push(storage.getStore() ?? "missing"); + resolve(`${label}-value`); + }); + }, + }; +} + +const asyncReturnStores: string[] = []; +const asyncReturnValue = await storage.run("async-return", async () => + makeThenable("async-return", asyncReturnStores), +); +console.log( + "async return thenable:", + asyncReturnStores.join(","), + asyncReturnValue, +); + +const handlerReturnStores: string[] = []; +const handlerReturnValue = await storage.run("handler-return", () => + Promise.resolve() + .then(() => makeThenable("handler-return", handlerReturnStores)) + .then((value) => { + handlerReturnStores.push(storage.getStore() ?? "missing"); + return value; + }), +); +console.log( + "handler return thenable:", + handlerReturnStores.join(","), + handlerReturnValue, +); +console.log("thenable return outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/thenable.ts b/test-parity/node-suite/async_hooks/propagation/thenable.ts new file mode 100644 index 0000000000..074df1157b --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/thenable.ts @@ -0,0 +1,31 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +function makeThenable(label: string) { + return { + then(resolve: (value: string) => void) { + console.log(label, "then store:", storage.getStore()); + queueMicrotask(() => { + console.log(label, "resolve store:", storage.getStore()); + resolve(`${label}-value`); + }); + }, + }; +} + +const awaited = await storage.run("await-context", async () => { + const value = await makeThenable("await"); + console.log("await continuation:", storage.getStore(), value); + return value; +}); +console.log("await result:", awaited); + +const resolved = await storage.run("resolve-context", () => + Promise.resolve(makeThenable("resolve")).then((value) => { + console.log("resolve continuation:", storage.getStore(), value); + return value; + }), +); +console.log("resolve result:", resolved); +console.log("thenable outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/timer-ref-unref.ts b/test-parity/node-suite/async_hooks/propagation/timer-ref-unref.ts new file mode 100644 index 0000000000..6eb1045cc6 --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/timer-ref-unref.ts @@ -0,0 +1,19 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +await storage.run( + "ref-unref", + () => + new Promise((resolve) => { + const timer = setTimeout(() => { + console.log("ref/unref timer store:", storage.getStore()); + resolve(); + }, 5); + console.log("timer initially refed:", timer.hasRef()); + timer.unref(); + console.log("timer after unref:", timer.hasRef()); + timer.ref(); + console.log("timer after ref:", timer.hasRef()); + }), +); +console.log("ref/unref outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/vm-context.ts b/test-parity/node-suite/async_hooks/propagation/vm-context.ts new file mode 100644 index 0000000000..d2179573e7 --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/vm-context.ts @@ -0,0 +1,21 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { runInNewContext } from "node:vm"; + +const storage = new AsyncLocalStorage(); +const observed = await storage.run( + "vm-context", + () => + new Promise((resolve) => { + runInNewContext( + "setImmediate(() => resolve(String(storage.getStore())))", + { + resolve, + setImmediate, + storage, + String, + }, + ); + }), +); +console.log("vm immediate store:", observed); +console.log("vm outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/propagation/web-locks.ts b/test-parity/node-suite/async_hooks/propagation/web-locks.ts new file mode 100644 index 0000000000..a12e999cc3 --- /dev/null +++ b/test-parity/node-suite/async_hooks/propagation/web-locks.ts @@ -0,0 +1,34 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import * as workerThreads from "node:worker_threads"; + +type Locks = { + request( + name: string, + callback: (lock: { name: string; mode: string }) => T | Promise, + ): Promise; +}; +const locks = (workerThreads as any).locks as Locks | undefined; +const available = typeof locks?.request === "function"; +console.log("web locks availability:", available); + +if (locks) { + const storage = new AsyncLocalStorage(); + const contexts: Array = []; + const result = await storage.run("web-lock", () => + locks.request("async-hooks-outer", async (outer) => { + contexts.push(storage.getStore()); + await Promise.resolve(); + contexts.push(storage.getStore()); + const inner = await locks.request("async-hooks-inner", async (lock) => { + await new Promise((resolve) => setImmediate(resolve)); + contexts.push(storage.getStore()); + return `${lock.name}:${lock.mode}`; + }); + return `${outer.name}:${outer.mode}|${inner}`; + }), + ); + console.log("web locks contexts:", contexts.join(",")); + console.log("web locks result:", result); + console.log("web locks outside:", String(storage.getStore())); + storage.disable(); +} diff --git a/test-parity/node-suite/async_hooks/providers/child-exec-file.ts b/test-parity/node-suite/async_hooks/providers/child-exec-file.ts new file mode 100644 index 0000000000..d2d9f51a07 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/child-exec-file.ts @@ -0,0 +1,19 @@ +import { execFile } from "node:child_process"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const output = await storage.run( + "child-exec-file", + () => + new Promise((resolve, reject) => { + execFile("/bin/sh", ["-c", "printf child-file"], (error, stdout) => { + console.log("child execFile store:", storage.getStore()); + if (error) return reject(error); + resolve(stdout); + }); + }), +); + +console.log("child execFile output:", output); +console.log("child execFile outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/child-exec.ts b/test-parity/node-suite/async_hooks/providers/child-exec.ts new file mode 100644 index 0000000000..1102a736dd --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/child-exec.ts @@ -0,0 +1,19 @@ +import { exec } from "node:child_process"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const output = await storage.run( + "child-exec", + () => + new Promise((resolve, reject) => { + exec("printf child-exec", (error, stdout) => { + console.log("child exec store:", storage.getStore()); + if (error) return reject(error); + resolve(stdout); + }); + }), +); + +console.log("child exec output:", output); +console.log("child exec outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/child-spawn-events.ts b/test-parity/node-suite/async_hooks/providers/child-spawn-events.ts new file mode 100644 index 0000000000..a3999ee280 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/child-spawn-events.ts @@ -0,0 +1,28 @@ +import { spawn } from "node:child_process"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const result = await storage.run( + "child-spawn", + () => + new Promise((resolve, reject) => { + const chunks: string[] = []; + const child = spawn("/bin/sh", ["-c", "printf spawned"]); + child.on("spawn", () => { + console.log("child spawn event store:", storage.getStore()); + }); + child.stdout.on("data", (chunk) => { + console.log("child stdout store:", storage.getStore()); + chunks.push(String(chunk)); + }); + child.on("error", reject); + child.on("close", (code) => { + console.log("child close store:", storage.getStore(), code); + resolve(chunks.join("")); + }); + }), +); + +console.log("child spawn output:", result); +console.log("child spawn outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/dgram-dual-context.ts b/test-parity/node-suite/async_hooks/providers/dgram-dual-context.ts new file mode 100644 index 0000000000..6d27c4bf4d --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/dgram-dual-context.ts @@ -0,0 +1,55 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { createSocket } from "node:dgram"; + +const storage = new AsyncLocalStorage(); +let serverMessageStore: string | undefined; +let clientSendStore: string | undefined; +let clientMessageStore: string | undefined; +const server = storage.run("server-context", () => createSocket("udp4")); +const client = storage.run("client-context", () => createSocket("udp4")); + +function closeSocket(socket: typeof server) { + return new Promise((resolve) => { + try { + socket.close(resolve); + } catch { + resolve(); + } + }); +} + +try { + await new Promise((resolve, reject) => { + server.once("error", reject); + client.once("error", reject); + server.on("message", (message, remote) => { + serverMessageStore = storage.getStore(); + server.send(message, remote.port, remote.address); + }); + client.once("message", () => { + clientMessageStore = storage.getStore(); + resolve(); + }); + storage.run("server-context", () => { + server.bind(0, "127.0.0.1", () => { + const address = server.address(); + storage.run("client-context", () => { + client.send("dual-context", address.port, "127.0.0.1", (error) => { + clientSendStore = storage.getStore(); + if (error) reject(error); + }); + }); + }); + }); + }); +} finally { + await Promise.all([closeSocket(client), closeSocket(server)]); +} + +console.log( + "dgram dual stores:", + serverMessageStore, + clientSendStore, + clientMessageStore, +); +console.log("dgram dual outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/dgram-events.ts b/test-parity/node-suite/async_hooks/providers/dgram-events.ts new file mode 100644 index 0000000000..26cfcdfcef --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/dgram-events.ts @@ -0,0 +1,31 @@ +import { createSocket } from "node:dgram"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +await storage.run( + "dgram-events", + () => + new Promise((resolve, reject) => { + const server = createSocket("udp4"); + const client = createSocket("udp4"); + server.on("error", reject); + client.on("error", reject); + server.on("listening", () => { + console.log("dgram listening store:", storage.getStore()); + client.send("payload", server.address().port, "127.0.0.1"); + }); + server.on("message", () => { + console.log("dgram message store:", storage.getStore()); + client.close(); + server.close(); + }); + server.on("close", () => { + console.log("dgram close store:", storage.getStore()); + resolve(); + }); + server.bind(0, "127.0.0.1"); + }), +); + +console.log("dgram events outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/dgram-send.ts b/test-parity/node-suite/async_hooks/providers/dgram-send.ts new file mode 100644 index 0000000000..7676eb13e3 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/dgram-send.ts @@ -0,0 +1,29 @@ +import { createSocket } from "node:dgram"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +await storage.run( + "dgram-send", + () => + new Promise((resolve, reject) => { + const server = createSocket("udp4"); + const client = createSocket("udp4"); + server.on("error", reject); + client.on("error", reject); + server.on("message", () => { + server.close(); + client.close(); + resolve(); + }); + server.bind(0, "127.0.0.1", () => { + const address = server.address(); + client.send("payload", address.port, "127.0.0.1", (error) => { + console.log("dgram send callback store:", storage.getStore()); + if (error) reject(error); + }); + }); + }), +); + +console.log("dgram send outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/dns-lookup-service.ts b/test-parity/node-suite/async_hooks/providers/dns-lookup-service.ts new file mode 100644 index 0000000000..75c1945339 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/dns-lookup-service.ts @@ -0,0 +1,28 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { lookupService } from "node:dns"; + +const storage = new AsyncLocalStorage(); +let callbackInvoked = false; +let resultShapeValid = false; + +await storage.run( + "lookup-service", + () => + new Promise((resolve) => { + lookupService("127.0.0.1", 80, (error, hostname, service) => { + callbackInvoked = true; + resultShapeValid = + error !== null || + (typeof hostname === "string" && typeof service === "string"); + console.log("lookupService store:", storage.getStore()); + resolve(); + }); + }), +); + +console.log( + "lookupService callback/result:", + callbackInvoked, + resultShapeValid, +); +console.log("lookupService outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/dns-lookup.ts b/test-parity/node-suite/async_hooks/providers/dns-lookup.ts new file mode 100644 index 0000000000..0d0a238b55 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/dns-lookup.ts @@ -0,0 +1,20 @@ +import { lookup } from "node:dns"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const family = await storage.run( + "dns-lookup", + () => + new Promise((resolve, reject) => { + lookup("localhost", (error, address, addressFamily) => { + console.log("dns lookup store:", storage.getStore()); + if (error) return reject(error); + console.log("dns lookup address type:", typeof address); + resolve(addressFamily); + }); + }), +); + +console.log("dns lookup family valid:", family === 4 || family === 6); +console.log("dns lookup outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/dns-resolve4.ts b/test-parity/node-suite/async_hooks/providers/dns-resolve4.ts new file mode 100644 index 0000000000..cb2e8a8107 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/dns-resolve4.ts @@ -0,0 +1,22 @@ +import { resolve4 } from "node:dns"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const completed = await storage.run( + "dns-resolve4", + () => + new Promise((resolve) => { + resolve4("localhost", (error, addresses) => { + console.log("dns resolve4 store:", storage.getStore()); + console.log( + "dns resolve4 completed:", + error ? "error" : Array.isArray(addresses), + ); + resolve("done"); + }); + }), +); + +console.log("dns resolve4 result:", completed); +console.log("dns resolve4 outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/events-on-async.ts b/test-parity/node-suite/async_hooks/providers/events-on-async.ts new file mode 100644 index 0000000000..b8eae632cb --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/events-on-async.ts @@ -0,0 +1,25 @@ +import { EventEmitter, on } from "node:events"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const emitter = new EventEmitter(); + +const result = await storage.run("events-on", async () => { + process.nextTick(() => { + emitter.emit("value", "first"); + emitter.emit("value", "second"); + emitter.emit("end"); + }); + + const output: string[] = []; + for await (const [value] of on(emitter, "value", { + close: ["end"], + })) { + console.log("events.on iterator store:", storage.getStore(), value); + output.push(String(value)); + } + return output.join(","); +}); + +console.log("events.on result:", result); +console.log("events.on outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/fixtures/context-worker.cjs b/test-parity/node-suite/async_hooks/providers/fixtures/context-worker.cjs new file mode 100644 index 0000000000..826226f0d6 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/fixtures/context-worker.cjs @@ -0,0 +1,6 @@ +const { parentPort } = require("node:worker_threads"); + +parentPort.postMessage({ phase: "ready" }); +parentPort.on("message", (message) => { + parentPort.postMessage({ phase: "reply", value: message.value + 1 }); +}); diff --git a/test-parity/node-suite/async_hooks/providers/fixtures/tls-credentials.js b/test-parity/node-suite/async_hooks/providers/fixtures/tls-credentials.js new file mode 100644 index 0000000000..38b80936f7 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/fixtures/tls-credentials.js @@ -0,0 +1,50 @@ +export const KEY = `-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCN6SnKGIrSrgfx +NCjTUlMDnrgVmk6K2DC8hgx7eqF/R0WWjYJuPElzHkY5DnuTq9w2Ut8gvbJXx/uE +k24WV8sAW+2b2mbjkG9uw+925bUC5IjegD9+l+xDkrVWAA9stkWF2d0KOgZbf9k7 +w9tQLkNOB7mW4J9ehXTMXNkBNsnHOLJBn2diylznxJ7pePQqEzdcLLZLtXnBU9Fe +jachbmGjzfId8rJXIoR7DueWjGGs0BiJHAfNXMG8Ki12Bkr+UGbWqy6AezfUtHpP +vmLNAaigg3XDPZX2SIcJb6zcKaAULLbkBs3njvEvCfh2OHlPPiUmEDhGHQPQX+/N +x4vlOgkHAgMBAAECggEACFfV8iDBQKOkqeSkJdBoOwVA01xQE8+kBeFnqHbMOdxp +1fEZ4vs+Yjs8a6xTTZpEBxmWLqmYa5rBSckVJtEgiTPeY1RSyjw6oOt6D6Zvnuzq +sxIdKYcrB8n/SUAVqBGLQtRNL4W7y/NXRTE9mpgtss+3dIxeMkNsW3t18qFS+Zhg +TP8q984k+zl3QOz6sc5T39Unuk1g98LC2sjCXwKANzZRMBMigoGDnWgk9t86cEXM +YWmyStS89HKEDmxWQMRIc/6zw5YC9Jo0cF2OJxGtN/O+LLeeNoJcdnlSAcyYRU1Q +asJhtNkMwfMRrTVH0kQfF5X3a/aJfusiJnQBcvlVeQKBgQDF8rI8dlaQ7jNOfSoZ +FhphZe1DriFaulRA9PUwrxEb/qvRstre0Egu967ILmqoqKfufyNT4W5JnWngliN6 +S7D9cvxpW0RsUQHZXMqZp7s6kt4hAdziuyC2Wx2y6+zFHkbOwJcaULYrSNHJCPOj +cMu5TIplum+hnO9rMHKEpE0fAwKBgQC3h17rEy4uFbWPQD3fNjAi9QzIKX9wm8eD +SYekgZaHpAjrLCa8oNR6qMxU5Cpn7I3o2HegSUe29jDAr8GMp47JYRTGMHUl1Zwa +KtSGEH19sRhVUqIVW2h2/tysuaYpK1hFjPWM+KpKQFNzgt2EPf5057zE7gOHLcAL +UccMgP1crQKBgQCy4h1SaHrYZHq3LoNRwli6thrRc9YuoH4taXD+uuaSTvZE/gWv +H7hrwWcQ/mli229PJ1PspKc/HWMmE2giR669jCEwsMrHu/kYzjNE4oBfcYQNfhp4 +RzVLtlHDdFM226KPixnCLThDK35x14YdqHxiixnyzqW8/g6a5mBHIBeVswKBgQCT +y79DndGdqTvqHbj1zWScci0V8F1BqSHVd1x1vSolF5NbF9YmJ3qVQOQ0JP6FbHmn +ntNPUFQhYkdGlQNQKwuQ3s5lAFcG3ev1IrK9OABnPTu0UnRWsKMC2SGLM4I9Ozu9 +3tNL8GDqpLzPk/6h5W7KZGifSnGq5cv3EaczSZk/jQKBgAcaLGi25ozeFgK1qvuQ +WFTjLYV6KaMrGd5+NF+2a/NQsDGTZSF1egKUvE5QH5YNf37xWkqwvR3rsbenxLAG +aNYjvX+bUs4Mc/bgNkO51P9sH6YoKsuFzTTx4eR5ZS+dtfoiZMfzKkRBK4Baggrv +7S9Q3thVBhvBcz19oFN2Rmvf +-----END PRIVATE KEY----- +`; + +export const CERT = `-----BEGIN CERTIFICATE----- +MIIDJTCCAg2gAwIBAgIUZF3wbyk6BduDu+lEeegKd2ULMK8wDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDUyNDE3NDI1NloXDTM2MDUy +MTE3NDI1NlowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAjekpyhiK0q4H8TQo01JTA564FZpOitgwvIYMe3qhf0dF +lo2CbjxJcx5GOQ57k6vcNlLfIL2yV8f7hJNuFlfLAFvtm9pm45BvbsPvduW1AuSI +3oA/fpfsQ5K1VgAPbLZFhdndCjoGW3/ZO8PbUC5DTge5luCfXoV0zFzZATbJxziy +QZ9nYspc58Se6Xj0KhM3XCy2S7V5wVPRXo2nIW5ho83yHfKyVyKEew7nloxhrNAY +iRwHzVzBvCotdgZK/lBm1qsugHs31LR6T75izQGooIN1wz2V9kiHCW+s3CmgFCy2 +5AbN547xLwn4djh5Tz4lJhA4Rh0D0F/vzceL5ToJBwIDAQABo28wbTAdBgNVHQ4E +FgQU1s+brNmcdkCqkncnW6rNlJpdiP0wHwYDVR0jBBgwFoAU1s+brNmcdkCqkncn +W6rNlJpdiP0wDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SH +BH8AAAEwDQYJKoZIhvcNAQELBQADggEBAHFmvSxFCTHcqiocEHF3i0seBmNwWq40 +TtyVf9qyZYUZVqM/Z7tGDsNfNOhM+YscLs1ZTs8XzdpdYBEVyCLDYGjb4Cv6r5gS +hr+E0NQBnPuker6Rw64nzahfWYjf/Eo+7nwUbCahTbXHAs43c4m0bmL02r1NxVmv +BKGQKO/uR9Dy+3TKykNQkacKJ6oDxdTDovMUKlbwU/HlyzwK/HTm762cJfgZiMYM +uru8x9wmqogCQSAz2q6a6q/CZfn1o7S5KiWd0FzinP+50g5cSL/ob0GJ8Jge1oI5 +5rap/3DFfnTn0zfJ60U52+BVFnOIqkYT7/g5N4laGrza73tYXq7FV4s= +-----END CERTIFICATE----- +`; diff --git a/test-parity/node-suite/async_hooks/providers/http-agent-isolation.ts b/test-parity/node-suite/async_hooks/providers/http-agent-isolation.ts new file mode 100644 index 0000000000..95923b2b44 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/http-agent-isolation.ts @@ -0,0 +1,38 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { Agent, createServer, get } from "node:http"; + +const storage = new AsyncLocalStorage(); +const server = createServer((_request, response) => response.end("ok")); +const agent = new Agent({ keepAlive: true, maxSockets: 1 }); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +if (!address || typeof address === "string") + throw new Error("missing HTTP address"); + +const stores: number[] = []; +try { + for (let index = 0; index < 3; index++) { + await storage.run( + index, + () => + new Promise((resolve, reject) => { + const req = get( + { agent, host: "127.0.0.1", port: address.port }, + (response) => { + stores.push(storage.getStore() ?? -1); + response.resume(); + response.on("end", resolve); + }, + ); + req.on("error", reject); + }), + ); + } +} finally { + agent.destroy(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); +} +console.log("http agent stores:", stores.join(",")); +console.log("http agent outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/http-client-events.ts b/test-parity/node-suite/async_hooks/providers/http-client-events.ts new file mode 100644 index 0000000000..ce9cb41c88 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/http-client-events.ts @@ -0,0 +1,47 @@ +import { createServer, request } from "node:http"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const server = createServer((incoming, response) => { + incoming.resume(); + response.end("ok"); +}); + +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +if (!address || typeof address === "string") + throw new Error("missing server address"); + +let requestHandle: ReturnType | undefined; +try { + await storage.run( + "http-client-events", + () => + new Promise((resolve, reject) => { + requestHandle = request({ + host: "127.0.0.1", + port: address.port, + method: "POST", + }); + requestHandle.on("response", (response) => { + console.log("client response event store:", storage.getStore()); + response.resume(); + }); + requestHandle.on("finish", () => { + console.log("client finish event store:", storage.getStore()); + }); + requestHandle.on("close", () => { + console.log("client close event store:", storage.getStore()); + resolve(); + }); + requestHandle.on("error", reject); + requestHandle.end("request-body"); + }), + ); +} finally { + requestHandle?.destroy(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); +} +console.log("http client outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/http-execution-resource.ts b/test-parity/node-suite/async_hooks/providers/http-execution-resource.ts new file mode 100644 index 0000000000..910eea3135 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/http-execution-resource.ts @@ -0,0 +1,126 @@ +import { + createHook, + executionAsyncId, + executionAsyncResource, +} from "node:async_hooks"; +import { get, createServer } from "node:http"; + +const resources = new Map(); +const providerTypes = new Set(["HTTPCLIENTREQUEST", "HTTPINCOMINGMESSAGE"]); +type ProviderEntry = { + id: number; + type: string; + before: number; + after: number; + destroy: number; +}; +const providerEntries: ProviderEntry[] = []; +const providerById = new Map(); +const hook = createHook({ + init(asyncId, type, _triggerAsyncId, resource) { + resources.set(asyncId, resource); + if (providerTypes.has(type)) { + const entry = { id: asyncId, type, before: 0, after: 0, destroy: 0 }; + providerEntries.push(entry); + providerById.set(asyncId, entry); + } + }, + before(asyncId) { + const entry = providerById.get(asyncId); + if (entry) entry.before++; + }, + after(asyncId) { + const entry = providerById.get(asyncId); + if (entry) entry.after++; + }, + destroy(asyncId) { + const entry = providerById.get(asyncId); + if (entry) entry.destroy++; + }, +}).enable(); +let incomingProviderMapped = false; +const server = createServer((_request, response) => { + incomingProviderMapped = providerEntries.some( + (entry) => + entry.type === "HTTPINCOMINGMESSAGE" && + entry.id === executionAsyncId() && + resources.get(entry.id) === executionAsyncResource(), + ); + response.end("ok"); +}); +let request: ReturnType | undefined; +let completed = false; +let clientProviderMapped = false; + +try { + await new Promise((resolve, reject) => { + const timeout = setTimeout(resolve, 1_000); + const finish = () => { + completed = true; + clearTimeout(timeout); + resolve(); + }; + const fail = (error: Error) => { + clearTimeout(timeout); + reject(error); + }; + server.once("error", fail); + server.listen(0, "127.0.0.1", () => { + console.log( + "http listen resource mapped:", + executionAsyncResource() === resources.get(executionAsyncId()), + ); + const address = server.address(); + if (!address || typeof address === "string") { + fail(new Error("missing address")); + return; + } + request = get({ host: "127.0.0.1", port: address.port }, (response) => { + clientProviderMapped = providerEntries.some( + (entry) => + entry.type === "HTTPCLIENTREQUEST" && + entry.id === executionAsyncId() && + resources.get(entry.id) === executionAsyncResource(), + ); + console.log( + "http response resource mapped:", + executionAsyncResource() === resources.get(executionAsyncId()), + ); + response.once("error", fail); + response.resume(); + response.once("end", finish); + }); + request.once("error", fail); + }); + }); +} finally { + request?.destroy(); + if (server.listening) { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } + await new Promise((resolve) => setImmediate(resolve)); + await new Promise((resolve) => setImmediate(resolve)); + hook.disable(); +} +console.log("http execution resource completed:", completed); +console.log( + "http provider mappings:", + clientProviderMapped, + incomingProviderMapped, +); +for (const type of providerTypes) { + const selected = providerEntries.filter((entry) => entry.type === type); + console.log( + `${type} provider lifecycle:`, + selected.length, + selected.length === 1 && + selected.every( + (entry) => + entry.before > 0 && + entry.before === entry.after && + entry.destroy === 1, + ), + ); +} diff --git a/test-parity/node-suite/async_hooks/providers/http-multiclient-isolation.ts b/test-parity/node-suite/async_hooks/providers/http-multiclient-isolation.ts new file mode 100644 index 0000000000..5fdbf704dc --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/http-multiclient-isolation.ts @@ -0,0 +1,102 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { createServer, get } from "node:http"; + +type ClientStore = { id: string; body: string }; +const storage = new AsyncLocalStorage(); +const responseStores = new Set(); +const dataStores = new Set(); +const endStores = new Set(); +let missingResponseStore = false; +let missingDataStore = false; +let missingEndStore = false; + +function recordResponseStore() { + const store = storage.getStore(); + if (store) responseStores.add(store.id); + else missingResponseStore = true; +} +function onData(chunk: Buffer) { + const store = storage.getStore(); + if (store) { + dataStores.add(store.id); + store.body += String(chunk); + } else { + missingDataStore = true; + } +} +function recordEndStore() { + const store = storage.getStore(); + if (store) endStores.add(store.id); + else missingEndStore = true; +} + +const server = createServer((request, response) => { + const id = request.url?.slice(1) || "missing"; + response.write(id.repeat(64)); + setImmediate(() => response.end(id.repeat(64))); +}); +const requests: ReturnType[] = []; +const ids = ["0", "1", "2", "3", "4"]; +let bodies: string[] = []; +try { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("missing HTTP address"); + bodies = await Promise.all( + ids.map((id) => { + const store: ClientStore = { id, body: "" }; + return storage.run( + store, + () => + new Promise((resolve, reject) => { + const request = get( + { host: "127.0.0.1", port: address.port, path: `/${id}` }, + (response) => { + recordResponseStore(); + response.on("error", reject); + response.on("data", onData); + response.on("end", () => { + recordEndStore(); + resolve(store.body); + }); + }, + ); + requests.push(request); + request.on("error", reject); + }), + ); + }), + ); +} finally { + for (const request of requests) request.destroy(); + if (server.listening) { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } +} + +console.log( + "http concurrent response stores:", + [...responseStores].sort().join(","), + missingResponseStore, +); +console.log( + "http concurrent data stores:", + [...dataStores].sort().join(","), + missingDataStore, +); +console.log( + "http concurrent end stores:", + [...endStores].sort().join(","), + missingEndStore, +); +console.log( + "http concurrent bodies isolated:", + bodies.map((body, index) => body === ids[index].repeat(128)).join(","), +); +console.log("http concurrent outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/http-request.ts b/test-parity/node-suite/async_hooks/providers/http-request.ts new file mode 100644 index 0000000000..c4e760e36d --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/http-request.ts @@ -0,0 +1,47 @@ +import { createServer, request } from "node:http"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const server = createServer((incoming, response) => { + response.end("http-payload"); +}); + +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +if (!address || typeof address === "string") + throw new Error("missing server address"); + +let requestHandle: ReturnType | undefined; +let body: string; +try { + body = await storage.run( + "http-request", + () => + new Promise((resolve, reject) => { + requestHandle = request( + { host: "127.0.0.1", port: address.port, path: "/" }, + (response) => { + console.log("http response store:", storage.getStore()); + const chunks: string[] = []; + response.on("data", (chunk) => { + console.log("http data store:", storage.getStore()); + chunks.push(String(chunk)); + }); + response.on("end", () => { + console.log("http end store:", storage.getStore()); + resolve(chunks.join("")); + }); + }, + ); + requestHandle.on("error", reject); + requestHandle.end(); + }), + ); +} finally { + requestHandle?.destroy(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); +} +console.log("http body:", body); +console.log("http outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/https-agent-isolation.ts b/test-parity/node-suite/async_hooks/providers/https-agent-isolation.ts new file mode 100644 index 0000000000..2a39f1ba2f --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/https-agent-isolation.ts @@ -0,0 +1,47 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { Agent, createServer, get } from "node:https"; +import { CERT, KEY } from "./fixtures/tls-credentials.js"; + +const storage = new AsyncLocalStorage(); +const server = createServer({ cert: CERT, key: KEY }, (_request, response) => { + response.end("ok"); +}); +const agent = new Agent({ keepAlive: true, maxSockets: 1 }); + +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +if (!address || typeof address === "string") + throw new Error("missing HTTPS address"); + +const stores: number[] = []; +try { + for (let index = 0; index < 3; index++) { + await storage.run( + index, + () => + new Promise((resolve, reject) => { + const req = get( + { + agent, + host: "127.0.0.1", + port: address.port, + rejectUnauthorized: false, + }, + (response) => { + stores.push(storage.getStore() ?? -1); + response.resume(); + response.on("end", resolve); + }, + ); + req.on("error", reject); + }), + ); + } +} finally { + agent.destroy(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); +} +console.log("https agent stores:", stores.join(",")); +console.log("https agent outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/https-request.ts b/test-parity/node-suite/async_hooks/providers/https-request.ts new file mode 100644 index 0000000000..b74e997034 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/https-request.ts @@ -0,0 +1,50 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { createServer, request } from "node:https"; +import { CERT, KEY } from "./fixtures/tls-credentials.js"; + +const storage = new AsyncLocalStorage(); +const server = createServer({ cert: CERT, key: KEY }, (_request, response) => { + response.end("https-payload"); +}); + +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +if (!address || typeof address === "string") + throw new Error("missing HTTPS address"); + +let events: string[]; +try { + events = await storage.run( + "https-client", + () => + new Promise((resolve, reject) => { + const seen: string[] = []; + const req = request( + { + host: "127.0.0.1", + port: address.port, + rejectUnauthorized: false, + }, + (response) => { + seen.push(`response:${storage.getStore()}`); + response.on("data", (chunk) => { + seen.push(`data:${storage.getStore()}:${String(chunk)}`); + }); + response.on("end", () => { + seen.push(`end:${storage.getStore()}`); + resolve(seen); + }); + }, + ); + req.on("finish", () => seen.push(`finish:${storage.getStore()}`)); + req.on("error", reject); + req.end(); + }), + ); +} finally { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); +} +console.log("https request events:", events.join("|")); +console.log("https outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/net-connect.ts b/test-parity/node-suite/async_hooks/providers/net-connect.ts new file mode 100644 index 0000000000..6789b5ef3f --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/net-connect.ts @@ -0,0 +1,34 @@ +import { connect, createServer } from "node:net"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const server = createServer((socket) => socket.end()); + +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +if (!address || typeof address === "string") + throw new Error("missing server address"); + +let client: ReturnType | undefined; +try { + await storage.run( + "net-connect", + () => + new Promise((resolve, reject) => { + client = connect(address.port, "127.0.0.1", () => { + console.log("net connect callback store:", storage.getStore()); + }); + client.on("error", reject); + client.on("close", () => { + console.log("net close event store:", storage.getStore()); + resolve(); + }); + }), + ); +} finally { + client?.destroy(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); +} +console.log("net connect outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/net-data-isolation.ts b/test-parity/node-suite/async_hooks/providers/net-data-isolation.ts new file mode 100644 index 0000000000..c2104630c4 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/net-data-isolation.ts @@ -0,0 +1,91 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { connect, createServer, type Socket } from "node:net"; + +type ClientStore = { id: string }; +const storage = new AsyncLocalStorage(); +const dataStores = new Set(); +const endStores = new Set(); +let missingDataStore = false; +let missingEndStore = false; + +function recordDataStore() { + const store = storage.getStore(); + if (store) dataStores.add(store.id); + else missingDataStore = true; +} +function recordEndStore() { + const store = storage.getStore(); + if (store) endStores.add(store.id); + else missingEndStore = true; +} + +const serverSockets = new Set(); +const server = createServer((socket) => { + serverSockets.add(socket); + socket.on("close", () => serverSockets.delete(socket)); + socket.once("data", (chunk) => { + const id = String(chunk); + socket.write(id); + setImmediate(() => socket.end(id)); + }); +}); +const clients: Socket[] = []; +const ids = ["a", "b", "c", "d"]; +let bodies: string[] = []; +try { + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("missing net address"); + bodies = await Promise.all( + ids.map((id) => + storage.run( + { id }, + () => + new Promise((resolve, reject) => { + let body = ""; + const client = connect(address.port, "127.0.0.1", () => { + client.end(id); + }); + clients.push(client); + client.on("data", (chunk) => { + recordDataStore(); + body += String(chunk); + }); + client.on("end", () => { + recordEndStore(); + }); + client.on("close", () => resolve(body)); + client.on("error", reject); + }), + ), + ), + ); +} finally { + for (const client of clients) client.destroy(); + for (const socket of serverSockets) socket.destroy(); + if (server.listening) { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } +} + +console.log( + "net concurrent data stores:", + [...dataStores].sort().join(","), + missingDataStore, +); +console.log( + "net concurrent end stores:", + [...endStores].sort().join(","), + missingEndStore, +); +console.log( + "net concurrent bodies isolated:", + bodies.map((body, index) => body === ids[index].repeat(2)).join(","), +); +console.log("net concurrent outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/net-dual-context.ts b/test-parity/node-suite/async_hooks/providers/net-dual-context.ts new file mode 100644 index 0000000000..a08a16a10b --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/net-dual-context.ts @@ -0,0 +1,49 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { connect, createServer, type Socket } from "node:net"; + +const storage = new AsyncLocalStorage(); +let serverConnectionStore: string | undefined; +let clientConnectStore: string | undefined; +const serverSockets = new Set(); +const server = storage.run("server-context", () => + createServer((socket) => { + serverSockets.add(socket); + socket.once("close", () => serverSockets.delete(socket)); + serverConnectionStore = storage.getStore(); + socket.end("ok"); + }), +); +let client: ReturnType | undefined; + +try { + await new Promise((resolve, reject) => { + server.once("error", reject); + storage.run("server-context", () => { + server.listen(0, "127.0.0.1", () => { + const address = server.address(); + if (!address || typeof address === "string") { + reject(new Error("missing net address")); + return; + } + storage.run("client-context", () => { + client = connect(address.port, "127.0.0.1", () => { + clientConnectStore = storage.getStore(); + }); + client.once("error", reject); + client.once("data", () => resolve()); + }); + }); + }); + }); +} finally { + client?.destroy(); + for (const socket of serverSockets) socket.destroy(); + if (server.listening) { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } +} + +console.log("net dual stores:", serverConnectionStore, clientConnectStore); +console.log("net dual outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/net-get-connections-context.ts b/test-parity/node-suite/async_hooks/providers/net-get-connections-context.ts new file mode 100644 index 0000000000..df9815eaf7 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/net-get-connections-context.ts @@ -0,0 +1,34 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { createServer } from "node:net"; + +const storage = new AsyncLocalStorage(); +const server = createServer(); + +try { + await storage.run( + "connections", + () => + new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", () => { + server.getConnections((error, count) => { + console.log( + "getConnections callback:", + error === null, + count, + storage.getStore(), + ); + if (error) reject(error); + else resolve(); + }); + }); + }), + ); +} finally { + if (server.listening) { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } +} +console.log("getConnections outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/net-server-events.ts b/test-parity/node-suite/async_hooks/providers/net-server-events.ts new file mode 100644 index 0000000000..fcfd55ee76 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/net-server-events.ts @@ -0,0 +1,39 @@ +import { connect, createServer } from "node:net"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const server = createServer(); +let client: ReturnType | undefined; + +try { + await storage.run( + "net-server", + () => + new Promise((resolve, reject) => { + server.on("connection", (socket) => { + console.log("net server connection store:", storage.getStore()); + socket.end(); + }); + server.on("error", reject); + server.on("listening", () => { + console.log("net server listening store:", storage.getStore()); + const address = server.address(); + if (!address || typeof address === "string") + return reject(new Error("address")); + client = connect(address.port, "127.0.0.1"); + client.on("error", reject); + client.on("close", resolve); + }); + server.listen(0, "127.0.0.1"); + }), + ); +} finally { + client?.destroy(); + if (server.listening) { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } +} + +console.log("net server outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/net-write-callbacks.ts b/test-parity/node-suite/async_hooks/providers/net-write-callbacks.ts new file mode 100644 index 0000000000..4b1dbcfac8 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/net-write-callbacks.ts @@ -0,0 +1,37 @@ +import { connect, createServer } from "node:net"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const server = createServer((socket) => socket.on("data", () => socket.end())); + +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +if (!address || typeof address === "string") + throw new Error("missing server address"); + +let client: ReturnType | undefined; +try { + await storage.run( + "net-write", + () => + new Promise((resolve, reject) => { + client = connect(address.port, "127.0.0.1"); + client.on("connect", () => { + client!.write("payload", () => { + console.log("net write callback store:", storage.getStore()); + }); + client!.end(() => { + console.log("net end callback store:", storage.getStore()); + }); + }); + client.on("error", reject); + client.on("close", resolve); + }), + ); +} finally { + client?.destroy(); + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); +} +console.log("net write outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/readline-interface.ts b/test-parity/node-suite/async_hooks/providers/readline-interface.ts new file mode 100644 index 0000000000..5755a1619b --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/readline-interface.ts @@ -0,0 +1,26 @@ +import { createInterface } from "node:readline"; +import { Readable } from "node:stream"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const lines = await storage.run( + "readline", + () => + new Promise((resolve) => { + const output: string[] = []; + const input = Readable.from(["first\n", "second\n"]); + const rl = createInterface({ input }); + rl.on("line", (line) => { + console.log("readline line store:", storage.getStore(), line); + output.push(line); + }); + rl.on("close", () => { + console.log("readline close store:", storage.getStore()); + resolve(output.join(",")); + }); + }), +); + +console.log("readline result:", lines); +console.log("readline outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/stream-async-iterator.ts b/test-parity/node-suite/async_hooks/providers/stream-async-iterator.ts new file mode 100644 index 0000000000..5bd8a07b17 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/stream-async-iterator.ts @@ -0,0 +1,17 @@ +import { Readable } from "node:stream"; +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +const result = await storage.run("stream-iterator", async () => { + const output: string[] = []; + const stream = Readable.from(["first", "second"]); + for await (const chunk of stream) { + console.log("stream iterator store:", storage.getStore(), String(chunk)); + output.push(String(chunk)); + } + return output.join(","); +}); + +console.log("stream iterator result:", result); +console.log("stream iterator outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/tls-connect.ts b/test-parity/node-suite/async_hooks/providers/tls-connect.ts new file mode 100644 index 0000000000..8b89d9628d --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/tls-connect.ts @@ -0,0 +1,49 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { connect, createServer } from "node:tls"; +import { CERT, KEY } from "./fixtures/tls-credentials.js"; + +const storage = new AsyncLocalStorage(); +const server = createServer({ cert: CERT, key: KEY }, (socket) => { + socket.end("tls-payload"); +}); + +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +if (!address || typeof address === "string") + throw new Error("missing TLS address"); + +let events: string[]; +try { + events = await storage.run( + "tls-client", + () => + new Promise((resolve, reject) => { + const seen: string[] = []; + const socket = connect({ + host: "127.0.0.1", + port: address.port, + rejectUnauthorized: false, + }); + socket.on("secureConnect", () => { + seen.push(`secure:${storage.getStore()}`); + }); + socket.on("data", (chunk) => { + seen.push(`data:${storage.getStore()}:${String(chunk)}`); + }); + socket.on("end", () => { + seen.push(`end:${storage.getStore()}`); + }); + socket.on("close", () => { + seen.push(`close:${storage.getStore()}`); + resolve(seen); + }); + socket.on("error", reject); + }), + ); +} finally { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); +} +console.log("tls client events:", events.join("|")); +console.log("tls outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/providers/worker-message-events.ts b/test-parity/node-suite/async_hooks/providers/worker-message-events.ts new file mode 100644 index 0000000000..b4967fc3c4 --- /dev/null +++ b/test-parity/node-suite/async_hooks/providers/worker-message-events.ts @@ -0,0 +1,109 @@ +import { Worker } from "node:worker_threads"; +import { + AsyncLocalStorage, + createHook, + executionAsyncId, + executionAsyncResource, +} from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +type Entry = { + id: number; + type: string; + trigger: number; + resource: object; + before: number; + after: number; + destroy: number; +}; +const entries: Entry[] = []; +const byId = new Map(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type !== "WORKER" && type !== "MESSAGEPORT") return; + const entry = { + id: asyncId, + type, + trigger: triggerAsyncId, + resource, + before: 0, + after: 0, + destroy: 0, + }; + entries.push(entry); + byId.set(asyncId, entry); + }, + before(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.before++; + }, + after(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.after++; + }, + destroy(asyncId) { + const entry = byId.get(asyncId); + if (entry) entry.destroy++; + }, +}).enable(); +process.chdir("test-parity/node-suite/async_hooks/providers"); + +let workerParent = -1; +let messageResourceMapped = false; +const result = await storage.run( + "worker-events", + () => + new Promise((resolve, reject) => { + workerParent = executionAsyncId(); + const worker = new Worker("./fixtures/context-worker.cjs"); + let reply = ""; + worker.on("online", () => { + console.log("worker online store:", storage.getStore()); + }); + worker.on("message", (message) => { + messageResourceMapped = entries.some( + (entry) => + entry.type === "MESSAGEPORT" && + entry.id === executionAsyncId() && + entry.resource === executionAsyncResource(), + ); + console.log("worker message store:", storage.getStore(), message.phase); + if (message.phase === "ready") { + worker.postMessage({ value: 41 }); + } else { + reply = String(message.value); + worker.terminate(); + } + }); + worker.on("error", reject); + worker.on("exit", (code) => { + console.log("worker exit store:", storage.getStore(), code); + resolve(reply); + }); + }), +); +await new Promise((resolve) => setImmediate(resolve)); +await new Promise((resolve) => setImmediate(resolve)); +hook.disable(); + +console.log("worker result:", result); +console.log("worker outside:", String(storage.getStore())); +const workers = entries.filter((entry) => entry.type === "WORKER"); +const ports = entries.filter((entry) => entry.type === "MESSAGEPORT"); +console.log( + "worker provider resources:", + workers.length, + ports.length >= 2, + entries.length > 0 && + entries.every((entry) => entry.trigger === workerParent), +); +console.log("worker message resource mapped:", messageResourceMapped); +console.log( + "worker provider lifecycles:", + workers.length === 1 && + ports.length >= 2 && + entries.every( + (entry) => + entry.before > 0 && entry.before === entry.after && entry.destroy === 1, + ), +); diff --git a/test-parity/node-suite/async_hooks/resource/async-scope-await.ts b/test-parity/node-suite/async_hooks/resource/async-scope-await.ts new file mode 100644 index 0000000000..cc70c68542 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/async-scope-await.ts @@ -0,0 +1,20 @@ +import { AsyncResource, executionAsyncId } from "node:async_hooks"; + +const parentId = executionAsyncId(); +const resource = new AsyncResource("ParityAsyncScope"); + +const pending = resource.runInAsyncScope(async () => { + console.log("async scope start:", executionAsyncId() === resource.asyncId()); + await Promise.resolve(); + console.log( + "async scope continuation:", + executionAsyncId() === resource.asyncId(), + ); + return "async-result"; +}); + +console.log("async scope immediate restore:", executionAsyncId() === parentId); +console.log("async scope result:", await pending); +console.log("async scope final restore:", executionAsyncId() === parentId); + +resource.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/resource/async-scope-storage.ts b/test-parity/node-suite/async_hooks/resource/async-scope-storage.ts new file mode 100644 index 0000000000..7cc363ca75 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/async-scope-storage.ts @@ -0,0 +1,24 @@ +import { AsyncLocalStorage, AsyncResource } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +let resource!: AsyncResource; + +storage.run("resource-captured", () => { + resource = new AsyncResource("ParityStorageResource"); +}); + +storage.enterWith("caller"); +const result = resource.runInAsyncScope( + (value: string) => { + console.log("resource storage scope:", storage.getStore()); + return value.toUpperCase(); + }, + null, + "value", +); + +console.log("resource storage result:", result); +console.log("resource storage restored:", storage.getStore()); + +resource.emitDestroy(); +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/resource/bind-receiver-values.ts b/test-parity/node-suite/async_hooks/resource/bind-receiver-values.ts new file mode 100644 index 0000000000..9adfee4539 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/bind-receiver-values.ts @@ -0,0 +1,29 @@ +import { AsyncResource } from "node:async_hooks"; + +const resource = new AsyncResource("ParityBindReceivers"); +function receiver(this: unknown) { + if (this === undefined) return "undefined"; + if (this === false) return "false"; + if (this === "value") return "value"; + if (this && typeof this === "object" && (this as any).tag === "object") { + return "object"; + } + return "other"; +} +const objectReceiver = { tag: "object" }; +console.log("bound undefined receiver:", resource.bind(receiver)()); +console.log("bound false receiver:", resource.bind(receiver, false)()); +console.log("bound string receiver:", resource.bind(receiver, "value")()); +console.log( + "bound object receiver:", + resource.bind(receiver, objectReceiver)(), +); +console.log( + "bound explicit receiver ignores call:", + resource.bind(receiver, false).call(objectReceiver), +); +console.log( + "bound implicit receiver uses call:", + resource.bind(receiver).call(objectReceiver), +); +resource.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/resource/bind-reuse.ts b/test-parity/node-suite/async_hooks/resource/bind-reuse.ts new file mode 100644 index 0000000000..cd4a140533 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/bind-reuse.ts @@ -0,0 +1,26 @@ +import { + AsyncResource, + executionAsyncId, + triggerAsyncId, +} from "node:async_hooks"; + +const parentId = executionAsyncId(); +const resource = new AsyncResource("ParityBindReuse", { + triggerAsyncId: parentId, +}); + +let calls = 0; +const bound = resource.bind((value: string) => { + calls++; + console.log("reuse call:", calls, value); + console.log("reuse execution:", executionAsyncId() === resource.asyncId()); + console.log("reuse trigger:", triggerAsyncId() === parentId); + return `${value}-${calls}`; +}); + +console.log("first reuse result:", bound("first")); +console.log("first reuse restored:", executionAsyncId() === parentId); +console.log("second reuse result:", bound("second")); +console.log("second reuse restored:", executionAsyncId() === parentId); + +resource.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/resource/bound-async-await.ts b/test-parity/node-suite/async_hooks/resource/bound-async-await.ts new file mode 100644 index 0000000000..ca7a63baee --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/bound-async-await.ts @@ -0,0 +1,21 @@ +import { AsyncResource, executionAsyncId } from "node:async_hooks"; + +const parentId = executionAsyncId(); +const resource = new AsyncResource("ParityBoundAsync"); + +const bound = resource.bind(async (value: string) => { + console.log("bound async start:", executionAsyncId() === resource.asyncId()); + await Promise.resolve(); + console.log( + "bound async continuation:", + executionAsyncId() === resource.asyncId(), + ); + return value.toUpperCase(); +}); + +const pending = bound("value"); +console.log("bound async immediate restore:", executionAsyncId() === parentId); +console.log("bound async result:", await pending); +console.log("bound async final restore:", executionAsyncId() === parentId); + +resource.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/resource/construction.ts b/test-parity/node-suite/async_hooks/resource/construction.ts new file mode 100644 index 0000000000..45949386fd --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/construction.ts @@ -0,0 +1,44 @@ +import { AsyncResource, createHook, executionAsyncId } from "node:async_hooks"; + +const observed: Array<{ + asyncId: number; + type: string; + triggerAsyncId: number; +}> = []; + +const hook = createHook({ + init(asyncId, type, triggerAsyncId) { + if (type === "ParityConstruction") { + observed.push({ asyncId, type, triggerAsyncId }); + } + }, +}).enable(); + +const parentId = executionAsyncId(); +const resource = new AsyncResource("ParityConstruction"); +const explicit = new AsyncResource("ExplicitTrigger", { triggerAsyncId: 73 }); + +console.log("custom init count:", observed.length); +console.log("custom type:", observed[0]?.type); +console.log("async id positive:", resource.asyncId() > 0); +console.log( + "async id matches init:", + resource.asyncId() === observed[0]?.asyncId, +); +console.log( + "default trigger matches parent:", + resource.triggerAsyncId() === parentId, +); +console.log( + "init trigger matches resource:", + observed[0]?.triggerAsyncId === resource.triggerAsyncId(), +); +console.log( + "ids are distinct:", + resource.asyncId() !== resource.triggerAsyncId(), +); +console.log("explicit trigger:", explicit.triggerAsyncId()); + +hook.disable(); +resource.emitDestroy(); +explicit.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/resource/destroy-lifecycle.ts b/test-parity/node-suite/async_hooks/resource/destroy-lifecycle.ts new file mode 100644 index 0000000000..587084dea5 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/destroy-lifecycle.ts @@ -0,0 +1,23 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +let resourceId = -1; +let destroyCount = 0; + +const hook = createHook({ + init(asyncId, type) { + if (type === "ParityDestroy") resourceId = asyncId; + }, + destroy(asyncId) { + if (asyncId === resourceId) destroyCount++; + }, +}).enable(); + +const resource = new AsyncResource("ParityDestroy"); +console.log("init matched resource:", resourceId === resource.asyncId()); +console.log("destroy return identity:", resource.emitDestroy() === resource); +resource.emitDestroy(); + +setImmediate(() => { + console.log("destroy callback count:", destroyCount); + hook.disable(); +}); diff --git a/test-parity/node-suite/async_hooks/resource/execution-resource-nesting.ts b/test-parity/node-suite/async_hooks/resource/execution-resource-nesting.ts new file mode 100644 index 0000000000..f3ea0d245a --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/execution-resource-nesting.ts @@ -0,0 +1,20 @@ +import { AsyncResource, executionAsyncResource } from "node:async_hooks"; + +const topResource = executionAsyncResource(); +const outer = new AsyncResource("ParityExecutionOuter"); +const inner = new AsyncResource("ParityExecutionInner"); + +outer.runInAsyncScope(() => { + console.log("outer resource identity:", executionAsyncResource() === outer); + + inner.runInAsyncScope(() => { + console.log("inner resource identity:", executionAsyncResource() === inner); + }); + + console.log("outer resource restored:", executionAsyncResource() === outer); +}); + +console.log("top resource restored:", executionAsyncResource() === topResource); + +outer.emitDestroy(); +inner.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/resource/hook-scope-lifecycle.ts b/test-parity/node-suite/async_hooks/resource/hook-scope-lifecycle.ts new file mode 100644 index 0000000000..f405c355a0 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/hook-scope-lifecycle.ts @@ -0,0 +1,25 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +const resource = new AsyncResource("ParityHookScope"); +const targetId = resource.asyncId(); +const events: string[] = []; + +const hook = createHook({ + before(asyncId) { + if (asyncId === targetId) events.push("before"); + }, + after(asyncId) { + if (asyncId === targetId) events.push("after"); + }, +}).enable(); + +const returned = resource.runInAsyncScope(() => { + events.push("callback"); + return "result"; +}); + +hook.disable(); +resource.emitDestroy(); + +console.log("scope events:", events.join(">")); +console.log("scope return:", returned); diff --git a/test-parity/node-suite/async_hooks/resource/method-receiver-validation.ts b/test-parity/node-suite/async_hooks/resource/method-receiver-validation.ts new file mode 100644 index 0000000000..8291110ebc --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/method-receiver-validation.ts @@ -0,0 +1,22 @@ +import { AsyncResource } from "node:async_hooks"; + +function probe(label: string, fn: () => unknown) { + try { + const value = fn(); + console.log(label, "ok", typeof value); + } catch (error: any) { + console.log(label, error.name, error.code || "no-code"); + } +} +const resource = new AsyncResource("ReceiverValidation"); +const asyncId = resource.asyncId; +const triggerAsyncId = resource.triggerAsyncId; +const runInAsyncScope = resource.runInAsyncScope; +const emitDestroy = resource.emitDestroy; +const bind = resource.bind; +probe("detached asyncId", () => asyncId()); +probe("detached triggerAsyncId", () => triggerAsyncId()); +probe("detached runInAsyncScope", () => runInAsyncScope(() => "result")); +probe("detached emitDestroy", () => emitDestroy()); +probe("detached bind", () => bind(() => "result")); +resource.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/resource/nested-scopes.ts b/test-parity/node-suite/async_hooks/resource/nested-scopes.ts new file mode 100644 index 0000000000..ee423e5167 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/nested-scopes.ts @@ -0,0 +1,31 @@ +import { + AsyncResource, + executionAsyncId, + triggerAsyncId, +} from "node:async_hooks"; + +const parentId = executionAsyncId(); +const outer = new AsyncResource("ParityOuter", { triggerAsyncId: parentId }); +const inner = new AsyncResource("ParityInner", { + triggerAsyncId: outer.asyncId(), +}); + +outer.runInAsyncScope(() => { + console.log("outer execution:", executionAsyncId() === outer.asyncId()); + console.log("outer trigger:", triggerAsyncId() === parentId); + + inner.runInAsyncScope(() => { + console.log("inner execution:", executionAsyncId() === inner.asyncId()); + console.log("inner trigger:", triggerAsyncId() === outer.asyncId()); + }); + + console.log( + "outer restored after inner:", + executionAsyncId() === outer.asyncId(), + ); +}); + +console.log("parent restored after outer:", executionAsyncId() === parentId); + +outer.emitDestroy(); +inner.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/resource/options-accessors.ts b/test-parity/node-suite/async_hooks/resource/options-accessors.ts new file mode 100644 index 0000000000..e22cd2049e --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/options-accessors.ts @@ -0,0 +1,43 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +const accesses: string[] = []; +const options = Object.create(null, { + triggerAsyncId: { + get() { + accesses.push("triggerAsyncId"); + return 73; + }, + }, + requireManualDestroy: { + get() { + accesses.push("requireManualDestroy"); + return true; + }, + }, +}); +let initTrigger = -1; +let initResource: object | undefined; +const hook = createHook({ + init(_asyncId, type, triggerAsyncId, resource) { + if (type === "AccessorOptionsResource") { + initTrigger = triggerAsyncId; + initResource = resource; + } + }, +}).enable(); + +const resource = new AsyncResource("AccessorOptionsResource", options); +const triggerMethod = (resource as any).triggerAsyncId; +const trigger = + typeof triggerMethod === "function" + ? triggerMethod.call(resource) + : "missing"; +console.log("resource option accessor order:", accesses.join(",")); +console.log( + "resource option values:", + trigger, + initTrigger, + initResource === resource, +); +resource.emitDestroy(); +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/resource/post-destroy-scope.ts b/test-parity/node-suite/async_hooks/resource/post-destroy-scope.ts new file mode 100644 index 0000000000..aff822f90f --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/post-destroy-scope.ts @@ -0,0 +1,33 @@ +import { AsyncResource, createHook, executionAsyncId } from "node:async_hooks"; + +let resourceId = -1; +let beforeCount = 0; +let afterCount = 0; +let destroyCount = 0; +const hook = createHook({ + before(asyncId) { + if (asyncId === resourceId) beforeCount++; + }, + after(asyncId) { + if (asyncId === resourceId) afterCount++; + }, + destroy(asyncId) { + if (asyncId === resourceId) destroyCount++; + }, +}).enable(); + +const resource = new AsyncResource("PostDestroyScope"); +resourceId = resource.asyncId(); +resource.emitDestroy(); +const scopeId = resource.runInAsyncScope(() => executionAsyncId()); + +setImmediate(() => { + console.log( + "post-destroy scope lifecycle:", + scopeId === resourceId, + beforeCount, + afterCount, + destroyCount, + ); + hook.disable(); +}); diff --git a/test-parity/node-suite/async_hooks/resource/scope-and-bind.ts b/test-parity/node-suite/async_hooks/resource/scope-and-bind.ts new file mode 100644 index 0000000000..6a0ab717e2 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/scope-and-bind.ts @@ -0,0 +1,45 @@ +import { + AsyncResource, + executionAsyncId, + triggerAsyncId, +} from "node:async_hooks"; + +const outerId = executionAsyncId(); +const resource = new AsyncResource("ParityScope", { triggerAsyncId: outerId }); +const receiver = { label: "bound" }; + +const result = resource.runInAsyncScope( + function (this: typeof receiver, left: number, right: number) { + console.log( + "scope execution id:", + executionAsyncId() === resource.asyncId(), + ); + console.log("scope trigger id:", triggerAsyncId() === outerId); + console.log("scope receiver:", this === receiver); + console.log("scope args:", left, right); + return left + right; + }, + receiver, + 2, + 5, +); + +console.log("scope return:", result); +console.log("scope restores execution:", executionAsyncId() === outerId); + +const bound = resource.bind(function ( + this: typeof receiver, + prefix: string, + value: number, +) { + console.log("bind execution id:", executionAsyncId() === resource.asyncId()); + console.log("bind receiver:", this === receiver); + console.log("bind args:", prefix, value); + return `${prefix}:${value}`; +}, receiver); + +console.log("bound shape:", typeof bound, bound.length); +console.log("bound return:", bound("value", 9)); +console.log("bind restores execution:", executionAsyncId() === outerId); + +resource.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/resource/scope-receiver-values.ts b/test-parity/node-suite/async_hooks/resource/scope-receiver-values.ts new file mode 100644 index 0000000000..557155bd7a --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/scope-receiver-values.ts @@ -0,0 +1,25 @@ +import { AsyncResource } from "node:async_hooks"; + +const resource = new AsyncResource("ParityScopeReceivers"); +function receiver(this: unknown) { + if (this === undefined) return "undefined"; + if (this === null) return "null"; + if (this === false) return "false"; + if (this === "value") return "value"; + if (this && typeof this === "object" && (this as any).tag === "object") { + return "object"; + } + return "other"; +} +console.log("scope omitted receiver:", resource.runInAsyncScope(receiver)); +console.log("scope null receiver:", resource.runInAsyncScope(receiver, null)); +console.log("scope false receiver:", resource.runInAsyncScope(receiver, false)); +console.log( + "scope string receiver:", + resource.runInAsyncScope(receiver, "value"), +); +console.log( + "scope object receiver:", + resource.runInAsyncScope(receiver, { tag: "object" }), +); +resource.emitDestroy(); diff --git a/test-parity/node-suite/async_hooks/resource/static-bind-types.ts b/test-parity/node-suite/async_hooks/resource/static-bind-types.ts new file mode 100644 index 0000000000..6162cca269 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/static-bind-types.ts @@ -0,0 +1,51 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +const observed: string[] = []; +const tracked = new Set([ + "NamedTask", + "ExplicitType", + "ExplicitReceiver", + "bound-anonymous-fn", +]); +const hook = createHook({ + init(_asyncId, type) { + if (tracked.has(type)) observed.push(type); + }, +}).enable(); + +function NamedTask(value: string) { + return `named:${value}`; +} +const named = AsyncResource.bind(NamedTask); +const explicit = AsyncResource.bind( + (value: string) => `explicit:${value}`, + "ExplicitType", +); +const receiver = { label: "receiver" }; +const explicitReceiver = AsyncResource.bind( + function (this: typeof receiver, value: string) { + return `${this === receiver}:${value}`; + }, + "ExplicitReceiver", + receiver, +); +const anonymous = AsyncResource.bind( + ((value: string) => `anonymous:${value}`) as (value: string) => string, +); + +console.log("static bind inferred types:", observed.join(",")); +console.log( + "static bind results:", + named("a"), + explicit("b"), + explicitReceiver("c"), + anonymous("d"), +); +console.log( + "static bind lengths:", + named.length, + explicit.length, + explicitReceiver.length, + anonymous.length, +); +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/resource/subclass-lifecycle.ts b/test-parity/node-suite/async_hooks/resource/subclass-lifecycle.ts new file mode 100644 index 0000000000..3a2cbe2f87 --- /dev/null +++ b/test-parity/node-suite/async_hooks/resource/subclass-lifecycle.ts @@ -0,0 +1,55 @@ +import { + AsyncResource, + createHook, + executionAsyncId, + executionAsyncResource, +} from "node:async_hooks"; + +let initId = -1; +let initResource: object | undefined; +let initTrigger = -1; +const root = executionAsyncId(); +const hook = createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (type === "SubclassResource") { + initId = asyncId; + initResource = resource; + initTrigger = triggerAsyncId; + } + }, +}).enable(); + +class ScopedResource extends AsyncResource { + readonly label: string; + constructor(label: string) { + super("SubclassResource"); + this.label = label; + } + invoke(fn: (label: string) => T): T { + return this.runInAsyncScope(fn, this, this.label); + } +} + +const resource = new ScopedResource("value"); +const result = resource.invoke(function (this: ScopedResource, label) { + return [ + this === resource, + label === "value", + executionAsyncResource() === resource, + executionAsyncId() === resource.asyncId(), + ].join(","); +}); +console.log( + "subclass shape:", + resource instanceof ScopedResource, + resource instanceof AsyncResource, +); +console.log( + "subclass init:", + initId === resource.asyncId(), + initResource === resource, + initTrigger === root, +); +console.log("subclass scope:", result); +resource.emitDestroy(); +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/static/bind-concurrent.ts b/test-parity/node-suite/async_hooks/static/bind-concurrent.ts new file mode 100644 index 0000000000..7a0b9cd4f3 --- /dev/null +++ b/test-parity/node-suite/async_hooks/static/bind-concurrent.ts @@ -0,0 +1,17 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const bound = storage.run("captured", () => + AsyncLocalStorage.bind(async (label: string) => { + console.log(label, "bound start:", storage.getStore()); + await Promise.resolve(); + console.log(label, "bound continuation:", storage.getStore()); + return `${label}-result`; + }), +); + +const first = storage.run("first-caller", () => bound("first")); +const second = storage.run("second-caller", () => bound("second")); + +console.log("concurrent bound results:", (await first) + "," + (await second)); +console.log("concurrent bound outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/static/bind-context.ts b/test-parity/node-suite/async_hooks/static/bind-context.ts new file mode 100644 index 0000000000..76715f9e9e --- /dev/null +++ b/test-parity/node-suite/async_hooks/static/bind-context.ts @@ -0,0 +1,43 @@ +import { AsyncLocalStorage, AsyncResource } from "node:async_hooks"; + +const first = new AsyncLocalStorage(); +const second = new AsyncLocalStorage(); +const receiver = { label: "receiver" }; + +let boundFromStorage: (...args: number[]) => number; +let boundFromResource: (...args: string[]) => string; + +first.run("captured-first", () => { + second.run("captured-second", () => { + boundFromStorage = AsyncLocalStorage.bind(function ( + this: typeof receiver, + ...values: number[] + ) { + console.log("storage contexts:", first.getStore(), second.getStore()); + console.log("storage receiver:", this === receiver); + console.log("storage args:", JSON.stringify(values)); + return values.reduce((sum, value) => sum + value, 0); + }); + + boundFromResource = AsyncResource.bind(function ( + this: typeof receiver, + ...values: string[] + ) { + console.log("resource contexts:", first.getStore(), second.getStore()); + console.log("resource receiver:", this === receiver); + console.log("resource args:", JSON.stringify(values)); + return values.join(":"); + }, "ParityStaticBind"); + }); +}); + +first.enterWith("current-first"); +second.enterWith("current-second"); + +console.log("storage return:", boundFromStorage!.call(receiver, 2, 3, 4)); +console.log("storage restored:", first.getStore(), second.getStore()); +console.log("resource return:", boundFromResource!.call(receiver, "a", "b")); +console.log("resource restored:", first.getStore(), second.getStore()); + +first.disable(); +second.disable(); diff --git a/test-parity/node-suite/async_hooks/static/bind-empty-context.ts b/test-parity/node-suite/async_hooks/static/bind-empty-context.ts new file mode 100644 index 0000000000..f20e7ef765 --- /dev/null +++ b/test-parity/node-suite/async_hooks/static/bind-empty-context.ts @@ -0,0 +1,21 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const receiver = { marker: "receiver" }; + +const bound = AsyncLocalStorage.bind(function ( + this: typeof receiver, + left: number, + right: number, +) { + console.log("empty bind store:", String(storage.getStore())); + console.log("empty bind receiver:", this === receiver); + console.log("empty bind args:", left, right); + return left * right; +}); + +storage.enterWith("caller"); +console.log("empty bind result:", bound.call(receiver, 3, 4)); +console.log("empty bind restored:", storage.getStore()); + +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/static/snapshot-async.ts b/test-parity/node-suite/async_hooks/static/snapshot-async.ts new file mode 100644 index 0000000000..a0fee7efac --- /dev/null +++ b/test-parity/node-suite/async_hooks/static/snapshot-async.ts @@ -0,0 +1,18 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const snapshot = storage.run("captured", () => AsyncLocalStorage.snapshot()); + +storage.enterWith("caller"); +const pending = snapshot(async (value: string) => { + console.log("snapshot async start:", storage.getStore(), value); + await Promise.resolve(); + console.log("snapshot async continuation:", storage.getStore()); + return value.toUpperCase(); +}, "value"); + +console.log("snapshot async immediate restore:", storage.getStore()); +console.log("snapshot async result:", await pending); +console.log("snapshot async final restore:", storage.getStore()); + +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/static/snapshot-context.ts b/test-parity/node-suite/async_hooks/static/snapshot-context.ts new file mode 100644 index 0000000000..37241b849b --- /dev/null +++ b/test-parity/node-suite/async_hooks/static/snapshot-context.ts @@ -0,0 +1,33 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const first = new AsyncLocalStorage(); +const second = new AsyncLocalStorage(); +const receiver = { label: "runner" }; + +let snapshot: ReturnType; +first.run("snapshot-first", () => { + second.run("snapshot-second", () => { + snapshot = AsyncLocalStorage.snapshot(); + }); +}); + +first.enterWith("current-first"); +second.enterWith("current-second"); + +const result = snapshot!.call( + receiver, + function (this: unknown, left: string, right: string) { + console.log("snapshot contexts:", first.getStore(), second.getStore()); + console.log("snapshot callback receiver undefined:", this === undefined); + console.log("snapshot args:", left, right); + return `${left}-${right}`; + }, + "x", + "y", +); + +console.log("snapshot return:", result); +console.log("snapshot restored:", first.getStore(), second.getStore()); + +first.disable(); +second.disable(); diff --git a/test-parity/node-suite/async_hooks/static/snapshot-reentrant.ts b/test-parity/node-suite/async_hooks/static/snapshot-reentrant.ts new file mode 100644 index 0000000000..e2fa6919a3 --- /dev/null +++ b/test-parity/node-suite/async_hooks/static/snapshot-reentrant.ts @@ -0,0 +1,19 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const snapshot = storage.run("captured", () => AsyncLocalStorage.snapshot()); + +storage.enterWith("caller"); + +const result = snapshot(() => { + console.log("outer snapshot store:", storage.getStore()); + return snapshot((value: string) => { + console.log("inner snapshot store:", storage.getStore()); + return value.toUpperCase(); + }, "nested"); +}); + +console.log("reentrant snapshot result:", result); +console.log("reentrant snapshot restored:", storage.getStore()); + +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/static/snapshot-throw-cleanup.ts b/test-parity/node-suite/async_hooks/static/snapshot-throw-cleanup.ts new file mode 100644 index 0000000000..a23db7596c --- /dev/null +++ b/test-parity/node-suite/async_hooks/static/snapshot-throw-cleanup.ts @@ -0,0 +1,15 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const snapshot = storage.run("captured", () => AsyncLocalStorage.snapshot()); +storage.enterWith("caller"); +try { + snapshot(() => { + console.log("snapshot throw store:", storage.getStore()); + throw new Error("snapshot-error"); + }); +} catch (error) { + console.log("snapshot throw error:", (error as Error).message); +} +console.log("snapshot throw restored:", storage.getStore()); +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/disable-one-pending-other.ts b/test-parity/node-suite/async_hooks/storage/disable-one-pending-other.ts new file mode 100644 index 0000000000..47b84ef73e --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/disable-one-pending-other.ts @@ -0,0 +1,31 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const first = new AsyncLocalStorage(); +const second = new AsyncLocalStorage(); + +const pending = first.run("first", () => + second.run( + "second", + () => + new Promise((resolve) => { + setImmediate(() => { + console.log( + "pending isolated stores:", + String(first.getStore()), + String(second.getStore()), + ); + resolve(); + }); + }), + ), +); + +first.disable(); +await pending; +console.log( + "pending isolated outside:", + String(first.getStore()), + String(second.getStore()), +); + +second.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/disable-reentry.ts b/test-parity/node-suite/async_hooks/storage/disable-reentry.ts new file mode 100644 index 0000000000..e5780a989c --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/disable-reentry.ts @@ -0,0 +1,29 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +storage.run("first", () => { + console.log("before disable:", storage.getStore()); + storage.disable(); + console.log("after disable:", String(storage.getStore())); + + storage.exit(() => { + console.log("exit while disabled:", String(storage.getStore())); + }); + + storage.run("second", () => { + console.log("re-entry store:", storage.getStore()); + process.nextTick(() => { + console.log("re-entry nextTick:", storage.getStore()); + }); + }); + + console.log("after re-entry:", String(storage.getStore())); +}); + +console.log("outside store:", String(storage.getStore())); + +setImmediate(() => { + console.log("completion store:", String(storage.getStore())); + storage.disable(); +}); diff --git a/test-parity/node-suite/async_hooks/storage/enter-exit.ts b/test-parity/node-suite/async_hooks/storage/enter-exit.ts new file mode 100644 index 0000000000..38a533e884 --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/enter-exit.ts @@ -0,0 +1,35 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +storage.enterWith("entered"); +console.log("entered store:", storage.getStore()); + +const runResult = storage.run("run", () => { + console.log("run store:", storage.getStore()); + + const exitResult = storage.exit( + (left: string, right: string) => { + console.log("exit store:", String(storage.getStore())); + console.log("exit args:", left, right); + return `${left}-${right}`; + }, + "a", + "b", + ); + + console.log("exit return:", exitResult); + console.log("run restored after exit:", storage.getStore()); + return "run-result"; +}); + +console.log("run return:", runResult); +console.log("entered restored after run:", storage.getStore()); + +storage.exit(() => { + storage.enterWith("temporary"); + console.log("enter inside exit:", storage.getStore()); +}); +console.log("exit restores entered:", storage.getStore()); + +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/enter-with-event-listeners.ts b/test-parity/node-suite/async_hooks/storage/enter-with-event-listeners.ts new file mode 100644 index 0000000000..819221c877 --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/enter-with-event-listeners.ts @@ -0,0 +1,22 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { EventEmitter } from "node:events"; + +const storage = new AsyncLocalStorage(); +const emitter = new EventEmitter(); +const observed: string[] = []; + +storage.enterWith("outer"); +emitter.on("event", () => { + observed.push(storage.getStore() ?? "missing"); + storage.enterWith("first-listener"); + observed.push(storage.getStore() ?? "missing"); +}); +emitter.on("event", () => { + observed.push(storage.getStore() ?? "missing"); + storage.enterWith("second-listener"); +}); + +emitter.emit("event"); +console.log("enterWith listener stores:", observed.join(",")); +console.log("enterWith after emit:", storage.getStore()); +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/enter-with-promise-boundary.ts b/test-parity/node-suite/async_hooks/storage/enter-with-promise-boundary.ts new file mode 100644 index 0000000000..632e960999 --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/enter-with-promise-boundary.ts @@ -0,0 +1,33 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +async function enterAfterAwait() { + await Promise.resolve(); + storage.enterWith("after-await"); + console.log("inside after-await:", storage.getStore()); +} + +function enterInsideThen() { + return Promise.resolve().then(() => { + storage.enterWith("inside-then"); + console.log("inside then:", storage.getStore()); + }); +} + +async function enterBeforeAwait() { + storage.enterWith("before-await"); + await Promise.resolve(); + console.log("inside before-await continuation:", storage.getStore()); +} + +await enterAfterAwait(); +console.log("after enterAfterAwait:", String(storage.getStore())); + +await enterInsideThen(); +console.log("after enterInsideThen:", String(storage.getStore())); + +await enterBeforeAwait(); +console.log("after enterBeforeAwait:", storage.getStore()); + +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/enter-with-values.ts b/test-parity/node-suite/async_hooks/storage/enter-with-values.ts new file mode 100644 index 0000000000..4c53d5a3e2 --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/enter-with-values.ts @@ -0,0 +1,23 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const objectStore = { marker: "object" }; +for (const [label, value] of [ + ["undefined", undefined], + ["null", null], + ["false", false], + ["zero", 0], + ["empty", ""], + ["object", objectStore], +] as const) { + const returned = storage.enterWith(value); + console.log( + `${label} enterWith:`, + returned === undefined, + Object.is(storage.getStore(), value), + ); + await Promise.resolve(); + console.log(`${label} continuation:`, Object.is(storage.getStore(), value)); + storage.disable(); + console.log(`${label} disabled:`, storage.getStore() === undefined); +} diff --git a/test-parity/node-suite/async_hooks/storage/exit-async-rejection.ts b/test-parity/node-suite/async_hooks/storage/exit-async-rejection.ts new file mode 100644 index 0000000000..729528a70b --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/exit-async-rejection.ts @@ -0,0 +1,18 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +storage.enterWith("outer"); + +try { + await storage.exit(async () => { + console.log("exit rejection start:", String(storage.getStore())); + await Promise.resolve(); + console.log("exit rejection continuation:", String(storage.getStore())); + throw new Error("expected-rejection"); + }); +} catch (error) { + console.log("exit rejection error:", (error as Error).message); +} + +console.log("exit rejection restored:", storage.getStore()); +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/exit-async.ts b/test-parity/node-suite/async_hooks/storage/exit-async.ts new file mode 100644 index 0000000000..e370e40d7e --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/exit-async.ts @@ -0,0 +1,24 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const first = new AsyncLocalStorage(); +const second = new AsyncLocalStorage(); + +first.enterWith("first-outer"); +second.enterWith("second-outer"); + +const returned = await first.exit(async () => { + console.log("exit start:", String(first.getStore()), second.getStore()); + await Promise.resolve(); + console.log( + "exit continuation:", + String(first.getStore()), + second.getStore(), + ); + return "exit-result"; +}); + +console.log("exit async return:", returned); +console.log("exit async restored:", first.getStore(), second.getStore()); + +first.disable(); +second.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/exit-descendants.ts b/test-parity/node-suite/async_hooks/storage/exit-descendants.ts new file mode 100644 index 0000000000..c891da5cee --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/exit-descendants.ts @@ -0,0 +1,19 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +storage.enterWith("outer"); + +let descendant!: Promise; +storage.exit(() => { + console.log("exit descendant sync:", String(storage.getStore())); + descendant = Promise.resolve().then(() => { + console.log("exit descendant promise:", String(storage.getStore())); + return "descendant-result"; + }); +}); + +console.log("exit descendant restored:", storage.getStore()); +console.log("exit descendant result:", await descendant); +console.log("exit descendant final:", storage.getStore()); + +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/exit-instance-isolation.ts b/test-parity/node-suite/async_hooks/storage/exit-instance-isolation.ts new file mode 100644 index 0000000000..e07f94115e --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/exit-instance-isolation.ts @@ -0,0 +1,39 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const first = new AsyncLocalStorage(); +const second = new AsyncLocalStorage(); + +const result = await first.run("first", () => + second.run("second", async () => { + const exited = first.exit(async () => { + console.log( + "isolated exit sync stores:", + String(first.getStore()), + second.getStore(), + ); + await Promise.resolve(); + console.log( + "isolated exit continuation stores:", + String(first.getStore()), + second.getStore(), + ); + return "exit-result"; + }); + + console.log( + "isolated exit immediate restore:", + first.getStore(), + second.getStore(), + ); + return await exited; + }), +); + +console.log("isolated exit result:", result); +console.log( + "isolated exit outside:", + String(first.getStore()), + String(second.getStore()), +); +first.disable(); +second.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/immediate-self-clear.ts b/test-parity/node-suite/async_hooks/storage/immediate-self-clear.ts new file mode 100644 index 0000000000..ec5c99540f --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/immediate-self-clear.ts @@ -0,0 +1,27 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage<{ + label: string; + immediate?: ReturnType; +}>(); + +const results = await Promise.all( + ["first", "second", "third"].map( + (label) => + new Promise((resolve) => { + storage.run({ label }, () => { + const immediate = setImmediate(() => { + const store = storage.getStore(); + const matches = store?.immediate === immediate; + clearImmediate(immediate); + resolve(`${store?.label}:${matches}:${immediate.constructor.name}`); + }); + storage.getStore()!.immediate = immediate; + }); + }), + ), +); + +console.log("self-cleared immediate contexts:", results.join("|")); +console.log("self-cleared immediate outside:", String(storage.getStore())); +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/instance-isolation.ts b/test-parity/node-suite/async_hooks/storage/instance-isolation.ts new file mode 100644 index 0000000000..53c516d85b --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/instance-isolation.ts @@ -0,0 +1,42 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const first = new AsyncLocalStorage(); +const second = new AsyncLocalStorage(); +const third = new AsyncLocalStorage(); + +third.enterWith("third"); + +first.run("first", () => { + second.run("second", () => { + console.log( + "all stores:", + first.getStore(), + second.getStore(), + third.getStore(), + ); + first.disable(); + console.log( + "after first disable:", + String(first.getStore()), + second.getStore(), + third.getStore(), + ); + }); + + console.log( + "after second run:", + String(first.getStore()), + String(second.getStore()), + third.getStore(), + ); +}); + +console.log( + "outside:", + String(first.getStore()), + String(second.getStore()), + third.getStore(), +); + +second.disable(); +third.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/method-receiver-validation.ts b/test-parity/node-suite/async_hooks/storage/method-receiver-validation.ts new file mode 100644 index 0000000000..72f787110c --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/method-receiver-validation.ts @@ -0,0 +1,32 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +function probe(label: string, fn: () => unknown) { + try { + const value = fn(); + console.log(label, "ok", value === undefined ? "undefined" : String(value)); + } catch (error: any) { + console.log(label, error.name, error.code || "no-code"); + } +} +const storage = new AsyncLocalStorage(); +storage.enterWith("store"); +const getStore = storage.getStore; +const run = storage.run; +const enterWith = storage.enterWith; +const exit = storage.exit; +const disable = storage.disable; +probe("detached getStore", () => getStore()); +probe("detached run", () => run("next", () => "result")); +probe("detached enterWith", () => enterWith("next")); +probe("detached exit", () => exit(() => "result")); +probe("detached disable", () => disable()); +for (const [name, method, args] of [ + ["getStore", AsyncLocalStorage.prototype.getStore, []], + ["run", AsyncLocalStorage.prototype.run, ["x", () => "result"]], + ["enterWith", AsyncLocalStorage.prototype.enterWith, ["x"]], + ["exit", AsyncLocalStorage.prototype.exit, [() => "result"]], + ["disable", AsyncLocalStorage.prototype.disable, []], +] as const) { + probe(`foreign ${name}`, () => (method as any).call({}, ...args)); +} +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/repeated-disable.ts b/test-parity/node-suite/async_hooks/storage/repeated-disable.ts new file mode 100644 index 0000000000..7d8b673887 --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/repeated-disable.ts @@ -0,0 +1,28 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); + +storage.enterWith("first-enter"); +console.log("first enter:", storage.getStore()); +storage.disable(); +storage.disable(); +console.log("first disabled:", String(storage.getStore())); + +storage.enterWith("second-enter"); +console.log("second enter:", storage.getStore()); +storage.disable(); +console.log("second disabled:", String(storage.getStore())); + +storage.run("first-run", () => { + console.log("first run:", storage.getStore()); + storage.disable(); + console.log("disabled in run:", String(storage.getStore())); + + storage.run("second-run", () => { + console.log("second run:", storage.getStore()); + storage.disable(); + console.log("disabled in second run:", String(storage.getStore())); + }); +}); + +console.log("repeated disable outside:", String(storage.getStore())); diff --git a/test-parity/node-suite/async_hooks/storage/run-event-listeners.ts b/test-parity/node-suite/async_hooks/storage/run-event-listeners.ts new file mode 100644 index 0000000000..59e8a4ff76 --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/run-event-listeners.ts @@ -0,0 +1,23 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { EventEmitter } from "node:events"; + +const storage = new AsyncLocalStorage(); +const emitter = new EventEmitter(); +const observed: string[] = []; + +storage.enterWith("outer"); +emitter.on("event", () => { + observed.push(storage.getStore() ?? "missing"); + storage.run("first-listener", () => { + observed.push(storage.getStore() ?? "missing"); + }); + observed.push(storage.getStore() ?? "missing"); +}); +emitter.on("event", () => { + observed.push(storage.getStore() ?? "missing"); +}); + +emitter.emit("event"); +console.log("run listener stores:", observed.join(",")); +console.log("run after emit:", storage.getStore()); +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/storage/run-nesting.ts b/test-parity/node-suite/async_hooks/storage/run-nesting.ts new file mode 100644 index 0000000000..e2d376ee09 --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/run-nesting.ts @@ -0,0 +1,35 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const first = new AsyncLocalStorage(); +const second = new AsyncLocalStorage<{ value: number }>(); + +console.log( + "initial stores:", + String(first.getStore()), + String(second.getStore()), +); + +const returned = first.run("outer", () => { + console.log("outer store:", first.getStore()); + console.log("second isolated:", String(second.getStore())); + + return second.run({ value: 4 }, () => { + console.log("combined stores:", first.getStore(), second.getStore()?.value); + + const inner = first.run("inner", () => { + console.log("inner store:", first.getStore()); + return "inner-result"; + }); + + console.log("inner return:", inner); + console.log("outer restored:", first.getStore()); + return "outer-result"; + }); +}); + +console.log("run return:", returned); +console.log( + "stores cleared:", + String(first.getStore()), + String(second.getStore()), +); diff --git a/test-parity/node-suite/async_hooks/storage/store-values.ts b/test-parity/node-suite/async_hooks/storage/store-values.ts new file mode 100644 index 0000000000..6fc3e92a9d --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/store-values.ts @@ -0,0 +1,21 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +const objectStore = { marker: "object" }; + +for (const [label, value] of [ + ["undefined", undefined], + ["null", null], + ["false", false], + ["zero", 0], + ["empty", ""], + ["object", objectStore], +] as const) { + const returned = storage.run(value, () => { + console.log(label, "inside type:", typeof storage.getStore()); + console.log(label, "identity:", Object.is(storage.getStore(), value)); + return label; + }); + console.log(label, "return:", returned); + console.log(label, "restored:", String(storage.getStore())); +} diff --git a/test-parity/node-suite/async_hooks/storage/subclass-propagation.ts b/test-parity/node-suite/async_hooks/storage/subclass-propagation.ts new file mode 100644 index 0000000000..76afb78081 --- /dev/null +++ b/test-parity/node-suite/async_hooks/storage/subclass-propagation.ts @@ -0,0 +1,20 @@ +import { AsyncLocalStorage } from "node:async_hooks"; + +class NamedStorage extends AsyncLocalStorage { + capture(label: string, value: string) { + return this.run(value, async () => { + const sync = this.getStore(); + await Promise.resolve(); + return `${label}:${sync}:${this.getStore()}`; + }); + } +} + +const storage = new NamedStorage(); +console.log( + "als subclass shape:", + storage instanceof NamedStorage, + storage instanceof AsyncLocalStorage, +); +console.log("als subclass result:", await storage.capture("label", "store")); +console.log("als subclass restored:", storage.getStore() === undefined); diff --git a/test-parity/node-suite/async_hooks/validation/constructor-call.ts b/test-parity/node-suite/async_hooks/validation/constructor-call.ts new file mode 100644 index 0000000000..9a78b6ac2a --- /dev/null +++ b/test-parity/node-suite/async_hooks/validation/constructor-call.ts @@ -0,0 +1,23 @@ +import { AsyncLocalStorage, AsyncResource } from "node:async_hooks"; + +function probe(label: string, operation: () => unknown) { + try { + operation(); + console.log(label, "no-throw"); + } catch (error: any) { + console.log(label, error.name, error.code || "no-code"); + } +} + +probe("AsyncResource call", () => (AsyncResource as any)("CallValidation")); +probe("AsyncLocalStorage call", () => (AsyncLocalStorage as any)()); + +const resource = new AsyncResource("ConstructValidation"); +const storage = new AsyncLocalStorage(); +console.log( + "constructor instances:", + resource instanceof AsyncResource, + storage instanceof AsyncLocalStorage, +); +resource.emitDestroy(); +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/validation/empty-resource-type.ts b/test-parity/node-suite/async_hooks/validation/empty-resource-type.ts new file mode 100644 index 0000000000..59cb77d130 --- /dev/null +++ b/test-parity/node-suite/async_hooks/validation/empty-resource-type.ts @@ -0,0 +1,20 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +function construct(label: string) { + try { + const resource = new AsyncResource(""); + console.log(label, "empty type: ok"); + resource.emitDestroy(); + } catch (error) { + const typed = error as { code?: string; name: string }; + console.log(label, "empty type:", typed.name, typed.code || "no-code"); + } +} + +construct("hooks disabled"); + +const hook = createHook({ init() {} }).enable(); +construct("hooks enabled"); +hook.disable(); + +construct("hooks disabled again"); diff --git a/test-parity/node-suite/async_hooks/validation/options-accessor-errors.ts b/test-parity/node-suite/async_hooks/validation/options-accessor-errors.ts new file mode 100644 index 0000000000..5bf3fb361c --- /dev/null +++ b/test-parity/node-suite/async_hooks/validation/options-accessor-errors.ts @@ -0,0 +1,70 @@ +import { AsyncResource, createHook } from "node:async_hooks"; + +const hookError = new Error("hook accessor failure"); +const hookAccesses: string[] = []; +const hookOptions = Object.create(null, { + init: { + get() { + hookAccesses.push("init"); + throw hookError; + }, + }, + before: { + get() { + hookAccesses.push("before"); + return () => {}; + }, + }, +}); +let unexpectedHook: ReturnType | undefined; +try { + unexpectedHook = createHook(hookOptions); + console.log("hook accessor error: no-throw"); +} catch (error) { + console.log("hook accessor error:", error === hookError); +} finally { + if (unexpectedHook) unexpectedHook.disable(); +} +console.log("hook accessor stop order:", hookAccesses.join(",")); + +let initCount = 0; +const hook = createHook({ + init(_asyncId, type) { + if (type === "AccessorErrorResource") initCount++; + }, +}).enable(); +const resourceError = new Error("resource accessor failure"); +const resourceAccesses: string[] = []; +const resourceOptions = Object.create(null, { + triggerAsyncId: { + get() { + resourceAccesses.push("triggerAsyncId"); + throw resourceError; + }, + }, + requireManualDestroy: { + get() { + resourceAccesses.push("requireManualDestroy"); + return true; + }, + }, +}); +let unexpectedResource: AsyncResource | undefined; +try { + unexpectedResource = new AsyncResource( + "AccessorErrorResource", + resourceOptions, + ); + console.log("resource accessor error: no-throw"); +} catch (error) { + console.log("resource accessor error:", error === resourceError); +} finally { + if (unexpectedResource) unexpectedResource.emitDestroy(); +} +console.log("resource accessor stop order:", resourceAccesses.join(",")); +console.log("failed resource init count:", initCount); + +const valid = new AsyncResource("AccessorErrorResource"); +console.log("valid resource after failure:", initCount === 1); +valid.emitDestroy(); +hook.disable(); diff --git a/test-parity/node-suite/async_hooks/validation/throw-cleanup.ts b/test-parity/node-suite/async_hooks/validation/throw-cleanup.ts new file mode 100644 index 0000000000..7973703578 --- /dev/null +++ b/test-parity/node-suite/async_hooks/validation/throw-cleanup.ts @@ -0,0 +1,58 @@ +import { + AsyncLocalStorage, + AsyncResource, + executionAsyncId, +} from "node:async_hooks"; + +const storage = new AsyncLocalStorage(); +storage.enterWith("outer"); + +try { + storage.run("inner", () => { + console.log("run before throw:", storage.getStore()); + throw new Error("run-error"); + }); +} catch (error) { + console.log("run error:", (error as Error).message); +} +console.log("run cleanup:", storage.getStore()); + +try { + storage.exit(() => { + console.log("exit before throw:", String(storage.getStore())); + throw new Error("exit-error"); + }); +} catch (error) { + console.log("exit error:", (error as Error).message); +} +console.log("exit cleanup:", storage.getStore()); + +const parentId = executionAsyncId(); +const resource = new AsyncResource("ParityThrowCleanup"); +try { + resource.runInAsyncScope(() => { + console.log( + "resource before throw:", + executionAsyncId() === resource.asyncId(), + ); + throw new Error("resource-error"); + }); +} catch (error) { + console.log("resource error:", (error as Error).message); +} +console.log("resource cleanup:", executionAsyncId() === parentId); + +const bound = AsyncLocalStorage.bind(() => { + console.log("bound before throw:", storage.getStore()); + throw new Error("bound-error"); +}); +storage.enterWith("current"); +try { + bound(); +} catch (error) { + console.log("bound error:", (error as Error).message); +} +console.log("bound cleanup:", storage.getStore()); + +resource.emitDestroy(); +storage.disable(); diff --git a/test-parity/node-suite/async_hooks/validation/track-promises.ts b/test-parity/node-suite/async_hooks/validation/track-promises.ts new file mode 100644 index 0000000000..680df51e8f --- /dev/null +++ b/test-parity/node-suite/async_hooks/validation/track-promises.ts @@ -0,0 +1,32 @@ +import { createHook } from "node:async_hooks"; + +function describe(value: unknown) { + if (value === null) return "null"; + if (typeof value === "symbol") return "symbol"; + if (typeof value === "function") return "function"; + if (Number.isNaN(value)) return "NaN"; + return String(value); +} +function probe(label: string, callback: () => unknown) { + try { + callback(); + console.log(label, "no-throw"); + } catch (error) { + const caught = error as { name?: string; code?: string }; + console.log(label, caught.name, caught.code); + } +} + +for (const value of [0, null, 1, NaN, Symbol("x"), () => {}, "test"]) { + probe(`trackPromises ${describe(value)}`, () => + createHook({ init() {}, trackPromises: value as any }), + ); +} +probe("trackPromises false promiseResolve", () => + createHook({ trackPromises: false, promiseResolve() {} }), +); +console.log( + "trackPromises booleans:", + typeof createHook({ init() {}, trackPromises: true }), + typeof createHook({ init() {}, trackPromises: false }), +); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 56a4fa4e88..020ccd298f 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -10,8 +10,8 @@ "total": 70 }, "async_hooks": { - "pass": 5, - "total": 5 + "pass": 78, + "total": 193 }, "bigint": { "pass": 3,