nodejs/node · #66789
lib: speed up AbortSignal
benchmark/abort_controller/abort-controller-abort.jsadded21 + / 0 −
@@ -0,0 +1,21 @@+'use strict';+const common = require('../common.js');++const bench = common.createBenchmark(main, {+ n: [1e6],+ listeners: [0, 1],+});++function main({ n, listeners }) {+ const reason = new Error('reason');+ const noop = () => {};++ bench.start();+ for (let i = 0; i < n; i++) {+ const ac = new AbortController();+ if (listeners > 0)+ ac.signal.addEventListener('abort', noop);+ ac.abort(reason);+ }+ bench.end(n);+}benchmark/abort_controller/abort-signal-add-remove.jsadded43 + / 0 −
@@ -0,0 +1,43 @@+'use strict';+const common = require('../common.js');+const { addAbortListener } = require('events');++const bench = common.createBenchmark(main, {+ n: [1e6],+ kind: ['addEventListener', 'addEventListener-once', 'addAbortListener'],+});++function main({ n, kind }) {+ const { signal } = new AbortController();+ const noop = () => {};++ switch (kind) {+ case 'addEventListener':+ bench.start();+ for (let i = 0; i < n; i++) {+ signal.addEventListener('abort', noop);+ signal.removeEventListener('abort', noop);+ }+ bench.end(n);+ break;+ case 'addEventListener-once': {+ const options = { once: true };+ bench.start();+ for (let i = 0; i < n; i++) {+ signal.addEventListener('abort', noop, options);+ signal.removeEventListener('abort', noop);+ }+ bench.end(n);+ break;+ }+ case 'addAbortListener':+ bench.start();+ for (let i = 0; i < n; i++) {+ addAbortListener(signal, noop)[Symbol.dispose]();+ }+ bench.end(n);+ break;+ default:+ throw new Error('Invalid kind');+ }+}benchmark/abort_controller/abort-signal-any.jsadded46 + / 0 −
@@ -0,0 +1,46 @@+'use strict';+const common = require('../common.js');++const bench = common.createBenchmark(main, {+ n: [2e5],+ kind: ['create', 'listen', 'abort'],+});++// Combines a per-iteration signal with a long-lived one, like a request+// signal combined with a server wide one. The work is split into batches+// that run in separate macrotasks because WeakRef targets are kept alive+// until the end of the current job, as they would be between requests.+function main({ n, kind }) {+ const reason = new Error('reason');+ const noop = () => {};+ const longLived = new AbortController().signal;+ const batch = 1000;++ function run(count) {+ for (let i = 0; i < count; i++) {+ const ac = new AbortController();+ const signal = AbortSignal.any([ac.signal, longLived]);+ if (kind === 'create')+ continue;+ signal.addEventListener('abort', noop, { once: true });+ if (kind === 'listen')+ signal.removeEventListener('abort', noop);+ else+ ac.abort(reason);+ }+ }++ let left = n;+ function step() {+ const count = Math.min(batch, left);+ run(count);+ left -= count;+ if (left > 0)+ setImmediate(step);+ else+ bench.end(n);+ }++ bench.start();+ step();+}lib/internal/abort_controller.js154 + / 89 −
@@ -5,9 +5,11 @@ const { ArrayPrototypePush,+ FunctionPrototypeCall, ObjectAssign, ObjectDefineProperties, ObjectDefineProperty,+ ObjectFreeze, ObjectSetPrototypeOf, PromiseResolve, PromiseWithResolvers,@@ -22,6 +24,7 @@ const { defineEventHandler, EventTarget, Event,+ kEvents, kTrustEvent, kNewListener, kRemoveListener,@@ -31,7 +34,6 @@ const { const { kMaxEventTargetListeners } = require('events'); const { customInspectSymbol,- kEmptyObject, kEnumerableProperty, } = require('internal/util'); const { inspect } = require('internal/util/inspect');@@ -41,6 +43,7 @@ const { ERR_INVALID_ARG_TYPE, ERR_INVALID_THIS, },+ isErrorStackTraceLimitWritable, } = require('internal/errors'); const { converters,@@ -75,6 +78,15 @@ let _MessageChannel; const kDontThrowSymbol = Symbol('kDontThrowSymbol'); +const kTrustedEventInit = ObjectFreeze({ [kTrustEvent]: true });++// V8 doesn't optimize keyed super property access such as+// super[kNewListener](), so call the EventTarget implementations directly.+const {+ [kNewListener]: eventTargetNewListener,+ [kRemoveListener]: eventTargetRemoveListener,+} = EventTarget.prototype;+ // Loading the MessageChannel and markTransferable have to be done lazily // because otherwise we'll end up with a require cycle that ends up with // an incomplete initialization of abort_controller.@@ -84,28 +96,37 @@ function lazyMessageChannel() { return new _MessageChannel(); } -const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout);-const dependantSignalsCleanupRegistry = new SafeFinalizationRegistry(- ({ sourceSignalRef, dependantSignalRef }) => {- sourceSignalsCleanupRegistry.unregister(dependantSignalRef);+const clearTimeoutRegistry = new SafeFinalizationRegistry((holder) => {+ if (holder.timeout !== undefined) {+ clearTimeout(holder.timeout);+ }+}); - const sourceSignal = sourceSignalRef.deref();- if (sourceSignal === undefined) {- return;+// Each followed composite signal is registered once. When it is collected,+// remove it from the dependant signals of its remaining sources.+const dependantSignalsCleanupRegistry = new SafeFinalizationRegistry(+ ({ sourceSignals, dependantSignalRef }) => {+ for (const sourceSignalRef of sourceSignals) {+ sourceSignalRef.deref()?.[kDependantSignals]?.delete(dependantSignalRef); }- sourceSignal[kDependantSignals].delete(dependantSignalRef); }); const gcPersistentSignals = new SafeSet(); -const sourceSignalsCleanupRegistry = new SafeFinalizationRegistry(({ sourceSignalRef, composedSignalRef }) => {- const composedSignal = composedSignalRef.deref();- if (composedSignal !== undefined) {- composedSignal[kSourceSignals].delete(sourceSignalRef);-- if (composedSignal[kSourceSignals].size === 0) {- // This signal will no longer abort. There's no need to keep it in the gcPersistentSignals set.- gcPersistentSignals.delete(composedSignal);+// Each source signal is registered once, when it gets its first dependant.+// When it is collected, remove it from the source signals of its remaining+// dependants. Registering per source rather than per (source, dependant) pair+// avoids unregister tokens, which are expensive to register.+const sourceSignalsCleanupRegistry = new SafeFinalizationRegistry(({ sourceSignalRef, dependantSignals }) => {+ for (const composedSignalRef of dependantSignals) {+ const composedSignal = composedSignalRef.deref();+ if (composedSignal !== undefined) {+ composedSignal[kSourceSignals].delete(sourceSignalRef);++ if (composedSignal[kSourceSignals].size === 0) {+ // This signal will no longer abort. There's no need to keep it in the gcPersistentSignals set.+ gcPersistentSignals.delete(composedSignal);+ } } } });@@ -117,10 +138,16 @@ const kTimeout = Symbol('kTimeout'); const kMakeTransferable = Symbol('kMakeTransferable'); const kComposite = Symbol('kComposite'); const kFollowing = Symbol('kFollowing');-const kResultSignalWeakRef = Symbol('kResultSignalWeakRef');+const kWeakRef = Symbol('kWeakRef'); const kSourceSignals = Symbol('kSourceSignals'); const kDependantSignals = Symbol('kDependantSignals'); +// A signal's own WeakRef, shared by every composite that follows it so that+// a long-lived source doesn't allocate a new WeakRef per AbortSignal.any().+function getWeakRef(signal) {+ return signal[kWeakRef] ??= new SafeWeakRef(signal);+}+ function customInspect(self, obj, depth, options) { if (depth < 0) return self;@@ -161,12 +188,18 @@ function followCompositeSignal(signal) { return; } - const resultSignalWeakRef = signal[kResultSignalWeakRef] ??= new SafeWeakRef(signal);+ const resultSignalWeakRef = getWeakRef(signal);+ const sourceSignals = signal[kSourceSignals]; - for (const sourceSignalWeakRef of signal[kSourceSignals]) {+ dependantSignalsCleanupRegistry.register(signal, {+ sourceSignals,+ dependantSignalRef: resultSignalWeakRef,+ });++ for (const sourceSignalWeakRef of sourceSignals) { const sourceSignal = sourceSignalWeakRef.deref(); if (sourceSignal === undefined) {- signal[kSourceSignals].delete(sourceSignalWeakRef);+ sourceSignals.delete(sourceSignalWeakRef); continue; } @@ -175,19 +208,15 @@ function followCompositeSignal(signal) { return; } - sourceSignal[kDependantSignals] ??= new SafeSet();- sourceSignal[kDependantSignals].add(resultSignalWeakRef);- dependantSignalsCleanupRegistry.register(signal, {- sourceSignalRef: sourceSignalWeakRef,- dependantSignalRef: resultSignalWeakRef,- });- // Nested composites share their sources' WeakRefs, so use this signal's- // own WeakRef as the unregister token. Otherwise collecting an inner- // composite would also unregister the outer composite from its sources.- sourceSignalsCleanupRegistry.register(sourceSignal, {- sourceSignalRef: sourceSignalWeakRef,- composedSignalRef: resultSignalWeakRef,- }, resultSignalWeakRef);+ let dependantSignals = sourceSignal[kDependantSignals];+ if (dependantSignals === undefined) {+ dependantSignals = sourceSignal[kDependantSignals] = new SafeSet();+ sourceSignalsCleanupRegistry.register(sourceSignal, {+ sourceSignalRef: sourceSignalWeakRef,+ dependantSignals,+ });+ }+ dependantSignals.add(resultSignalWeakRef); } signal[kFollowing] = true;@@ -201,11 +230,16 @@ function followCompositeSignal(signal) { // over the WeakRef rather than directly over the AbortSignal, and we unref // the created timer object. Separately, we add the signal to a // FinalizerRegistry that will clear the timeout when the signal is gc'd.+// The registry holds the timeout through a holder that is emptied once the+// timer fires, which releases the fired timer without having to unregister+// the signal. Registering with an unregister token is several times more+// expensive than registering without one. function setWeakAbortSignalTimeout(weakRef, delay) {- const timeout = setTimeout(() => {+ const holder = { timeout: undefined };+ holder.timeout = setTimeout(() => {+ holder.timeout = undefined; const signal = weakRef.deref(); if (signal !== undefined) {- clearTimeoutRegistry.unregister(signal); gcPersistentSignals.delete(signal); abortSignal( signal,@@ -214,8 +248,25 @@ function setWeakAbortSignalTimeout(weakRef, delay) { 'TimeoutError')); } }, delay);- timeout.unref();- return timeout;+ holder.timeout.unref();+ return holder;+}++// The default reason captures no stack. A captured stack keeps the receivers+// and functions of the aborting caller's frames alive until it is formatted,+// so a retained signal would keep the aborted work alive, and formatting it+// eagerly costs several microseconds per abort. Callers that want the stack+// can pass their own reason.+// Refs: https://github.com/nodejs/node/issues/66192+function createDefaultAbortReason() {+ if (!isErrorStackTraceLimitWritable()) {+ return new DOMException('This operation was aborted', 'AbortError');+ }+ const limit = Error.stackTraceLimit;+ Error.stackTraceLimit = 0;+ const reason = new DOMException('This operation was aborted', 'AbortError');+ Error.stackTraceLimit = limit;+ return reason; } class AbortSignal extends EventTarget {@@ -229,31 +280,37 @@ class AbortSignal extends EventTarget { } /**+ * Takes positional arguments rather than an options object: destructuring+ * the default kEmptyObject, which V8 keeps in dictionary mode, cost more+ * than the rest of the constructor. * @param {symbol | undefined} dontThrowSymbol- * @param {{- * aborted? : boolean,- * reason? : any,- * transferable? : boolean,- * composite? : boolean,- * }} [init]+ * @param {boolean} [aborted]+ * @param {any} [reason]+ * @param {boolean} [transferable]+ * @param {boolean} [composite] * @private */- constructor(dontThrowSymbol = undefined, init = kEmptyObject) {+ constructor(dontThrowSymbol = undefined, aborted = false, reason = undefined,+ transferable = false, composite = false) { if (dontThrowSymbol !== kDontThrowSymbol) { throw new ERR_ILLEGAL_CONSTRUCTOR(); } super(); this[kMaxEventTargetListeners] = 0;- const {- aborted = false,- reason = undefined,- transferable = false,- composite = false,- } = init; this[kAborted] = aborted; this[kReason] = reason; this[kComposite] = composite;+ // Signals that become sources of AbortSignal.any() get these slots later.+ // Initialize them up front so that adding them doesn't change the shape+ // of live signals and make the call sites that use them polymorphic.+ this[kWeakRef] = undefined;+ this[kDependantSignals] = undefined;+ if (composite) {+ this[kTimeout] = false;+ this[kFollowing] = false;+ this[kSourceSignals] = undefined;+ } if (transferable) { markTransferMode(this, false, true); }@@ -295,9 +352,8 @@ class AbortSignal extends EventTarget { * @param {any} [reason] * @returns {AbortSignal} */- static abort(- reason = new DOMException('This operation was aborted', 'AbortError')) {- return new AbortSignal(kDontThrowSymbol, { aborted: true, reason });+ static abort(reason = createDefaultAbortReason()) {+ return new AbortSignal(kDontThrowSymbol, true, reason); } /**@@ -310,8 +366,7 @@ class AbortSignal extends EventTarget { signal[kTimeout] = true; clearTimeoutRegistry.register( signal,- setWeakAbortSignalTimeout(new SafeWeakRef(signal), delay),- signal);+ setWeakAbortSignalTimeout(getWeakRef(signal), delay)); return signal; } @@ -325,7 +380,7 @@ class AbortSignal extends EventTarget { { __proto__: null, context: 'signals' }, ); - const resultSignal = new AbortSignal(kDontThrowSymbol, { composite: true });+ const resultSignal = new AbortSignal(kDontThrowSymbol, false, undefined, false, true); if (!signalsArray.length) { return resultSignal; }@@ -354,8 +409,7 @@ class AbortSignal extends EventTarget { } if (!signal[kComposite]) {- const signalWeakRef = new SafeWeakRef(signal);- resultSignal[kSourceSignals].add(signalWeakRef);+ resultSignal[kSourceSignals].add(getWeakRef(signal)); } else if (!signal[kSourceSignals]) { continue; } else {@@ -391,7 +445,7 @@ class AbortSignal extends EventTarget { } [kNewListener](size, type, listener, once, capture, passive, weak) {- super[kNewListener](size, type, listener, once, capture, passive, weak);+ FunctionPrototypeCall(eventTargetNewListener, this, size, type, listener, once, capture, passive, weak); if (this[kComposite] && type === 'abort' && !this.aborted && size === 1) { followCompositeSignal(this);@@ -412,7 +466,7 @@ class AbortSignal extends EventTarget { } [kRemoveListener](size, type, listener, capture) {- super[kRemoveListener](size, type, listener, capture);+ FunctionPrototypeCall(eventTargetRemoveListener, this, size, type, listener, capture); const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size); if (isTimeoutOrNonEmptyCompositeSignal && type === 'abort' && size === 0) { gcPersistentSignals.delete(this);@@ -479,7 +533,7 @@ class AbortSignal extends EventTarget { converters['sequence<AbortSignal>'] = createSequenceConverter(converters.AbortSignal); function ClonedAbortSignal() {- return new AbortSignal(kDontThrowSymbol, { transferable: true });+ return new AbortSignal(kDontThrowSymbol, false, undefined, true); } ClonedAbortSignal.prototype[kDeserialize] = () => {}; @@ -508,36 +562,43 @@ function abortSignal(signal, reason) { signal[kReason] = reason; // 3. Let dependentSignalsToAbort be a new list.- const dependentSignalsToAbort = ObjectSetPrototypeOf([], null);+ // The list is only allocated when there are dependent signals.+ let dependentSignalsToAbort; // 4. For each dependentSignal of signal's dependent signals:- signal[kDependantSignals]?.forEach((s) => {- const dependentSignal = s.deref();- // 1. If dependentSignal is not aborted, then:- if (dependentSignal && !dependentSignal[kAborted]) {- // 1. Set dependentSignal's abort reason to signal's abort reason.- dependentSignal[kReason] = reason;- dependentSignal[kAborted] = true;- // 2. Append dependentSignal to dependentSignalsToAbort.- ArrayPrototypePush(dependentSignalsToAbort, dependentSignal);- }- });+ const dependantSignals = signal[kDependantSignals];+ if (dependantSignals !== undefined && dependantSignals.size > 0) {+ dependentSignalsToAbort = ObjectSetPrototypeOf([], null);+ dependantSignals.forEach((s) => {+ const dependentSignal = s.deref();+ // 1. If dependentSignal is not aborted, then:+ if (dependentSignal && !dependentSignal[kAborted]) {+ // 1. Set dependentSignal's abort reason to signal's abort reason.+ dependentSignal[kReason] = reason;+ dependentSignal[kAborted] = true;+ // 2. Append dependentSignal to dependentSignalsToAbort.+ ArrayPrototypePush(dependentSignalsToAbort, dependentSignal);+ }+ });+ } // 5. Run the abort steps for signal runAbort(signal); // 6. For each dependentSignal of dependentSignalsToAbort, // run the abort steps for dependentSignal.- for (let i = 0; i < dependentSignalsToAbort.length; i++) {- const dependentSignal = dependentSignalsToAbort[i];- runAbort(dependentSignal);- // A transitively-aborted dependent signal can never abort again, so there- // is no longer any reason to keep it alive. Dropping it from- // gcPersistentSignals lets it be collected, which in turn prunes its- // WeakRef from its sources' kDependantSignals sets. Otherwise, an observed- // composite that follows a long-lived source stays retained forever and- // its entry accumulates in that source's kDependantSignals.- gcPersistentSignals.delete(dependentSignal);+ if (dependentSignalsToAbort !== undefined) {+ for (let i = 0; i < dependentSignalsToAbort.length; i++) {+ const dependentSignal = dependentSignalsToAbort[i];+ runAbort(dependentSignal);+ // A transitively-aborted dependent signal can never abort again, so there+ // is no longer any reason to keep it alive. Dropping it from+ // gcPersistentSignals lets it be collected, which in turn prunes its+ // WeakRef from its sources' kDependantSignals sets. Otherwise, an observed+ // composite that follows a long-lived source stays retained forever and+ // its entry accumulates in that source's kDependantSignals.+ gcPersistentSignals.delete(dependentSignal);+ } } // Clean up the signal from gcPersistentSignals@@ -557,9 +618,13 @@ function abortSignal(signal, reason) { // To run the abort steps for an AbortSignal signal function runAbort(signal) {- const event = new Event('abort', {- [kTrustEvent]: true,- });+ // Firing an event at a signal without abort listeners has no observable+ // effect, so skip allocating and dispatching it.+ const root = signal[kEvents]?.get('abort');+ if (root === undefined || root.next === undefined) {+ return;+ }+ const event = new Event('abort', kTrustedEventInit); signal.dispatchEvent(event); } @@ -578,7 +643,7 @@ class AbortController { /** * @param {any} [reason] */- abort(reason = new DOMException('This operation was aborted', 'AbortError')) {+ abort(reason = createDefaultAbortReason()) { abortSignal(this.#signal ??= new AbortSignal(kDontThrowSymbol), reason); } @@ -590,7 +655,7 @@ class AbortController { static [kMakeTransferable]() { const controller = new AbortController();- controller.#signal = new AbortSignal(kDontThrowSymbol, { transferable: true });+ controller.#signal = new AbortSignal(kDontThrowSymbol, false, undefined, true); return controller; } }lib/internal/event_target.js42 + / 56 −
@@ -17,7 +17,6 @@ const { SafeSet, SafeWeakMap, SafeWeakRef,- SafeWeakSet, String, Symbol, SymbolFor,@@ -79,19 +78,7 @@ const kType = Symbol('type'); const kDetail = Symbol('detail'); const kInPassiveListener = Symbol('kInPassiveListener'); -const isTrustedSet = new SafeWeakSet();-const isTrusted = ObjectGetOwnPropertyDescriptor({- get isTrusted() {- return isTrustedSet.has(this);- },-}, 'isTrusted').get;--const isTrustedDescriptor = {- __proto__: null,- configurable: false,- enumerable: true,- get: isTrusted,-};+let isTrusted; function isEvent(value) { return typeof value?.[kType] === 'string';@@ -104,6 +91,16 @@ class Event { #defaultPrevented = false; #timestamp = now(); #propagationStopped = false;+ #isTrusted = false;++ static {+ isTrusted = ObjectGetOwnPropertyDescriptor({+ get isTrusted() {+ return typeof this === 'object' && this !== null &&+ #isTrusted in this && this.#isTrusted;+ },+ }, 'isTrusted').get;+ } /** * @param {string} type@@ -124,7 +121,7 @@ class Event { this[kType] = `${type}`; if (options?.[kTrustEvent]) {- isTrustedSet.add(this);+ this.#isTrusted = true; } this[kTarget] = null;@@ -367,7 +364,12 @@ ObjectDefineProperties( // LegacyUnforgeable but defining it in the constructor has a big // performance impact and the property doesn't seem to be useful outside of // browsers.- isTrusted: isTrustedDescriptor,+ isTrusted: {+ __proto__: null,+ configurable: false,+ enumerable: true,+ get: isTrusted,+ }, }); const staticProps = ['NONE', 'CAPTURING_PHASE', 'AT_TARGET', 'BUBBLING_PHASE'];@@ -481,9 +483,7 @@ class Listener { flags |= kFlagResistStopPropagation; this.flags = flags; - this.removed = false;-- if (this.weak) {+ if (weak) { this.callback = new SafeWeakRef(listener); weakListeners().registry.register(listener, { __proto__: null,@@ -545,17 +545,18 @@ class Listener { } same(listener, capture) {- const myListener = this.weak ? this.listener.deref() : this.listener;- return myListener === listener && this.capture === capture;+ const flags = this.flags;+ const myListener = (flags & kFlagWeak) !== 0 ? this.listener.deref() : this.listener;+ return myListener === listener && ((flags & kFlagCapture) !== 0) === capture; } remove() { if (this.previous !== undefined) this.previous.next = this.next; if (this.next !== undefined) this.next.previous = this.previous;- this.removed = true;- if (this.weak) {+ this.flags |= kFlagRemoved;+ if ((this.flags & kFlagWeak) !== 0) { weakListeners().registry.unregister(this); const weakKey = this.weakKeyRef?.deref(); const listener = this.callback?.deref();@@ -646,28 +647,31 @@ class EventTarget { let weak = false; let resistStopPropagation = false; - if (options !== kEmptyObject) {- // We validateOptions before the validateListener check because the spec- // requires us to hit getters.- options = validateEventListenerOptions(options);-- once = options.once;- capture = options.capture;- passive = options.passive;- isNodeStyleListener = options.isNodeStyleListener;- weak = options.weak;- resistStopPropagation = options.resistStopPropagation;-+ // We flatten the options before the validateListener check because the+ // spec requires us to hit getters.+ if (typeof options === 'boolean') {+ capture = options;+ } else if (options !== kEmptyObject && options !== null) {+ validateObject(options, 'options', kValidateObjectAllowObjects);+ once = Boolean(options.once);+ capture = Boolean(options.capture);+ passive = Boolean(options.passive); const signal = options.signal;+ weak = options[kWeakHandler];+ resistStopPropagation = options[kResistStopPropagation] ?? false;+ isNodeStyleListener = Boolean(options[kIsNodeStyleListener]); - validateAbortSignal(signal, 'options.signal');+ if (signal !== undefined) {+ validateAbortSignal(signal, 'options.signal'); - if (signal) { if (signal.aborted) { return; }+ // Capture a copy so that the closure doesn't move `capture` into the+ // function context, which slows down the listener lookup below.+ const removeCapture = capture; signal.addEventListener('abort', () => {- this.removeEventListener(type, listener, options);+ this.removeEventListener(type, listener, removeCapture); }, { __proto__: null, once: true, [kWeakHandler]: this, [kResistStopPropagation]: true }); } }@@ -1106,24 +1110,6 @@ function validateEventListener(listener) { throw new ERR_INVALID_ARG_TYPE('listener', 'EventListener', listener); } -function validateEventListenerOptions(options) {- if (typeof options === 'boolean')- return { capture: options };-- if (options === null)- return kEmptyObject;- validateObject(options, 'options', kValidateObjectAllowObjects);- return {- once: Boolean(options.once),- capture: Boolean(options.capture),- passive: Boolean(options.passive),- signal: options.signal,- weak: options[kWeakHandler],- resistStopPropagation: options[kResistStopPropagation] ?? false,- isNodeStyleListener: Boolean(options[kIsNodeStyleListener]),- };-}- // Test whether the argument is an event object. This is far from a fool-proof // test, for example this input will result in a false positive: // > isEventTarget({ constructor: EventTarget })test/fixtures/test-runner/output/abort.snapshot0 + / 20 −
@@ -54,8 +54,6 @@ TAP version 13 error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames> ... # Subtest: not ok 4 not ok 8 - not ok 4@@ -67,8 +65,6 @@ TAP version 13 error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames> ... # Subtest: not ok 5 not ok 9 - not ok 5@@ -80,8 +76,6 @@ TAP version 13 error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames> ... 1..9 not ok 1 - promise timeout signal@@ -106,10 +100,6 @@ not ok 2 - promise abort signal error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames>- Object.<anonymous> (<project-root>/test/fixtures/test-runner/output/abort.js:21:52)- <node-internal-frames> ... # Subtest: callback timeout signal # Subtest: ok 1@@ -166,8 +156,6 @@ not ok 2 - promise abort signal error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames> ... # Subtest: not ok 4 not ok 8 - not ok 4@@ -179,8 +167,6 @@ not ok 2 - promise abort signal error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames> ... # Subtest: not ok 5 not ok 9 - not ok 5@@ -192,8 +178,6 @@ not ok 2 - promise abort signal error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames> ... 1..9 not ok 3 - callback timeout signal@@ -218,10 +202,6 @@ not ok 4 - callback abort signal error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames>- Object.<anonymous> (<project-root>/test/fixtures/test-runner/output/abort.js:39:53)- <node-internal-frames> ... 1..4 # tests 22test/fixtures/test-runner/output/abort_hooks.snapshot0 + / 24 −
@@ -38,10 +38,6 @@ not ok 1 - 1 before describe error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames>- before.signal (<project-root>/test/fixtures/test-runner/output/abort_hooks.js:8:8)- <node-internal-frames> ... # Subtest: 2 after describe # Subtest: test 1@@ -66,10 +62,6 @@ not ok 2 - 2 after describe error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames>- after.signal (<project-root>/test/fixtures/test-runner/output/abort_hooks.js:23:8)- <node-internal-frames> ... # Subtest: 3 beforeEach describe # Subtest: test 1@@ -82,10 +74,6 @@ not ok 2 - 2 after describe error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames>- beforeEach.signal (<project-root>/test/fixtures/test-runner/output/abort_hooks.js:38:8)- <node-internal-frames> ... # Subtest: test 2 not ok 2 - test 2@@ -97,10 +85,6 @@ not ok 2 - 2 after describe error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames>- beforeEach.signal (<project-root>/test/fixtures/test-runner/output/abort_hooks.js:38:8)- <node-internal-frames> ... 1..2 not ok 3 - 3 beforeEach describe@@ -123,10 +107,6 @@ not ok 3 - 3 beforeEach describe error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames>- afterEach.signal (<project-root>/test/fixtures/test-runner/output/abort_hooks.js:53:8)- <node-internal-frames> ... # Subtest: test 2 not ok 2 - test 2@@ -138,10 +118,6 @@ not ok 3 - 3 beforeEach describe error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames>- afterEach.signal (<project-root>/test/fixtures/test-runner/output/abort_hooks.js:53:8)- <node-internal-frames> ... 1..2 not ok 4 - 4 afterEach describetest/fixtures/test-runner/output/abort_suite.snapshot0 + / 10 −
@@ -54,8 +54,6 @@ TAP version 13 error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames> ... # Subtest: not ok 4 not ok 8 - not ok 4@@ -67,8 +65,6 @@ TAP version 13 error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames> ... # Subtest: not ok 5 not ok 9 - not ok 5@@ -80,8 +76,6 @@ TAP version 13 error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames> ... 1..9 not ok 1 - describe timeout signal@@ -106,10 +100,6 @@ not ok 2 - describe abort signal error: 'This operation was aborted' code: 20 name: 'AbortError'- stack: |-- <node-internal-frames>- Object.<anonymous> (<project-root>/test/fixtures/test-runner/output/abort_suite.js:19:57)- <node-internal-frames> ... 1..2 # tests 9test/parallel/test-abortcontroller-default-reason-gc.mjsadded66 + / 0 −
@@ -0,0 +1,66 @@+// Flags: --expose-gc++import '../common/index.mjs';+import { gcUntil } from '../common/gc.js';+import assert from 'node:assert/strict';+import { it } from 'node:test';++// The default abort reason must not keep the objects on the caller's stack+// alive. It captures no stack at all. Refs: https://github.com/nodejs/node/issues/66192++class Job {+ controller = new AbortController();++ cancel() {+ this.controller.abort();+ return this.controller.signal;+ }++ cancelStatic() {+ return AbortSignal.abort();+ }+}++for (const method of ['cancel', 'cancelStatic']) {+ it(`does not retain the caller through the default reason (${method})`, async () => {+ let job = new Job();+ const jobRef = new WeakRef(job);+ const signal = job[method]();+ job = null;++ await gcUntil('job is collected', () => jobRef.deref() === undefined);+ assert.strictEqual(signal.aborted, true);+ assert.ok(signal.reason instanceof DOMException);+ assert.strictEqual(signal.reason.name, 'AbortError');+ assert.strictEqual(signal.reason.message, 'This operation was aborted');+ assert.doesNotMatch(signal.reason.stack, /\n\s+at /);+ });+}++it('creates a new default reason for every abort', () => {+ assert.notStrictEqual(new Job().cancel().reason, new Job().cancel().reason);+ assert.notStrictEqual(AbortSignal.abort().reason, AbortSignal.abort().reason);+});++it('restores Error.stackTraceLimit', () => {+ const { stackTraceLimit } = Error;+ Error.stackTraceLimit = 7;+ try {+ new Job().cancel();+ new Job().cancelStatic();+ assert.strictEqual(Error.stackTraceLimit, 7);+ } finally {+ Error.stackTraceLimit = stackTraceLimit;+ }+});++it('does not throw when Error.prepareStackTrace throws', () => {+ const { prepareStackTrace } = Error;+ Error.prepareStackTrace = () => { throw new Error('boom'); };+ try {+ assert.strictEqual(new Job().cancel().reason.name, 'AbortError');+ assert.strictEqual(new Job().cancelStatic().reason.name, 'AbortError');+ } finally {+ Error.prepareStackTrace = prepareStackTrace;+ }+});test/parallel/test-abortsignal-any.mjs19 + / 0 −
@@ -75,6 +75,25 @@ describe('AbortSignal.any()', { concurrency: !process.env.TEST_PARALLEL }, () => assert.strictEqual(signal.reason, 'reason'); }); + it('aborts every signal that shares a source', () => {+ const controller = new AbortController();+ const other = new AbortController();+ const signals = [+ AbortSignal.any([controller.signal]),+ AbortSignal.any([other.signal, controller.signal]),+ AbortSignal.any([AbortSignal.any([controller.signal]), other.signal]),+ ];+ for (const signal of signals) {+ signal.addEventListener('abort', common.mustCall());+ }+ controller.abort('reason');+ for (const signal of signals) {+ assert.ok(signal.aborted);+ assert.strictEqual(signal.reason, 'reason');+ }+ other.abort('other reason');+ });+ it('handles deeply aborted signals', async () => { const controllers = Array.from({ length: 2 }, () => new AbortController()); const composedSignal1 = AbortSignal.any([controllers[0].signal]);test/parallel/test-abortsignal-drop-settled-signals.mjs17 + / 0 −
@@ -271,6 +271,23 @@ it('drops an observed composite of a composite once its sources are collected', await gcUntil('outer composite is collected', () => outerRef.deref() === undefined); }); +it('drops observed composites that share a source once it is collected', async () => {+ const handler = () => {};+ // Using a helper keeps the source and the composites off this stack.+ const createObservedComposites = () => {+ const controller = new AbortController();+ return Array.from({ length: 10 }, () => {+ const composite = AbortSignal.any([controller.signal]);+ composite.addEventListener('abort', handler);+ return new WeakRef(composite);+ });+ };+ const refs = createObservedComposites();++ await gcUntil('observed composites are collected',+ () => refs.every((ref) => ref.deref() === undefined));+});+ it('drops settled signals even when there are listeners', (t, done) => { runWithOrphanListeners(limit, async (signalRefs) => { await gcUntil('all signals are GCed', () => {test/parallel/test-eventtarget.js31 + / 0 −
@@ -779,3 +779,34 @@ let asyncTest = Promise.resolve(); new Event('', null); new Event('', undefined); }++{+ // Aborting the signal option only removes the listener that was registered+ // with the same capture flag.+ const et = new EventTarget();+ const controller = new AbortController();+ let calls = 0;+ const listener = () => calls++;+ et.addEventListener('foo', listener, { capture: true, signal: controller.signal });+ et.addEventListener('foo', listener, false);+ et.dispatchEvent(new Event('foo'));+ assert.strictEqual(calls, 2);+ controller.abort();+ et.dispatchEvent(new Event('foo'));+ assert.strictEqual(calls, 3);+ et.removeEventListener('foo', listener);+ et.dispatchEvent(new Event('foo'));+ assert.strictEqual(calls, 3);+}++{+ // The isTrusted getter returns false for any receiver that is not a trusted+ // event, without throwing.+ const { get } = Object.getOwnPropertyDescriptor(Event.prototype, 'isTrusted');+ for (const value of [+ undefined, null, 1, 'foo', Symbol('foo'), {}, () => {},+ { __proto__: Event.prototype }, new Event('foo'), new CustomEvent('foo'),+ ]) {+ assert.strictEqual(get.call(value), false);+ }+}