• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3// Modeled very closely on the AbortController implementation
4// in https://github.com/mysticatea/abort-controller (MIT license)
5
6const {
7  ObjectAssign,
8  ObjectDefineProperties,
9  ObjectSetPrototypeOf,
10  ObjectDefineProperty,
11  PromiseResolve,
12  SafeFinalizationRegistry,
13  SafeSet,
14  Symbol,
15  SymbolToStringTag,
16  WeakRef,
17} = primordials;
18
19const {
20  defineEventHandler,
21  EventTarget,
22  Event,
23  kTrustEvent,
24  kNewListener,
25  kRemoveListener,
26  kResistStopPropagation,
27  kWeakHandler,
28} = require('internal/event_target');
29const {
30  createDeferredPromise,
31  customInspectSymbol,
32  kEmptyObject,
33  kEnumerableProperty,
34} = require('internal/util');
35const { inspect } = require('internal/util/inspect');
36const {
37  codes: {
38    ERR_ILLEGAL_CONSTRUCTOR,
39    ERR_INVALID_ARG_TYPE,
40    ERR_INVALID_THIS,
41  },
42} = require('internal/errors');
43
44const {
45  validateAbortSignal,
46  validateAbortSignalArray,
47  validateObject,
48  validateUint32,
49} = require('internal/validators');
50
51const {
52  DOMException,
53} = internalBinding('messaging');
54
55const {
56  clearTimeout,
57  setTimeout,
58} = require('timers');
59const assert = require('internal/assert');
60
61const {
62  messaging_deserialize_symbol: kDeserialize,
63  messaging_transfer_symbol: kTransfer,
64  messaging_transfer_list_symbol: kTransferList,
65} = internalBinding('symbols');
66
67let _MessageChannel;
68let makeTransferable;
69
70// Loading the MessageChannel and makeTransferable have to be done lazily
71// because otherwise we'll end up with a require cycle that ends up with
72// an incomplete initialization of abort_controller.
73
74function lazyMessageChannel() {
75  _MessageChannel ??= require('internal/worker/io').MessageChannel;
76  return new _MessageChannel();
77}
78
79function lazyMakeTransferable(obj) {
80  makeTransferable ??=
81    require('internal/worker/js_transferable').makeTransferable;
82  return makeTransferable(obj);
83}
84
85const clearTimeoutRegistry = new SafeFinalizationRegistry(clearTimeout);
86const gcPersistentSignals = new SafeSet();
87
88const kAborted = Symbol('kAborted');
89const kReason = Symbol('kReason');
90const kCloneData = Symbol('kCloneData');
91const kTimeout = Symbol('kTimeout');
92const kMakeTransferable = Symbol('kMakeTransferable');
93const kComposite = Symbol('kComposite');
94const kSourceSignals = Symbol('kSourceSignals');
95const kDependantSignals = Symbol('kDependantSignals');
96
97function customInspect(self, obj, depth, options) {
98  if (depth < 0)
99    return self;
100
101  const opts = ObjectAssign({}, options, {
102    depth: options.depth === null ? null : options.depth - 1,
103  });
104
105  return `${self.constructor.name} ${inspect(obj, opts)}`;
106}
107
108function validateThisAbortSignal(obj) {
109  if (obj?.[kAborted] === undefined)
110    throw new ERR_INVALID_THIS('AbortSignal');
111}
112
113// Because the AbortSignal timeout cannot be canceled, we don't want the
114// presence of the timer alone to keep the AbortSignal from being garbage
115// collected if it otherwise no longer accessible. We also don't want the
116// timer to keep the Node.js process open on it's own. Therefore, we wrap
117// the AbortSignal in a WeakRef and have the setTimeout callback close
118// over the WeakRef rather than directly over the AbortSignal, and we unref
119// the created timer object. Separately, we add the signal to a
120// FinalizerRegistry that will clear the timeout when the signal is gc'd.
121function setWeakAbortSignalTimeout(weakRef, delay) {
122  const timeout = setTimeout(() => {
123    const signal = weakRef.deref();
124    if (signal !== undefined) {
125      gcPersistentSignals.delete(signal);
126      abortSignal(
127        signal,
128        new DOMException(
129          'The operation was aborted due to timeout',
130          'TimeoutError'));
131    }
132  }, delay);
133  timeout.unref();
134  return timeout;
135}
136
137class AbortSignal extends EventTarget {
138  constructor() {
139    throw new ERR_ILLEGAL_CONSTRUCTOR();
140  }
141
142  /**
143   * @type {boolean}
144   */
145  get aborted() {
146    validateThisAbortSignal(this);
147    return !!this[kAborted];
148  }
149
150  /**
151   * @type {any}
152   */
153  get reason() {
154    validateThisAbortSignal(this);
155    return this[kReason];
156  }
157
158  throwIfAborted() {
159    validateThisAbortSignal(this);
160    if (this[kAborted]) {
161      throw this[kReason];
162    }
163  }
164
165  [customInspectSymbol](depth, options) {
166    return customInspect(this, {
167      aborted: this.aborted,
168    }, depth, options);
169  }
170
171  /**
172   * @param {any} [reason]
173   * @returns {AbortSignal}
174   */
175  static abort(
176    reason = new DOMException('This operation was aborted', 'AbortError')) {
177    return createAbortSignal({ aborted: true, reason });
178  }
179
180  /**
181   * @param {number} delay
182   * @returns {AbortSignal}
183   */
184  static timeout(delay) {
185    validateUint32(delay, 'delay', false);
186    const signal = createAbortSignal();
187    signal[kTimeout] = true;
188    clearTimeoutRegistry.register(
189      signal,
190      setWeakAbortSignalTimeout(new WeakRef(signal), delay));
191    return signal;
192  }
193
194  /**
195   * @param {AbortSignal[]} signals
196   * @returns {AbortSignal}
197   */
198  static any(signals) {
199    validateAbortSignalArray(signals, 'signals');
200    const resultSignal = createAbortSignal({ composite: true });
201    if (!signals.length) {
202      return resultSignal;
203    }
204    const resultSignalWeakRef = new WeakRef(resultSignal);
205    resultSignal[kSourceSignals] = new SafeSet();
206    for (let i = 0; i < signals.length; i++) {
207      const signal = signals[i];
208      if (signal.aborted) {
209        abortSignal(resultSignal, signal.reason);
210        return resultSignal;
211      }
212      signal[kDependantSignals] ??= new SafeSet();
213      if (!signal[kComposite]) {
214        resultSignal[kSourceSignals].add(new WeakRef(signal));
215        signal[kDependantSignals].add(resultSignalWeakRef);
216      } else if (!signal[kSourceSignals]) {
217        continue;
218      } else {
219        for (const sourceSignal of signal[kSourceSignals]) {
220          const sourceSignalRef = sourceSignal.deref();
221          if (!sourceSignalRef) {
222            continue;
223          }
224          assert(!sourceSignalRef.aborted);
225          assert(!sourceSignalRef[kComposite]);
226
227          if (resultSignal[kSourceSignals].has(sourceSignal)) {
228            continue;
229          }
230          resultSignal[kSourceSignals].add(sourceSignal);
231          sourceSignalRef[kDependantSignals].add(resultSignalWeakRef);
232        }
233      }
234    }
235    return resultSignal;
236  }
237
238  [kNewListener](size, type, listener, once, capture, passive, weak) {
239    super[kNewListener](size, type, listener, once, capture, passive, weak);
240    const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size);
241    if (isTimeoutOrNonEmptyCompositeSignal &&
242        type === 'abort' &&
243        !this.aborted &&
244        !weak &&
245        size === 1) {
246      // If this is a timeout signal, or a non-empty composite signal, and we're adding a non-weak abort
247      // listener, then we don't want it to be gc'd while the listener
248      // is attached and the timer still hasn't fired. So, we retain a
249      // strong ref that is held for as long as the listener is registered.
250      gcPersistentSignals.add(this);
251    }
252  }
253
254  [kRemoveListener](size, type, listener, capture) {
255    super[kRemoveListener](size, type, listener, capture);
256    const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size);
257    if (isTimeoutOrNonEmptyCompositeSignal && type === 'abort' && size === 0) {
258      gcPersistentSignals.delete(this);
259    }
260  }
261
262  [kTransfer]() {
263    validateThisAbortSignal(this);
264    const aborted = this.aborted;
265    if (aborted) {
266      const reason = this.reason;
267      return {
268        data: { aborted, reason },
269        deserializeInfo: 'internal/abort_controller:ClonedAbortSignal',
270      };
271    }
272
273    const { port1, port2 } = this[kCloneData];
274    this[kCloneData] = undefined;
275
276    this.addEventListener('abort', () => {
277      port1.postMessage(this.reason);
278      port1.close();
279    }, { once: true });
280
281    return {
282      data: { port: port2 },
283      deserializeInfo: 'internal/abort_controller:ClonedAbortSignal',
284    };
285  }
286
287  [kTransferList]() {
288    if (!this.aborted) {
289      const { port1, port2 } = lazyMessageChannel();
290      port1.unref();
291      port2.unref();
292      this[kCloneData] = {
293        port1,
294        port2,
295      };
296      return [port2];
297    }
298    return [];
299  }
300
301  [kDeserialize]({ aborted, reason, port }) {
302    if (aborted) {
303      this[kAborted] = aborted;
304      this[kReason] = reason;
305      return;
306    }
307
308    port.onmessage = ({ data }) => {
309      abortSignal(this, data);
310      port.close();
311      port.onmessage = undefined;
312    };
313    // The receiving port, by itself, should never keep the event loop open.
314    // The unref() has to be called *after* setting the onmessage handler.
315    port.unref();
316  }
317}
318
319function ClonedAbortSignal() {
320  return createAbortSignal({ transferable: true });
321}
322ClonedAbortSignal.prototype[kDeserialize] = () => { };
323
324ObjectDefineProperties(AbortSignal.prototype, {
325  aborted: kEnumerableProperty,
326});
327
328ObjectDefineProperty(AbortSignal.prototype, SymbolToStringTag, {
329  __proto__: null,
330  writable: false,
331  enumerable: false,
332  configurable: true,
333  value: 'AbortSignal',
334});
335
336defineEventHandler(AbortSignal.prototype, 'abort');
337
338/**
339 * @param {{
340 *   aborted? : boolean,
341 *   reason? : any,
342 *   transferable? : boolean,
343 *   composite? : boolean,
344 * }} [init]
345 * @returns {AbortSignal}
346 */
347function createAbortSignal(init = kEmptyObject) {
348  const {
349    aborted = false,
350    reason = undefined,
351    transferable = false,
352    composite = false,
353  } = init;
354  const signal = new EventTarget();
355  ObjectSetPrototypeOf(signal, AbortSignal.prototype);
356  signal[kAborted] = aborted;
357  signal[kReason] = reason;
358  signal[kComposite] = composite;
359  return transferable ? lazyMakeTransferable(signal) : signal;
360}
361
362function abortSignal(signal, reason) {
363  if (signal[kAborted]) return;
364  signal[kAborted] = true;
365  signal[kReason] = reason;
366  const event = new Event('abort', {
367    [kTrustEvent]: true,
368  });
369  signal.dispatchEvent(event);
370  signal[kDependantSignals]?.forEach((s) => {
371    const signalRef = s.deref();
372    if (signalRef) abortSignal(signalRef, reason);
373  });
374}
375
376// TODO(joyeecheung): use private fields and we'll get invalid access
377// validation from V8 instead of throwing ERR_INVALID_THIS ourselves.
378const kSignal = Symbol('signal');
379
380function validateAbortController(obj) {
381  if (obj?.[kSignal] === undefined)
382    throw new ERR_INVALID_THIS('AbortController');
383}
384
385class AbortController {
386  constructor() {
387    this[kSignal] = createAbortSignal();
388  }
389
390  /**
391   * @type {AbortSignal}
392   */
393  get signal() {
394    validateAbortController(this);
395    return this[kSignal];
396  }
397
398  /**
399   * @param {any} [reason]
400   */
401  abort(reason = new DOMException('This operation was aborted', 'AbortError')) {
402    validateAbortController(this);
403    abortSignal(this[kSignal], reason);
404  }
405
406  [customInspectSymbol](depth, options) {
407    return customInspect(this, {
408      signal: this.signal,
409    }, depth, options);
410  }
411
412  static [kMakeTransferable]() {
413    const controller = new AbortController();
414    controller[kSignal] = transferableAbortSignal(controller[kSignal]);
415    return controller;
416  }
417}
418
419/**
420 * Enables the AbortSignal to be transferable using structuredClone/postMessage.
421 * @param {AbortSignal} signal
422 * @returns {AbortSignal}
423 */
424function transferableAbortSignal(signal) {
425  if (signal?.[kAborted] === undefined)
426    throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
427  return lazyMakeTransferable(signal);
428}
429
430/**
431 * Creates an AbortController with a transferable AbortSignal
432 */
433function transferableAbortController() {
434  return AbortController[kMakeTransferable]();
435}
436
437/**
438 * @param {AbortSignal} signal
439 * @param {any} resource
440 * @returns {Promise<void>}
441 */
442async function aborted(signal, resource) {
443  if (signal === undefined) {
444    throw new ERR_INVALID_ARG_TYPE('signal', 'AbortSignal', signal);
445  }
446  validateAbortSignal(signal, 'signal');
447  validateObject(resource, 'resource', { nullable: false, allowFunction: true, allowArray: true });
448  if (signal.aborted)
449    return PromiseResolve();
450  const abortPromise = createDeferredPromise();
451  const opts = { __proto__: null, [kWeakHandler]: resource, once: true, [kResistStopPropagation]: true };
452  signal.addEventListener('abort', abortPromise.resolve, opts);
453  return abortPromise.promise;
454}
455
456ObjectDefineProperties(AbortController.prototype, {
457  signal: kEnumerableProperty,
458  abort: kEnumerableProperty,
459});
460
461ObjectDefineProperty(AbortController.prototype, SymbolToStringTag, {
462  __proto__: null,
463  writable: false,
464  enumerable: false,
465  configurable: true,
466  value: 'AbortController',
467});
468
469module.exports = {
470  AbortController,
471  AbortSignal,
472  ClonedAbortSignal,
473  aborted,
474  transferableAbortSignal,
475  transferableAbortController,
476};
477