UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

472 lines (421 loc) 12.9 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/abort_controller.js import * as __hoisted_internal_worker_js_transferable__ from "nstdlib/lib/internal/worker/js_transferable"; import { defineEventHandler, EventTarget, Event, kTrustEvent, kNewListener, kRemoveListener, kResistStopPropagation, kWeakHandler, } from "nstdlib/lib/internal/event_target"; import { createDeferredPromise, customInspectSymbol, kEmptyObject, kEnumerableProperty, } from "nstdlib/lib/internal/util"; import { inspect } from "nstdlib/lib/internal/util/inspect"; import { codes as __codes__ } from "nstdlib/lib/internal/errors"; import { validateAbortSignal, validateAbortSignalArray, validateObject, validateUint32, kValidateObjectAllowObjects, } from "nstdlib/lib/internal/validators"; import { DOMException } from "nstdlib/stub/binding/messaging"; import { clearTimeout, setTimeout } from "nstdlib/lib/timers"; import * as assert from "nstdlib/lib/internal/assert"; import { kDeserialize, kTransfer, kTransferList, } from "nstdlib/lib/internal/worker/js_transferable"; import * as __hoisted_internal_worker_io__ from "nstdlib/lib/internal/worker/io"; // Modeled very closely on the AbortController implementation // in https://github.com/mysticatea/abort-controller (MIT license) const { ERR_ILLEGAL_CONSTRUCTOR, ERR_INVALID_ARG_TYPE, ERR_INVALID_THIS } = __codes__; let _MessageChannel; let markTransferMode; const kDontThrowSymbol = Symbol("kDontThrowSymbol"); // 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. function lazyMessageChannel() { _MessageChannel ??= __hoisted_internal_worker_io__.MessageChannel; return new _MessageChannel(); } function lazyMarkTransferMode(obj, cloneable, transferable) { markTransferMode ??= __hoisted_internal_worker_js_transferable__.markTransferMode; markTransferMode(obj, cloneable, transferable); } const clearTimeoutRegistry = new FinalizationRegistry(clearTimeout); const gcPersistentSignals = new Set(); const kAborted = Symbol("kAborted"); const kReason = Symbol("kReason"); const kCloneData = Symbol("kCloneData"); const kTimeout = Symbol("kTimeout"); const kMakeTransferable = Symbol("kMakeTransferable"); const kComposite = Symbol("kComposite"); const kSourceSignals = Symbol("kSourceSignals"); const kDependantSignals = Symbol("kDependantSignals"); function customInspect(self, obj, depth, options) { if (depth < 0) return self; const opts = Object.assign({}, options, { depth: options.depth === null ? null : options.depth - 1, }); return `${self.constructor.name} ${inspect(obj, opts)}`; } function validateThisAbortSignal(obj) { if (obj?.[kAborted] === undefined) throw new ERR_INVALID_THIS("AbortSignal"); } // Because the AbortSignal timeout cannot be canceled, we don't want the // presence of the timer alone to keep the AbortSignal from being garbage // collected if it otherwise no longer accessible. We also don't want the // timer to keep the Node.js process open on it's own. Therefore, we wrap // the AbortSignal in a WeakRef and have the setTimeout callback close // 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. function setWeakAbortSignalTimeout(weakRef, delay) { const timeout = setTimeout(() => { const signal = weakRef.deref(); if (signal !== undefined) { gcPersistentSignals.delete(signal); abortSignal( signal, new DOMException( "The operation was aborted due to timeout", "TimeoutError", ), ); } }, delay); timeout.unref(); return timeout; } class AbortSignal extends EventTarget { /** * @param {symbol | undefined} dontThrowSymbol * @param {{ * aborted? : boolean, * reason? : any, * transferable? : boolean, * composite? : boolean, * }} [init] * @private */ constructor(dontThrowSymbol = undefined, init = kEmptyObject) { if (dontThrowSymbol !== kDontThrowSymbol) { throw new ERR_ILLEGAL_CONSTRUCTOR(); } super(); const { aborted = false, reason = undefined, transferable = false, composite = false, } = init; this[kAborted] = aborted; this[kReason] = reason; this[kComposite] = composite; if (transferable) { lazyMarkTransferMode(this, false, true); } } /** * @type {boolean} */ get aborted() { validateThisAbortSignal(this); return !!this[kAborted]; } /** * @type {any} */ get reason() { validateThisAbortSignal(this); return this[kReason]; } throwIfAborted() { validateThisAbortSignal(this); if (this[kAborted]) { throw this[kReason]; } } [customInspectSymbol](depth, options) { return customInspect( this, { aborted: this.aborted, }, depth, options, ); } /** * @param {any} [reason] * @returns {AbortSignal} */ static abort( reason = new DOMException("This operation was aborted", "AbortError"), ) { return new AbortSignal(kDontThrowSymbol, { aborted: true, reason }); } /** * @param {number} delay * @returns {AbortSignal} */ static timeout(delay) { validateUint32(delay, "delay", false); const signal = new AbortSignal(kDontThrowSymbol); signal[kTimeout] = true; clearTimeoutRegistry.register( signal, setWeakAbortSignalTimeout(new WeakRef(signal), delay), ); return signal; } /** * @param {AbortSignal[]} signals * @returns {AbortSignal} */ static any(signals) { validateAbortSignalArray(signals, "signals"); const resultSignal = new AbortSignal(kDontThrowSymbol, { composite: true }); if (!signals.length) { return resultSignal; } const resultSignalWeakRef = new WeakRef(resultSignal); resultSignal[kSourceSignals] = new Set(); for (let i = 0; i < signals.length; i++) { const signal = signals[i]; if (signal.aborted) { abortSignal(resultSignal, signal.reason); return resultSignal; } signal[kDependantSignals] ??= new Set(); if (!signal[kComposite]) { resultSignal[kSourceSignals].add(new WeakRef(signal)); signal[kDependantSignals].add(resultSignalWeakRef); } else if (!signal[kSourceSignals]) { continue; } else { for (const sourceSignal of signal[kSourceSignals]) { const sourceSignalRef = sourceSignal.deref(); if (!sourceSignalRef) { continue; } assert(!sourceSignalRef.aborted); assert(!sourceSignalRef[kComposite]); if (resultSignal[kSourceSignals].has(sourceSignal)) { continue; } resultSignal[kSourceSignals].add(sourceSignal); sourceSignalRef[kDependantSignals].add(resultSignalWeakRef); } } } return resultSignal; } [kNewListener](size, type, listener, once, capture, passive, weak) { super[kNewListener](size, type, listener, once, capture, passive, weak); const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size); if ( isTimeoutOrNonEmptyCompositeSignal && type === "abort" && !this.aborted && !weak && size === 1 ) { // If this is a timeout signal, or a non-empty composite signal, and we're adding a non-weak abort // listener, then we don't want it to be gc'd while the listener // is attached and the timer still hasn't fired. So, we retain a // strong ref that is held for as long as the listener is registered. gcPersistentSignals.add(this); } } [kRemoveListener](size, type, listener, capture) { super[kRemoveListener](size, type, listener, capture); const isTimeoutOrNonEmptyCompositeSignal = this[kTimeout] || (this[kComposite] && this[kSourceSignals]?.size); if (isTimeoutOrNonEmptyCompositeSignal && type === "abort" && size === 0) { gcPersistentSignals.delete(this); } } [kTransfer]() { validateThisAbortSignal(this); const aborted = this.aborted; if (aborted) { const reason = this.reason; return { data: { aborted, reason }, deserializeInfo: "internal/abort_controller:ClonedAbortSignal", }; } const { port1, port2 } = this[kCloneData]; this[kCloneData] = undefined; this.addEventListener( "abort", () => { port1.postMessage(this.reason); port1.close(); }, { once: true }, ); return { data: { port: port2 }, deserializeInfo: "internal/abort_controller:ClonedAbortSignal", }; } [kTransferList]() { if (!this.aborted) { const { port1, port2 } = lazyMessageChannel(); port1.unref(); port2.unref(); this[kCloneData] = { port1, port2, }; return [port2]; } return []; } [kDeserialize]({ aborted, reason, port }) { if (aborted) { this[kAborted] = aborted; this[kReason] = reason; return; } port.onmessage = ({ data }) => { abortSignal(this, data); port.close(); port.onmessage = undefined; }; // The receiving port, by itself, should never keep the event loop open. // The unref() has to be called *after* setting the onmessage handler. port.unref(); } } function ClonedAbortSignal() { return new AbortSignal(kDontThrowSymbol, { transferable: true }); } ClonedAbortSignal.prototype[kDeserialize] = () => {}; Object.defineProperties(AbortSignal.prototype, { aborted: kEnumerableProperty, }); Object.defineProperty(AbortSignal.prototype, Symbol.toStringTag, { __proto__: null, writable: false, enumerable: false, configurable: true, value: "AbortSignal", }); defineEventHandler(AbortSignal.prototype, "abort"); function abortSignal(signal, reason) { if (signal[kAborted]) return; signal[kAborted] = true; signal[kReason] = reason; const event = new Event("abort", { [kTrustEvent]: true, }); signal.dispatchEvent(event); signal[kDependantSignals]?.forEach((s) => { const signalRef = s.deref(); if (signalRef) abortSignal(signalRef, reason); }); } class AbortController { #signal; /** * @type {AbortSignal} */ get signal() { this.#signal ??= new AbortSignal(kDontThrowSymbol); return this.#signal; } /** * @param {any} [reason] */ abort(reason = new DOMException("This operation was aborted", "AbortError")) { abortSignal((this.#signal ??= new AbortSignal(kDontThrowSymbol)), reason); } [customInspectSymbol](depth, options) { return customInspect( this, { signal: this.signal, }, depth, options, ); } static [kMakeTransferable]() { const controller = new AbortController(); controller.#signal = new AbortSignal(kDontThrowSymbol, { transferable: true, }); return controller; } } /** * Enables the AbortSignal to be transferable using structuredClone/postMessage. * @param {AbortSignal} signal * @returns {AbortSignal} */ function transferableAbortSignal(signal) { if (signal?.[kAborted] === undefined) throw new ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); lazyMarkTransferMode(signal, false, true); return signal; } /** * Creates an AbortController with a transferable AbortSignal */ function transferableAbortController() { return AbortController[kMakeTransferable](); } /** * @param {AbortSignal} signal * @param {any} resource * @returns {Promise<void>} */ async function aborted(signal, resource) { if (signal === undefined) { throw new ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal); } validateAbortSignal(signal, "signal"); validateObject(resource, "resource", kValidateObjectAllowObjects); if (signal.aborted) return Promise.resolve(); const abortPromise = createDeferredPromise(); const opts = { __proto__: null, [kWeakHandler]: resource, once: true, [kResistStopPropagation]: true, }; signal.addEventListener("abort", abortPromise.resolve, opts); return abortPromise.promise; } Object.defineProperties(AbortController.prototype, { signal: kEnumerableProperty, abort: kEnumerableProperty, }); Object.defineProperty(AbortController.prototype, Symbol.toStringTag, { __proto__: null, writable: false, enumerable: false, configurable: true, value: "AbortController", }); export { AbortController }; export { AbortSignal }; export { ClonedAbortSignal }; export { aborted }; export { transferableAbortSignal }; export { transferableAbortController };