diff options
author | Kenta Moriuchi <moriken@kimamass.com> | 2023-11-13 09:04:11 +0900 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-13 01:04:11 +0100 |
commit | 39223f709bcb86069f3aa8eab7a4be80304128e6 (patch) | |
tree | f20331d487de0e776f483c9f67b8cc85eaddd88b | |
parent | 55e04836261c577804bae4bbf7a49c53022880bd (diff) |
feat(ext/web): add `AbortSignal.any()` (#21087)
Fixes #18944
-rw-r--r-- | ext/fetch/23_request.js | 47 | ||||
-rw-r--r-- | ext/web/03_abort_signal.js | 160 | ||||
-rw-r--r-- | ext/web/lib.deno_web.d.ts | 1 | ||||
-rw-r--r-- | tools/wpt/expectation.json | 5 |
4 files changed, 170 insertions, 43 deletions
diff --git a/ext/fetch/23_request.js b/ext/fetch/23_request.js index a59bfb29d..bbaf886a6 100644 --- a/ext/fetch/23_request.js +++ b/ext/fetch/23_request.js @@ -10,6 +10,7 @@ /// <reference lib="esnext" /> import * as webidl from "ext:deno_webidl/00_webidl.js"; +import { assert } from "ext:deno_web/00_infra.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; import { byteUpperCase, @@ -356,21 +357,19 @@ class Request { request.clientRid = init.client?.rid ?? null; } - // 27. - this[_request] = request; - // 28. - this[_signal] = abortSignal.newSignal(); + this[_request] = request; // 29. - if (signal !== null) { - abortSignal.follow(this[_signal], signal); - } + const signals = signal !== null ? [signal] : []; // 30. + this[_signal] = abortSignal.createDependentAbortSignal(signals, prefix); + + // 31. this[_headers] = headersFromHeaderList(request.headerList, "request"); - // 32. + // 33. if (init.headers || ObjectKeys(init).length > 0) { const headerList = headerListFromHeaders(this[_headers]); const headers = init.headers ?? ArrayPrototypeSlice( @@ -384,13 +383,13 @@ class Request { fillHeaders(this[_headers], headers); } - // 33. + // 34. let inputBody = null; if (ObjectPrototypeIsPrototypeOf(RequestPrototype, input)) { inputBody = input[_body]; } - // 34. + // 35. if ( (request.method === "GET" || request.method === "HEAD") && ((init.body !== undefined && init.body !== null) || @@ -399,10 +398,10 @@ class Request { throw new TypeError("Request with GET/HEAD method cannot have body."); } - // 35. + // 36. let initBody = null; - // 36. + // 37. if (init.body !== undefined && init.body !== null) { const res = extractBody(init.body); initBody = res.body; @@ -411,13 +410,13 @@ class Request { } } - // 37. + // 38. const inputOrInitBody = initBody ?? inputBody; - // 39. + // 40. let finalBody = inputOrInitBody; - // 40. + // 41. if (initBody === null && inputBody !== null) { if (input[_body] && input[_body].unusable()) { throw new TypeError("Input request's body is unusable."); @@ -425,7 +424,7 @@ class Request { finalBody = inputBody.createProxy(); } - // 41. + // 42. request.body = finalBody; } @@ -464,20 +463,22 @@ class Request { } clone() { + const prefix = "Failed to call 'Request.clone'"; webidl.assertBranded(this, RequestPrototype); if (this[_body] && this[_body].unusable()) { throw new TypeError("Body is unusable."); } - const newReq = cloneInnerRequest(this[_request]); - const newSignal = abortSignal.newSignal(); + const clonedReq = cloneInnerRequest(this[_request]); - if (this[_signal]) { - abortSignal.follow(newSignal, this[_signal]); - } + assert(this[_signal] !== null); + const clonedSignal = abortSignal.createDependentAbortSignal( + [this[_signal]], + prefix, + ); return fromInnerRequest( - newReq, - newSignal, + clonedReq, + clonedSignal, guardFromHeaders(this[_headers]), ); } diff --git a/ext/web/03_abort_signal.js b/ext/web/03_abort_signal.js index 9b5eb51ad..a237b273c 100644 --- a/ext/web/03_abort_signal.js +++ b/ext/web/03_abort_signal.js @@ -4,6 +4,7 @@ /// <reference path="../../core/internal.d.ts" /> import * as webidl from "ext:deno_webidl/00_webidl.js"; +import { assert } from "ext:deno_web/00_infra.js"; import { defineEventHandler, Event, @@ -13,27 +14,76 @@ import { } from "ext:deno_web/02_event.js"; const primordials = globalThis.__bootstrap.primordials; const { + ArrayPrototypeEvery, + ArrayPrototypePush, SafeArrayIterator, SafeSet, SafeSetIterator, + SafeWeakRef, + SafeWeakSet, SetPrototypeAdd, SetPrototypeDelete, Symbol, TypeError, + WeakRefPrototypeDeref, + WeakSetPrototypeAdd, + WeakSetPrototypeHas, } = primordials; import { refTimer, setTimeout, unrefTimer } from "ext:deno_web/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 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 { + static any(signals) { + const prefix = "Failed to call 'AbortSignal.any'"; + webidl.requiredArguments(arguments.length, 1, prefix); + return createDependentAbortSignal(signals, prefix); + } + static abort(reason = undefined) { if (reason !== undefined) { reason = webidl.converters.any(reason); @@ -73,9 +123,7 @@ class AbortSignal extends EventTarget { if (this.aborted) { return; } - if (this[abortAlgos] === null) { - this[abortAlgos] = new SafeSet(); - } + this[abortAlgos] ??= new SafeSet(); SetPrototypeAdd(this[abortAlgos], algorithm); } @@ -91,12 +139,20 @@ class AbortSignal extends EventTarget { const event = new Event("abort"); setIsTrusted(event, true); - this.dispatchEvent(event); + super.dispatchEvent(event); if (algos !== null) { for (const algorithm of new SafeSetIterator(algos)) { algorithm(); } } + + if (this[dependentSignals] !== null) { + const dependentSignalArray = this[dependentSignals].toArray(); + for (let i = 0; i < dependentSignalArray.length; ++i) { + const dependentSignal = dependentSignalArray[i]; + dependentSignal[signalAbort](reason); + } + } } [remove](algorithm) { @@ -104,12 +160,15 @@ class AbortSignal extends EventTarget { } constructor(key = null) { - if (key != illegalConstructorKey) { + if (key !== illegalConstructorKey) { throw new TypeError("Illegal constructor."); } super(); this[abortReason] = undefined; this[abortAlgos] = null; + this[dependent] = false; + this[sourceSignals] = null; + this[dependentSignals] = null; this[timerId] = null; this[webidl.brand] = webidl.brand; } @@ -138,15 +197,45 @@ class AbortSignal extends EventTarget { // ops which would block the event loop. addEventListener(...args) { super.addEventListener(...new SafeArrayIterator(args)); - if (this[timerId] !== null && listenerCount(this, "abort") > 0) { - refTimer(this[timerId]); + 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(...args) { super.removeEventListener(...new SafeArrayIterator(args)); - if (this[timerId] !== null && listenerCount(this, "abort") === 0) { - unrefTimer(this[timerId]); + 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]); + } + } + } + } } } } @@ -176,24 +265,59 @@ class AbortController { webidl.configureInterface(AbortController); const AbortControllerPrototype = AbortController.prototype; -webidl.converters["AbortSignal"] = webidl.createInterfaceConverter( +webidl.converters.AbortSignal = webidl.createInterfaceConverter( "AbortSignal", AbortSignal.prototype, ); +webidl.converters["sequence<AbortSignal>"] = webidl.createSequenceConverter( + webidl.converters.AbortSignal, +); function newSignal() { return new AbortSignal(illegalConstructorKey); } -function follow(followingSignal, parentSignal) { - if (followingSignal.aborted) { - return; +function createDependentAbortSignal(signals, prefix) { + signals = webidl.converters["sequence<AbortSignal>"]( + 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; + } } - if (parentSignal.aborted) { - followingSignal[signalAbort](parentSignal.reason); - } else { - parentSignal[add](() => followingSignal[signalAbort](parentSignal.reason)); + + 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 { @@ -201,7 +325,7 @@ export { AbortSignal, AbortSignalPrototype, add, - follow, + createDependentAbortSignal, newSignal, remove, signalAbort, diff --git a/ext/web/lib.deno_web.d.ts b/ext/web/lib.deno_web.d.ts index 5a3f658a3..aa832cc01 100644 --- a/ext/web/lib.deno_web.d.ts +++ b/ext/web/lib.deno_web.d.ts @@ -442,6 +442,7 @@ declare var AbortSignal: { readonly prototype: AbortSignal; new (): never; abort(reason?: any): AbortSignal; + any(signals: AbortSignal[]): AbortSignal; timeout(milliseconds: number): AbortSignal; }; diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json index 4b01f0c00..fc5870f62 100644 --- a/tools/wpt/expectation.json +++ b/tools/wpt/expectation.json @@ -2302,7 +2302,9 @@ "AbortSignal.any.html": true, "AbortSignal.any.worker.html": true, "event.any.html": true, - "event.any.worker.html": true + "event.any.worker.html": true, + "abort-signal-any.any.html": true, + "abort-signal-any.any.worker.html": true }, "events": { "AddEventListenerOptions-once.any.html": true, @@ -2364,7 +2366,6 @@ "EventTarget interface: operation addEventListener(DOMString, EventListener?, optional (AddEventListenerOptions or boolean))", "EventTarget interface: operation removeEventListener(DOMString, EventListener?, optional (EventListenerOptions or boolean))", "AbortController interface: operation abort(optional any)", - "AbortSignal interface: operation any(sequence<AbortSignal>)", "AbortSignal interface: attribute onabort", "NodeList interface: existence and properties of interface object", "NodeList interface object length", |