Expand Node 24 async event consumption compatibility

This commit is contained in:
Chris Tate
2026-10-03 11:47:41 -05:00
parent 724fa7591b
commit 3c680c4fb2
15 changed files with 425 additions and 48 deletions
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "875a9e22efbde88c11b9",
"artifactVersion": "11c3ebd9479ef9f7eca0",
"rowCount": 3662
}
@@ -443,9 +443,9 @@
},
"dynamic": {
"supported": 0,
"partial": 25,
"partial": 26,
"refused": 0,
"not-implemented": 53,
"not-implemented": 52,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -34795,7 +34795,7 @@
},
"dynamic": {
"status": "partial",
"detail": "Implemented by the embedded Node compatibility shim.",
"detail": "Waits for EventEmitter or EventTarget events with AbortSignal cancellation, Node abort errors, and listener cleanup.",
"verification": "test-backed"
}
},
@@ -34851,7 +34851,7 @@
},
"dynamic": {
"status": "partial",
"detail": "Implemented by the embedded Node compatibility shim.",
"detail": "Iterates EventEmitter or EventTarget events with buffered delivery, close events, cancellation, watermark backpressure, and listener cleanup.",
"verification": "test-backed"
}
},
@@ -34906,9 +34906,9 @@
"verification": "registry-gap"
},
"dynamic": {
"status": "not-implemented",
"detail": "This API is not implemented by the dynamic-island shim.",
"verification": "registry-gap"
"status": "partial",
"detail": "Registers disposable abort listeners that run even when ordinary propagation is stopped; already-aborted signals schedule the callback in a microtask.",
"verification": "test-backed"
}
},
{
+2 -1
View File
@@ -19,6 +19,7 @@
"wasi": "by-design"
},
"features": [
{ "chapter": "events", "symbols": ["events.once", "events.on", "events.addAbortListener"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-async-events/main.ts"] },
{ "chapter": "events", "symbols": ["emitter.on", "emitter.addListener", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.removeListener", "emitter.off", "emitter.removeAllListeners", "emitter.emit", "emitter.listenerCount", "emitter.eventNames", "events.errorMonitor"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/stream-shims/main.ts", "tests/fixtures/npm/cases/node24-event-emitter/main.ts"] },
{ "chapter": "util", "symbols": ["util.promisify", "util.callbackify"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/util-shims/main.ts", "tests/fixtures/npm/cases/node24-util-adapters/main.ts"] },
{ "chapter": "util", "symbols": ["util.parseArgs", "parseArgs"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/util-shims/main.ts", "tests/fixtures/npm/cases/node24-parseargs/main.ts"] },
@@ -45,7 +46,7 @@
{ "chapter": "typescript", "symbols": ["type"], "status": "supported", "evidence": ["tests/corpus/2705-type-import-link-dynamic/main.ts", "tests/corpus/2706-type-import-link-dynamic-success/main.ts", "tests/corpus/2708-type-import-link-dynamic/main.ts", "tests/harness/errors.test.ts"] }
],
"modules": {
"events": { "exports": ["EventEmitter", "once", "on", "listenerCount", "getEventListeners", "setMaxListeners", "defaultMaxListeners", "errorMonitor", "captureRejectionSymbol"], "members": ["emitter.addListener", "emitter.emit", "emitter.eventNames", "emitter.getMaxListeners", "emitter.listenerCount", "emitter.listeners", "emitter.off", "emitter.on", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.removeAllListeners", "emitter.removeListener", "emitter.setMaxListeners", "emitter.rawListeners", "events.getEventListeners", "events.listenerCount", "events.once", "events.on", "events.setMaxListeners"], "evidence": ["tests/fixtures/npm/cases/stream-shims/main.ts"] },
"events": { "exports": ["EventEmitter", "once", "on", "addAbortListener", "listenerCount", "getEventListeners", "setMaxListeners", "defaultMaxListeners", "errorMonitor", "captureRejectionSymbol"], "members": ["emitter.addListener", "emitter.emit", "emitter.eventNames", "emitter.getMaxListeners", "emitter.listenerCount", "emitter.listeners", "emitter.off", "emitter.on", "emitter.once", "emitter.prependListener", "emitter.prependOnceListener", "emitter.removeAllListeners", "emitter.removeListener", "emitter.setMaxListeners", "emitter.rawListeners", "events.getEventListeners", "events.listenerCount", "events.once", "events.on", "events.setMaxListeners"], "evidence": ["tests/fixtures/npm/cases/stream-shims/main.ts"] },
"path": { "exports": ["sep", "delimiter", "basename", "dirname", "extname", "join", "resolve", "normalize", "relative", "isAbsolute", "toNamespacedPath", "parse", "format", "posix", "win32"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"path/posix": { "exports": ["sep", "delimiter", "basename", "dirname", "extname", "join", "resolve", "normalize", "relative", "isAbsolute", "toNamespacedPath", "parse", "format", "posix", "win32"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
"path/win32": { "exports": ["sep", "delimiter", "basename", "dirname", "extname", "join", "resolve", "normalize", "relative", "isAbsolute", "toNamespacedPath", "parse", "format", "posix", "win32"], "evidence": ["tests/fixtures/npm/cases/misc-shims/main.ts"] },
@@ -14,8 +14,8 @@
"implement": 81
},
"dynamic": {
"audit-partial": 815,
"verify-gap": 2223,
"audit-partial": 816,
"verify-gap": 2222,
"replace-refusal": 112,
"implement": 218
}
@@ -33726,9 +33726,9 @@
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "island-member:node:events.events.once",
"source": "island-feature:events.events.once",
"tests": [
"tests/fixtures/npm/cases/stream-shims/main.ts"
"tests/fixtures/npm/cases/node24-async-events/main.ts"
]
}
}
@@ -33802,9 +33802,9 @@
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "island-member:node:events.events.on",
"source": "island-feature:events.events.on",
"tests": [
"tests/fixtures/npm/cases/stream-shims/main.ts"
"tests/fixtures/npm/cases/node24-async-events/main.ts"
]
}
}
@@ -33871,12 +33871,14 @@
"tests": []
},
"dynamic": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "island-unmatched:events",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "island-feature:events.events.addAbortListener",
"tests": [
"tests/fixtures/npm/cases/node24-async-events/main.ts"
]
}
}
},
@@ -492,9 +492,9 @@
},
"dynamic": {
"supported": 0,
"partial": 25,
"partial": 26,
"refused": 0,
"not-implemented": 53,
"not-implemented": 52,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
@@ -48354,6 +48354,7 @@
"status": "partial",
"evidence": "derived:descendants",
"tests": [
"tests/fixtures/npm/cases/node24-async-events/main.ts",
"tests/fixtures/npm/cases/node24-event-emitter/main.ts",
"tests/fixtures/npm/cases/stream-shims/main.ts"
]
@@ -50488,9 +50489,9 @@
},
"dynamic": {
"status": "partial",
"evidence": "island-member:node:events.events.once",
"evidence": "island-feature:events.events.once",
"tests": [
"tests/fixtures/npm/cases/stream-shims/main.ts"
"tests/fixtures/npm/cases/node24-async-events/main.ts"
]
},
"anchorSource": "exact"
@@ -50583,9 +50584,9 @@
},
"dynamic": {
"status": "partial",
"evidence": "island-member:node:events.events.on",
"evidence": "island-feature:events.events.on",
"tests": [
"tests/fixtures/npm/cases/stream-shims/main.ts"
"tests/fixtures/npm/cases/node24-async-events/main.ts"
]
},
"anchorSource": "exact"
@@ -50646,8 +50647,11 @@
"evidence": "compiler-unmatched:events"
},
"dynamic": {
"status": "not-implemented",
"evidence": "island-unmatched:events"
"status": "partial",
"evidence": "island-feature:events.events.addAbortListener",
"tests": [
"tests/fixtures/npm/cases/node24-async-events/main.ts"
]
},
"anchorSource": "exact"
},
+3
View File
@@ -678,6 +678,9 @@ function publicDetail(tier) {
return "No dynamic-island implementation exists for this Node API family yet.";
}
if (source.startsWith("island-feature:")) {
if (source === "island-feature:events.events.once") return "Waits for EventEmitter or EventTarget events with AbortSignal cancellation, Node abort errors, and listener cleanup.";
if (source === "island-feature:events.events.on") return "Iterates EventEmitter or EventTarget events with buffered delivery, close events, cancellation, watermark backpressure, and listener cleanup.";
if (source === "island-feature:events.events.addAbortListener") return "Registers disposable abort listeners that run even when ordinary propagation is stopped; already-aborted signals schedule the callback in a microtask.";
if (source.startsWith("island-feature:events.")) {
return "Implements EventEmitter listener lifecycle, symbol event names, filtered listener counts, and error monitoring; rejection capture and listener warnings remain outside this subset.";
}
@@ -13420,6 +13420,12 @@
"<repo>/tests/fixtures/npm/cases/node24-event-emitter/main.ts"
],
"diags": []
},
"<repo>/tests/fixtures/npm/cases/node24-async-events/main.ts": {
"order": [
"<repo>/tests/fixtures/npm/cases/node24-async-events/main.ts"
],
"diags": []
}
}
}
+148 -11
View File
@@ -3860,7 +3860,7 @@ static const char isl_modules_bootstrap[] =
/* Node's events module: the emitter surface streams and CLIs drive —
* prepend/once/remove with listener-unwrap, maxListeners bookkeeping
* (warnings are not emitted), eventNames/rawListeners, Node's
* unhandled-'error' throw, and the once/getEventListeners statics.
* unhandled-'error' throw, cancellable once, and async event iteration.
* EventEmitter is callable so legacy constructors can use .call(this). */
" builtins.events = memo(() => {\n"
" function EventEmitter() {\n"
@@ -3968,18 +3968,155 @@ static const char isl_modules_bootstrap[] =
" EventEmitter.listenerCount = (emitter, n) => emitter.listenerCount(n);\n"
" EventEmitter.getEventListeners = (emitter, n) => emitter.listeners(n);\n"
" EventEmitter.setMaxListeners = (m, ...emitters) => { for (const e of emitters) e.setMaxListeners(m); };\n"
" EventEmitter.once = (emitter, name) => new Promise((resolve, reject) => {\n"
" const onEvent = (...args) => {\n"
" emitter.removeListener('error', onError);\n"
" resolve(args);\n"
" const resistStopPropagation = Symbol.for('scriptc.events.resistStopPropagation');\n"
" const checkSignal = (signal, name) => {\n"
" if (signal !== undefined && (signal === null || typeof signal !== 'object' || !('aborted' in signal)))\n"
" envArgumentError(name, 'an instance of AbortSignal', signal);\n"
" };\n"
" const checkOptions = (options) => {\n"
" if (options === null || typeof options !== 'object' || Array.isArray(options))\n"
" envArgumentError('options', 'of type object', options);\n"
" };\n"
" const abortError = (signal) => {\n"
" const error = new Error('The operation was aborted', { cause: signal.reason });\n"
" error.name = 'AbortError';\n"
" error.code = 'ABORT_ERR';\n"
" return error;\n"
" };\n"
" const add = (emitter, name, listener, flags) => {\n"
" if (typeof emitter.on === 'function') {\n"
" if (flags && flags.once) emitter.once(name, listener);\n"
" else emitter.on(name, listener);\n"
" } else if (typeof emitter.addEventListener === 'function') emitter.addEventListener(name, listener, flags);\n"
" else envArgumentError('emitter', 'an instance of EventEmitter', emitter);\n"
" };\n"
" const remove = (emitter, name, listener) => {\n"
" if (typeof emitter.removeListener === 'function') emitter.removeListener(name, listener);\n"
" else if (typeof emitter.removeEventListener === 'function') emitter.removeEventListener(name, listener);\n"
" else envArgumentError('emitter', 'an instance of EventEmitter', emitter);\n"
" };\n"
" EventEmitter.addAbortListener = (signal, listener) => {\n"
" if (signal === undefined) envArgumentError('signal', 'an instance of AbortSignal', signal);\n"
" checkSignal(signal, 'signal');\n"
" checkListener(listener);\n"
" let dispose;\n"
" if (signal.aborted) queueMicrotask(() => listener());\n"
" else {\n"
" signal.addEventListener('abort', listener, { once: true, [resistStopPropagation]: true });\n"
" dispose = () => signal.removeEventListener('abort', listener);\n"
" }\n"
" return { __proto__: null, [Symbol.dispose]() { if (dispose) dispose(); } };\n"
" };\n"
" EventEmitter.once = async (emitter, name, options = {}) => {\n"
" checkOptions(options);\n"
" const signal = options.signal;\n"
" checkSignal(signal, 'options.signal');\n"
" if (signal && signal.aborted) throw abortError(signal);\n"
" return new Promise((resolve, reject) => {\n"
" const onError = (err) => {\n"
" remove(emitter, name, onEvent);\n"
" if (signal) remove(signal, 'abort', onAbort);\n"
" reject(err);\n"
" };\n"
" const onEvent = (...args) => {\n"
" if (typeof emitter.removeListener === 'function') emitter.removeListener('error', onError);\n"
" if (signal) remove(signal, 'abort', onAbort);\n"
" resolve(args);\n"
" };\n"
" const onAbort = () => {\n"
" remove(emitter, name, onEvent);\n"
" remove(emitter, 'error', onError);\n"
" reject(abortError(signal));\n"
" };\n"
" const flags = { __proto__: null, once: true, [resistStopPropagation]: true };\n"
" add(emitter, name, onEvent, flags);\n"
" if (name !== 'error' && typeof emitter.once === 'function') emitter.once('error', onError);\n"
" if (signal) add(signal, 'abort', onAbort, flags);\n"
" });\n"
" };\n"
" const queue = () => {\n"
" let values = [], head = 0;\n"
" return {\n"
" get size() { return values.length - head; },\n"
" push(value) { values.push(value); },\n"
" shift() {\n"
" const value = values[head];\n"
" values[head++] = undefined;\n"
" if (head === values.length) { values = []; head = 0; }\n"
" else if (head >= 1024 && head * 2 > values.length) { values = values.slice(head); head = 0; }\n"
" return value;\n"
" },\n"
" };\n"
" };\n"
" const checkWatermark = (value, name) => {\n"
" if (typeof value !== 'number') envArgumentError(name, 'of type number', value);\n"
" if (!Number.isInteger(value) || value < 1 || value > Number.MAX_SAFE_INTEGER) {\n"
" const range = Number.isInteger(value) ? '>= 1 && <= 9007199254740991' : 'an integer';\n"
" const received = Number.isInteger(value) && Math.abs(value) > 2 ** 32 ? String(value).replace(/\\B(?=(\\d{3})+(?!\\d))/g, '_') : String(value);\n"
" const error = new RangeError('The value of \"' + name + '\" is out of range. It must be ' + range + '. Received ' + received);\n"
" error.code = 'ERR_OUT_OF_RANGE';\n"
" throw error;\n"
" }\n"
" };\n"
" const iteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(Object.getPrototypeOf((async function* () {})())));\n"
" EventEmitter.on = (emitter, name, options = {}) => {\n"
" checkOptions(options);\n"
" const signal = options.signal;\n"
" checkSignal(signal, 'options.signal');\n"
" if (signal && signal.aborted) throw abortError(signal);\n"
" const high = options.highWaterMark ?? options.highWatermark ?? Number.MAX_SAFE_INTEGER;\n"
" checkWatermark(high, 'options.highWaterMark');\n"
" const low = options.lowWaterMark ?? options.lowWatermark ?? 1;\n"
" checkWatermark(low, 'options.lowWaterMark');\n"
" const events = queue(), pending = queue(), listeners = [];\n"
" let paused = false, error = null, finished = false, abortListener;\n"
" const close = () => {\n"
" if (abortListener) abortListener[Symbol.dispose]();\n"
" while (listeners.length) { const [event, handler] = listeners.pop(); remove(emitter, event, handler); }\n"
" finished = true;\n"
" paused = false;\n"
" const result = { value: undefined, done: true };\n"
" while (pending.size) pending.shift().resolve(result);\n"
" return Promise.resolve(result);\n"
" };\n"
" const onError = (err) => {\n"
" emitter.removeListener(name, onEvent);\n"
" reject(err);\n"
" if (pending.size) pending.shift().reject(err);\n"
" else error = err;\n"
" close();\n"
" };\n"
" emitter.once(name, onEvent);\n"
" if (name !== 'error') emitter.once('error', onError);\n"
" });\n"
" const onEvent = (...args) => {\n"
" if (pending.size) pending.shift().resolve({ value: args, done: false });\n"
" else {\n"
" if (!paused && events.size + 1 > high) { paused = true; emitter.pause(); }\n"
" events.push(args);\n"
" }\n"
" };\n"
" const iterator = Object.setPrototypeOf({\n"
" next() {\n"
" if (events.size) {\n"
" const value = events.shift();\n"
" if (paused && events.size < low) { emitter.resume(); paused = false; }\n"
" return Promise.resolve({ value, done: false });\n"
" }\n"
" if (error) { const err = error; error = null; return Promise.reject(err); }\n"
" if (finished) return close();\n"
" return new Promise((resolve, reject) => pending.push({ resolve, reject }));\n"
" },\n"
" return() { return close(); },\n"
" throw(err) {\n"
" if (!(err instanceof Error)) envArgumentError('EventEmitter.AsyncIterator', 'an instance of Error', err);\n"
" onError(err);\n"
" },\n"
" [Symbol.asyncIterator]() { return this; },\n"
" }, iteratorPrototype);\n"
" const listen = (event, handler) => { add(emitter, event, handler); listeners.push([event, handler]); };\n"
" listen(name, onEvent);\n"
" if (name !== 'error' && typeof emitter.on === 'function') listen('error', onError);\n"
" const closeEvents = options.close;\n"
" if (closeEvents && closeEvents.length) { for (let i = 0; i < closeEvents.length; i++) listen(closeEvents[i], close); }\n"
" if (signal) abortListener = EventEmitter.addAbortListener(signal, () => onError(abortError(signal)));\n"
" return iterator;\n"
" };\n"
" EventEmitter.EventEmitter = EventEmitter;\n"
" EventEmitter.default = EventEmitter;\n"
" return EventEmitter;\n"
@@ -7469,7 +7606,7 @@ static const char isl_modules_bootstrap[] =
" else if (type === 'function') received = 'function ' + value.name;\n"
" else if (type === 'object' && value.constructor && value.constructor.name) received = 'an instance of ' + value.constructor.name;\n"
" else received = type === 'object' ? builtins.util().inspect(value, { depth: -1 }) : 'type ' + type + ' (' + builtins.util().inspect(value) + ')';\n"
" const error = new TypeError('The \"' + name + '\" argument must be ' + expected + '. Received ' + received);\n"
" const error = new TypeError('The \"' + name + '\" ' + (name.includes('.') ? 'property' : 'argument') + ' must be ' + expected + '. Received ' + received);\n"
" error.code = 'ERR_INVALID_ARG_TYPE';\n"
" throw error;\n"
" };\n"
+2 -2
View File
@@ -5,8 +5,8 @@ static const struct {
const char *exports;
} isl_builtins[] = {
{"node:events",
"EventEmitter,once,on,listenerCount,getEventListeners,setMaxListeners,defaultMaxListeners"
",errorMonitor,captureRejectionSymbol"},
"EventEmitter,once,on,addAbortListener,listenerCount,getEventListeners,setMaxListeners,de"
"faultMaxListeners,errorMonitor,captureRejectionSymbol"},
{"node:path",
"sep,delimiter,basename,dirname,extname,join,resolve,normalize,relative,isAbsolute,toName"
"spacedPath,parse,format,posix,win32"},
+13 -6
View File
@@ -924,13 +924,14 @@ static const char web_prelude[] =
" const once = !!(options !== null && typeof options === 'object' && options.once);\n"
" const list = this._et[t] || (this._et[t] = []);\n"
" for (const e of list) { if (e.listener === listener) return; }\n"
" list.push({ listener, once });\n"
" const resistant = !!(options && options[Symbol.for('scriptc.events.resistStopPropagation')]);\n"
" list.push({ listener, once, resistant });\n"
" }\n"
" removeEventListener(type, listener) {\n"
" const list = this._et[String(type)];\n"
" if (!list) return;\n"
" const i = list.findIndex((e) => e.listener === listener);\n"
" if (i >= 0) list.splice(i, 1);\n"
" if (i >= 0) list.splice(i, 1)[0].removed = true;\n"
" }\n"
" dispatchEvent(event) {\n"
" if (!(event instanceof Event)) throw new TypeError('The \"event\" argument must be an instance of Event.');\n"
@@ -940,7 +941,8 @@ static const char web_prelude[] =
" const list = this._et[event.type];\n"
" if (list) {\n"
" for (const e of [...list]) {\n"
" if (event._stopImmediate) break;\n"
" if (e.removed) continue;\n"
" if (event._stopImmediate && !e.resistant) continue;\n"
" if (e.once) this.removeEventListener(event.type, e.listener);\n"
" if (typeof e.listener === 'function') e.listener.call(this, event);\n"
" else if (e.listener !== null && typeof e.listener.handleEvent === 'function') e.listener.handleEvent(event);\n"
@@ -1087,13 +1089,16 @@ static const char web_prelude[] =
" if (s._aborted) return;\n"
" s._aborted = true;\n"
" s._reason = reason === undefined ? mkAbortError() : reason;\n"
" const ev = { type: 'abort', target: s, currentTarget: s };\n"
" const ev = new Event('abort');\n"
" ev.target = s; ev.currentTarget = s; ev.eventPhase = Event.AT_TARGET; ev.isTrusted = true;\n"
" let firstErr;\n"
" let threw = false;\n"
" if (s._onabort !== null) {\n"
" try { s._onabort.call(s, ev); } catch (e) { firstErr = e; threw = true; }\n"
" }\n"
" for (const l of s._listeners.slice()) {\n"
" if (l.removed) continue;\n"
" if (ev._stopImmediate && !l.resistant) continue;\n"
" if (l.once) s.removeEventListener('abort', l.fn);\n"
" try { l.fn.call(s, ev); } catch (e) { if (!threw) { firstErr = e; threw = true; } }\n"
" }\n"
@@ -1109,13 +1114,15 @@ static const char web_prelude[] =
" throwIfAborted() { if (this._aborted) throw this._reason; }\n"
" addEventListener(type, fn, options) {\n"
" if (String(type) !== 'abort' || typeof fn !== 'function') return;\n"
" if (this._listeners.some(l => l.fn === fn)) return;\n"
" const once = options !== undefined && options !== null && !!options.once;\n"
" this._listeners.push({ fn, once });\n"
" const resistant = !!(options && options[Symbol.for('scriptc.events.resistStopPropagation')]);\n"
" this._listeners.push({ fn, once, resistant });\n"
" }\n"
" removeEventListener(type, fn) {\n"
" if (String(type) !== 'abort') return;\n"
" const i = this._listeners.findIndex((l) => l.fn === fn);\n"
" if (i >= 0) this._listeners.splice(i, 1);\n"
" if (i >= 0) this._listeners.splice(i, 1)[0].removed = true;\n"
" }\n"
" static abort(reason) {\n"
" const s = mkSignal();\n"
+4
View File
@@ -0,0 +1,4 @@
// @dynamic
// @stderr
import { report } from "node24-async-events-fixture";
await report();
@@ -0,0 +1 @@
{"type":"module"}
@@ -0,0 +1 @@
export function report(): Promise<void>;
+210
View File
@@ -0,0 +1,210 @@
import { EventEmitter, once, on, addAbortListener } from 'node:events';
export async function report() {
const line = (name, value) => console.log(name, JSON.stringify(value));
const shape = e => [e.name, e.code, e.message];
const reject = async (name, fn) => {
try { await fn(); line(name, 'no error'); } catch (e) { line(name, shape(e)); }
};
const counts = em => em.eventNames().map(n => [String(n), em.listenerCount(n)]);
for (const value of [null, false, 3, 'bad', [], () => {}]) {
await reject('once options', () => once(new EventEmitter(), 'x', value));
await reject('on options', () => on(new EventEmitter(), 'x', value));
}
for (const value of [null, false, {}, 3]) {
await reject('once signal', () => once(new EventEmitter(), 'x', { signal: value }));
await reject('on signal', () => on(new EventEmitter(), 'x', { signal: value }));
await reject('abort signal', () => addAbortListener(value, () => {}));
}
await reject('abort undefined', () => addAbortListener(undefined, () => {}));
await reject('abort listener', () => addAbortListener(new AbortController().signal, null));
await reject('once emitter', () => once({}, 'x'));
await reject('on emitter', () => on({}, 'x'));
for (const name of ['highWaterMark', 'lowWaterMark', 'highWatermark', 'lowWatermark']) {
for (const value of ['2', 0, -1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1]) {
await reject(name, () => on(new EventEmitter(), 'x', { [name]: value }));
}
}
{
const em = new EventEmitter(), ac = new AbortController(), event = Symbol('ready');
const promise = once(em, event, { signal: ac.signal });
line('once registered', counts(em));
em.emit(event, 'ready', 42);
line('once result', await promise);
line('once cleaned', counts(em));
ac.abort('after completion');
}
{
const em = new EventEmitter(), ac = new AbortController(), failure = new Error('failure');
const promise = once(em, 'ready', { signal: ac.signal });
em.emit('error', failure);
try { await promise; } catch (e) { line('once error identity', e === failure); }
line('once error cleaned', counts(em));
const errorEvent = once(em, 'error'); em.emit('error', failure);
line('once error event', (await errorEvent)[0] === failure);
}
for (const preaborted of [false, true]) {
const em = new EventEmitter(), ac = new AbortController(), cause = { why: 'cancelled' };
ac.signal.addEventListener('abort', e => e.stopImmediatePropagation());
if (preaborted) ac.abort(cause);
const promise = once(em, 'ready', { signal: ac.signal });
if (!preaborted) ac.abort(cause);
try { await promise; } catch (e) { line('once aborted', [preaborted, ...shape(e), e.cause === cause]); }
line('once abort cleaned', counts(em));
}
{
const target = new EventTarget(), ac = new AbortController();
target.addEventListener('ready', e => e.stopImmediatePropagation());
const promise = once(target, 'ready', { signal: ac.signal });
target.dispatchEvent(new Event('error'));
const event = new Event('ready'); target.dispatchEvent(event);
line('once target', (await promise)[0] === event);
ac.abort();
const aborted = once(target, 'never', { signal: ac.signal });
await reject('once target aborted', () => aborted);
const active = new AbortController(), waiting = once(target, 'never', { signal: active.signal });
active.abort('target cancellation'); await reject('once target active abort', () => waiting);
const controller = new AbortController(), signalEvent = once(controller.signal, 'abort');
controller.abort(); line('once signal event', (await signalEvent)[0] instanceof Event);
}
{
const em = new EventEmitter(), symbol = Symbol('item');
const iterator = on(em, symbol, { close: ['end', 'close'] });
line('iterator self', iterator[Symbol.asyncIterator]() === iterator);
em.emit(symbol, 1, 'a'); em.emit(symbol, 2); em.emit('end');
line('iterator closed listeners', counts(em));
const values = []; for await (const value of iterator) values.push(value);
line('buffered close', values);
line('closed next', await iterator.next());
}
{
const em = new EventEmitter(), iterator = on(em, 'item');
const first = iterator.next(), second = iterator.next(), third = iterator.next();
em.emit('item', 'first'); em.emit('item', 'second');
line('pending delivery', [await first, await second]);
line('return result', await iterator.return('ignored'));
line('pending return', await third);
line('return cleaned', counts(em));
}
{
const em = new EventEmitter(), iterator = on(em, 'item'), failure = new Error('failure');
em.emit('item', 'buffered'); em.emit('error', failure);
line('error drains buffer', await iterator.next());
try { await iterator.next(); } catch (e) { line('iterator error identity', e === failure); }
line('error next done', await iterator.next());
line('error cleaned', counts(em));
const errorIterator = on(em, 'error'); em.emit('error', failure);
line('iterator error event', (await errorIterator.next()).value[0] === failure);
await errorIterator.return();
}
{
const em = new EventEmitter(), iterator = on(em, 'x'), failure = new Error('pending failure');
const first = iterator.next().catch(e => e === failure), second = iterator.next();
em.emit('error', failure);
line('pending error', [await first, await second]);
}
for (const preaborted of [false, true]) {
const em = new EventEmitter(), ac = new AbortController(), cause = { why: 'cancelled' };
ac.signal.addEventListener('abort', e => e.stopImmediatePropagation());
if (preaborted) ac.abort(cause);
try {
const iterator = on(em, 'x', { signal: ac.signal });
const first = iterator.next().catch(e => [shape(e), e.cause === cause]), second = iterator.next();
ac.abort(cause);
line('iterator aborted', [await first, await second, await iterator.next()]);
} catch (e) { line('iterator preaborted', [shape(e), e.cause === cause]); }
line('iterator abort cleaned', counts(em));
}
{
const em = new EventEmitter(), ac = new AbortController(), iterator = on(em, 'x', { signal: ac.signal });
em.emit('x', 'buffered'); ac.abort('buffered abort');
line('abort drains buffer', await iterator.next());
await reject('buffered abort', () => iterator.next());
line('buffered abort done', await iterator.next());
}
{
const em = new EventEmitter(), iterator = on(em, 'x', { close: ['end'] });
const a = iterator.next(), b = iterator.next(); em.emit('end');
line('pending close', [await a, await b, await iterator.next()]);
}
{
const em = new EventEmitter(), iterator = on(em, 'x');
const failure = new Error('thrown');
line('iterator throw return', iterator.throw(failure) === undefined);
try { await iterator.next(); } catch (e) { line('iterator throw identity', e === failure); }
line('iterator throw cleaned', counts(em));
for (const value of [undefined, null, 'failure', {}]) await reject('iterator throw type', () => iterator.throw(value));
}
for (const legacy of [false, true]) {
const em = new EventEmitter(), log = [];
em.pause = () => { log.push('pause'); }; em.resume = () => { log.push('resume'); };
const options = legacy ? { highWatermark: 2, lowWatermark: 2 } : { highWaterMark: 2, lowWaterMark: 2, highWatermark: 9 };
const iterator = on(em, 'item', options);
em.emit('item', 1); em.emit('item', 2); line('watermark threshold', log.slice());
em.emit('item', 3); line('watermark pause', log.slice());
await iterator.next(); line('watermark still paused', log.slice());
await iterator.next(); line('watermark resume', log.slice());
await iterator.return();
}
{
const em = new EventEmitter(), iterator = on(em, 'x');
for (let i = 0; i < 3000; i++) em.emit('x', i);
let sum = 0; for (let i = 0; i < 3000; i++) sum += (await iterator.next()).value[0];
line('queue compaction', sum); await iterator.return();
}
{
const em = new EventEmitter(), iterator = on(em, 'x');
queueMicrotask(() => { em.emit('x', 'loop'); });
for await (const value of iterator) { line('loop break', value); break; }
line('loop break cleaned', counts(em));
const ac = new AbortController(), returned = on(em, 'x', { signal: ac.signal });
await returned.return(); ac.abort(); line('return removes abort', await returned.next());
}
{
const target = new EventTarget(), iterator = on(target, 'item', { close: ['end'] });
target.dispatchEvent(new Event('error'));
const event = new Event('item'); target.dispatchEvent(event); target.dispatchEvent(new Event('end'));
line('iterator target', [(await iterator.next()).value[0] === event, await iterator.next()]);
}
{
const ac = new AbortController(), log = [];
ac.signal.addEventListener('abort', e => { log.push('stop'); e.stopImmediatePropagation(); });
ac.signal.addEventListener('abort', () => { log.push('ordinary'); });
const disposable = addAbortListener(ac.signal, function(e) { log.push([this === ac.signal, e instanceof Event, e.type]); });
line('disposable shape', [Object.getPrototypeOf(disposable) === null, typeof disposable[Symbol.dispose]]);
ac.abort(); ac.abort(); disposable[Symbol.dispose](); disposable[Symbol.dispose](); line('resistant abort', log);
}
{
const ac = new AbortController(), log = [];
const first = addAbortListener(ac.signal, () => { log.push('first'); second[Symbol.dispose](); });
const second = addAbortListener(ac.signal, () => { log.push('second'); });
const disposed = addAbortListener(ac.signal, () => { log.push('disposed'); });
disposed[Symbol.dispose](); ac.abort(); first[Symbol.dispose](); line('disposed abort', log);
}
{
const signal = AbortSignal.abort('already aborted'), log = [];
const disposable = addAbortListener(signal, function(e) { log.push([e === undefined, this === undefined]); });
disposable[Symbol.dispose](); line('preaborted scheduling', log.slice());
await Promise.resolve(); line('preaborted callback', log);
}
{
const ac = new AbortController(); let calls = 0;
const listener = () => { calls++; };
const a = addAbortListener(ac.signal, listener), b = addAbortListener(ac.signal, listener);
ac.abort(); line('duplicate abort listener', calls); a[Symbol.dispose](); b[Symbol.dispose]();
const other = new AbortController(), c = addAbortListener(other.signal, listener), d = addAbortListener(other.signal, listener);
c[Symbol.dispose](); other.abort(); line('duplicate disposal', calls); d[Symbol.dispose]();
}
{
const em = new EventEmitter(), ac = new AbortController(); let signals = 0, closes = 0, removed = 0;
const original = ac.signal.removeEventListener.bind(ac.signal);
ac.signal.removeEventListener = (...args) => { removed++; return original(...args); };
const options = { get signal() { signals++; return ac.signal; }, get close() { closes++; return ['end']; } };
const promise = once(em, 'x', options); em.emit('x', 1); await promise;
const iterator = on(em, 'x', options); await iterator.return();
line('option getters and abort cleanup', [signals, closes, removed]);
let lows = 0;
await reject('watermark validation order', () => on(em, 'x', { highWaterMark: 0, get lowWaterMark() { lows++; return 1; } }));
line('invalid high skips low', lows);
}
}
@@ -0,0 +1 @@
{"name":"node24-async-events-fixture","version":"1.0.0","type":"module","main":"index.js","types":"index.d.ts"}