// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // @ts-check /// import { core, primordials } from "ext:core/mod.js"; const { ArrayPrototypeEvery, ArrayPrototypePush, FunctionPrototypeApply, ObjectPrototypeIsPrototypeOf, SafeSet, SafeSetIterator, SafeWeakRef, SafeWeakSet, SetPrototypeAdd, SetPrototypeDelete, Symbol, SymbolFor, TypeError, WeakRefPrototypeDeref, WeakSetPrototypeAdd, WeakSetPrototypeHas, } = primordials; import * as webidl from "ext:deno_webidl/00_webidl.js"; import { assert } from "./00_infra.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; import { defineEventHandler, Event, EventTarget, listenerCount, setIsTrusted, } from "./02_event.js"; import { clearTimeout, refTimer, unrefTimer } from "./02_timers.js"; // Since WeakSet is not a iterable, WeakRefSet class is provided to store and // iterate objects. // To create an AsyncIterable using GeneratorFunction in the internal code, // there are many primordial considerations, so we simply implement the // toArray method. class WeakRefSet { #weakSet = new SafeWeakSet(); #refs = []; add(value) { if (WeakSetPrototypeHas(this.#weakSet, value)) { return; } WeakSetPrototypeAdd(this.#weakSet, value); ArrayPrototypePush(this.#refs, new SafeWeakRef(value)); } has(value) { return WeakSetPrototypeHas(this.#weakSet, value); } toArray() { const ret = []; for (let i = 0; i < this.#refs.length; ++i) { const value = WeakRefPrototypeDeref(this.#refs[i]); if (value !== undefined) { ArrayPrototypePush(ret, value); } } return ret; } } const add = Symbol("[[add]]"); const signalAbort = Symbol("[[signalAbort]]"); const remove = Symbol("[[remove]]"); const runAbortSteps = Symbol("[[runAbortSteps]]"); const abortReason = Symbol("[[abortReason]]"); const abortAlgos = Symbol("[[abortAlgos]]"); const dependent = Symbol("[[dependent]]"); const sourceSignals = Symbol("[[sourceSignals]]"); const dependentSignals = Symbol("[[dependentSignals]]"); const signal = Symbol("[[signal]]"); const timerId = Symbol("[[timerId]]"); const illegalConstructorKey = Symbol("illegalConstructorKey"); class AbortSignal extends EventTarget { [abortReason] = undefined; [abortAlgos] = null; [dependent] = false; [sourceSignals] = null; [dependentSignals] = null; [timerId] = null; [webidl.brand] = webidl.brand; static any(signals) { const prefix = "Failed to execute 'AbortSignal.any'"; webidl.requiredArguments(arguments.length, 1, prefix); return createDependentAbortSignal(signals, prefix); } static abort(reason = undefined) { if (reason !== undefined) { reason = webidl.converters.any(reason); } const signal = new AbortSignal(illegalConstructorKey); signal[signalAbort](reason); return signal; } static timeout(millis) { const prefix = "Failed to execute 'AbortSignal.timeout'"; webidl.requiredArguments(arguments.length, 1, prefix); millis = webidl.converters["unsigned long long"]( millis, prefix, "Argument 1", { enforceRange: true, }, ); const signal = new AbortSignal(illegalConstructorKey); signal[timerId] = core.queueSystemTimer( undefined, false, millis, () => { clearTimeout(signal[timerId]); signal[timerId] = null; signal[signalAbort]( new DOMException("Signal timed out.", "TimeoutError"), ); }, ); unrefTimer(signal[timerId]); return signal; } [add](algorithm) { if (this.aborted) { return; } this[abortAlgos] ??= new SafeSet(); SetPrototypeAdd(this[abortAlgos], algorithm); } [signalAbort]( reason = new DOMException("The signal has been aborted", "AbortError"), ) { if (this.aborted) { return; } this[abortReason] = reason; const dependentSignalsToAbort = []; if (this[dependentSignals] !== null) { const dependentSignalArray = this[dependentSignals].toArray(); for (let i = 0; i < dependentSignalArray.length; ++i) { const dependentSignal = dependentSignalArray[i]; if (dependentSignal[abortReason] === undefined) { dependentSignal[abortReason] = this[abortReason]; ArrayPrototypePush(dependentSignalsToAbort, dependentSignal); } } } this[runAbortSteps](); if (dependentSignalsToAbort.length !== 0) { for (let i = 0; i < dependentSignalsToAbort.length; ++i) { const dependentSignal = dependentSignalsToAbort[i]; dependentSignal[runAbortSteps](); } } } [runAbortSteps]() { const algos = this[abortAlgos]; this[abortAlgos] = null; if (algos !== null) { for (const algorithm of new SafeSetIterator(algos)) { algorithm(); } } if (listenerCount(this, "abort") > 0) { const event = new Event("abort"); setIsTrusted(event, true); super.dispatchEvent(event); } } [remove](algorithm) { this[abortAlgos] && SetPrototypeDelete(this[abortAlgos], algorithm); } constructor(key = null) { if (key !== illegalConstructorKey) { throw new TypeError("Illegal constructor."); } super(); } get aborted() { webidl.assertBranded(this, AbortSignalPrototype); return this[abortReason] !== undefined; } get reason() { webidl.assertBranded(this, AbortSignalPrototype); return this[abortReason]; } throwIfAborted() { webidl.assertBranded(this, AbortSignalPrototype); if (this[abortReason] !== undefined) { throw this[abortReason]; } } // `addEventListener` and `removeEventListener` have to be overridden in // order to have the timer block the event loop while there are listeners. // `[add]` and `[remove]` don't ref and unref the timer because they can // only be used by Deno internals, which use it to essentially cancel async // ops which would block the event loop. addEventListener() { FunctionPrototypeApply(super.addEventListener, this, arguments); if (listenerCount(this, "abort") > 0) { if (this[timerId] !== null) { refTimer(this[timerId]); } else if (this[sourceSignals] !== null) { const sourceSignalArray = this[sourceSignals].toArray(); for (let i = 0; i < sourceSignalArray.length; ++i) { const sourceSignal = sourceSignalArray[i]; if (sourceSignal[timerId] !== null) { refTimer(sourceSignal[timerId]); } } } } } removeEventListener() { FunctionPrototypeApply(super.removeEventListener, this, arguments); if (listenerCount(this, "abort") === 0) { if (this[timerId] !== null) { unrefTimer(this[timerId]); } else if (this[sourceSignals] !== null) { const sourceSignalArray = this[sourceSignals].toArray(); for (let i = 0; i < sourceSignalArray.length; ++i) { const sourceSignal = sourceSignalArray[i]; if (sourceSignal[timerId] !== null) { // Check that all dependent signals of the timer signal do not have listeners if ( ArrayPrototypeEvery( sourceSignal[dependentSignals].toArray(), (dependentSignal) => dependentSignal === this || listenerCount(dependentSignal, "abort") === 0, ) ) { unrefTimer(sourceSignal[timerId]); } } } } } } [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { return inspect( createFilteredInspectProxy({ object: this, evaluate: ObjectPrototypeIsPrototypeOf(AbortSignalPrototype, this), keys: [ "aborted", "reason", "onabort", ], }), inspectOptions, ); } } defineEventHandler(AbortSignal.prototype, "abort"); webidl.configureInterface(AbortSignal); const AbortSignalPrototype = AbortSignal.prototype; class AbortController { [signal] = new AbortSignal(illegalConstructorKey); constructor() { this[webidl.brand] = webidl.brand; } get signal() { webidl.assertBranded(this, AbortControllerPrototype); return this[signal]; } abort(reason) { webidl.assertBranded(this, AbortControllerPrototype); this[signal][signalAbort](reason); } [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { return inspect( createFilteredInspectProxy({ object: this, evaluate: ObjectPrototypeIsPrototypeOf(AbortControllerPrototype, this), keys: [ "signal", ], }), inspectOptions, ); } } webidl.configureInterface(AbortController); const AbortControllerPrototype = AbortController.prototype; webidl.converters.AbortSignal = webidl.createInterfaceConverter( "AbortSignal", AbortSignal.prototype, ); webidl.converters["sequence"] = webidl.createSequenceConverter( webidl.converters.AbortSignal, ); function newSignal() { return new AbortSignal(illegalConstructorKey); } function createDependentAbortSignal(signals, prefix) { signals = webidl.converters["sequence"]( signals, prefix, "Argument 1", ); const resultSignal = new AbortSignal(illegalConstructorKey); for (let i = 0; i < signals.length; ++i) { const signal = signals[i]; if (signal[abortReason] !== undefined) { resultSignal[abortReason] = signal[abortReason]; return resultSignal; } } resultSignal[dependent] = true; resultSignal[sourceSignals] = new WeakRefSet(); for (let i = 0; i < signals.length; ++i) { const signal = signals[i]; if (!signal[dependent]) { signal[dependentSignals] ??= new WeakRefSet(); resultSignal[sourceSignals].add(signal); signal[dependentSignals].add(resultSignal); } else { const sourceSignalArray = signal[sourceSignals].toArray(); for (let j = 0; j < sourceSignalArray.length; ++j) { const sourceSignal = sourceSignalArray[j]; assert(sourceSignal[abortReason] === undefined); assert(!sourceSignal[dependent]); if (resultSignal[sourceSignals].has(sourceSignal)) { continue; } resultSignal[sourceSignals].add(sourceSignal); sourceSignal[dependentSignals].add(resultSignal); } } } return resultSignal; } export { AbortController, AbortSignal, AbortSignalPrototype, add, createDependentAbortSignal, newSignal, remove, signalAbort, timerId, };