feat(ext/web): add AbortSignal.any() (#21087)

Fixes #18944
This commit is contained in:
Kenta Moriuchi 2023-11-13 09:04:11 +09:00 committed by GitHub
parent 55e0483626
commit 39223f709b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 170 additions and 43 deletions

View file

@ -10,6 +10,7 @@
/// <reference lib="esnext" /> /// <reference lib="esnext" />
import * as webidl from "ext:deno_webidl/00_webidl.js"; 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 { createFilteredInspectProxy } from "ext:deno_console/01_console.js";
import { import {
byteUpperCase, byteUpperCase,
@ -356,21 +357,19 @@ class Request {
request.clientRid = init.client?.rid ?? null; request.clientRid = init.client?.rid ?? null;
} }
// 27. // 28.
this[_request] = request; this[_request] = request;
// 28.
this[_signal] = abortSignal.newSignal();
// 29. // 29.
if (signal !== null) { const signals = signal !== null ? [signal] : [];
abortSignal.follow(this[_signal], signal);
}
// 30. // 30.
this[_signal] = abortSignal.createDependentAbortSignal(signals, prefix);
// 31.
this[_headers] = headersFromHeaderList(request.headerList, "request"); this[_headers] = headersFromHeaderList(request.headerList, "request");
// 32. // 33.
if (init.headers || ObjectKeys(init).length > 0) { if (init.headers || ObjectKeys(init).length > 0) {
const headerList = headerListFromHeaders(this[_headers]); const headerList = headerListFromHeaders(this[_headers]);
const headers = init.headers ?? ArrayPrototypeSlice( const headers = init.headers ?? ArrayPrototypeSlice(
@ -384,13 +383,13 @@ class Request {
fillHeaders(this[_headers], headers); fillHeaders(this[_headers], headers);
} }
// 33. // 34.
let inputBody = null; let inputBody = null;
if (ObjectPrototypeIsPrototypeOf(RequestPrototype, input)) { if (ObjectPrototypeIsPrototypeOf(RequestPrototype, input)) {
inputBody = input[_body]; inputBody = input[_body];
} }
// 34. // 35.
if ( if (
(request.method === "GET" || request.method === "HEAD") && (request.method === "GET" || request.method === "HEAD") &&
((init.body !== undefined && init.body !== null) || ((init.body !== undefined && init.body !== null) ||
@ -399,10 +398,10 @@ class Request {
throw new TypeError("Request with GET/HEAD method cannot have body."); throw new TypeError("Request with GET/HEAD method cannot have body.");
} }
// 35. // 36.
let initBody = null; let initBody = null;
// 36. // 37.
if (init.body !== undefined && init.body !== null) { if (init.body !== undefined && init.body !== null) {
const res = extractBody(init.body); const res = extractBody(init.body);
initBody = res.body; initBody = res.body;
@ -411,13 +410,13 @@ class Request {
} }
} }
// 37. // 38.
const inputOrInitBody = initBody ?? inputBody; const inputOrInitBody = initBody ?? inputBody;
// 39. // 40.
let finalBody = inputOrInitBody; let finalBody = inputOrInitBody;
// 40. // 41.
if (initBody === null && inputBody !== null) { if (initBody === null && inputBody !== null) {
if (input[_body] && input[_body].unusable()) { if (input[_body] && input[_body].unusable()) {
throw new TypeError("Input request's body is unusable."); throw new TypeError("Input request's body is unusable.");
@ -425,7 +424,7 @@ class Request {
finalBody = inputBody.createProxy(); finalBody = inputBody.createProxy();
} }
// 41. // 42.
request.body = finalBody; request.body = finalBody;
} }
@ -464,20 +463,22 @@ class Request {
} }
clone() { clone() {
const prefix = "Failed to call 'Request.clone'";
webidl.assertBranded(this, RequestPrototype); webidl.assertBranded(this, RequestPrototype);
if (this[_body] && this[_body].unusable()) { if (this[_body] && this[_body].unusable()) {
throw new TypeError("Body is unusable."); throw new TypeError("Body is unusable.");
} }
const newReq = cloneInnerRequest(this[_request]); const clonedReq = cloneInnerRequest(this[_request]);
const newSignal = abortSignal.newSignal();
if (this[_signal]) { assert(this[_signal] !== null);
abortSignal.follow(newSignal, this[_signal]); const clonedSignal = abortSignal.createDependentAbortSignal(
} [this[_signal]],
prefix,
);
return fromInnerRequest( return fromInnerRequest(
newReq, clonedReq,
newSignal, clonedSignal,
guardFromHeaders(this[_headers]), guardFromHeaders(this[_headers]),
); );
} }

View file

@ -4,6 +4,7 @@
/// <reference path="../../core/internal.d.ts" /> /// <reference path="../../core/internal.d.ts" />
import * as webidl from "ext:deno_webidl/00_webidl.js"; import * as webidl from "ext:deno_webidl/00_webidl.js";
import { assert } from "ext:deno_web/00_infra.js";
import { import {
defineEventHandler, defineEventHandler,
Event, Event,
@ -13,27 +14,76 @@ import {
} from "ext:deno_web/02_event.js"; } from "ext:deno_web/02_event.js";
const primordials = globalThis.__bootstrap.primordials; const primordials = globalThis.__bootstrap.primordials;
const { const {
ArrayPrototypeEvery,
ArrayPrototypePush,
SafeArrayIterator, SafeArrayIterator,
SafeSet, SafeSet,
SafeSetIterator, SafeSetIterator,
SafeWeakRef,
SafeWeakSet,
SetPrototypeAdd, SetPrototypeAdd,
SetPrototypeDelete, SetPrototypeDelete,
Symbol, Symbol,
TypeError, TypeError,
WeakRefPrototypeDeref,
WeakSetPrototypeAdd,
WeakSetPrototypeHas,
} = primordials; } = primordials;
import { refTimer, setTimeout, unrefTimer } from "ext:deno_web/02_timers.js"; 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 add = Symbol("[[add]]");
const signalAbort = Symbol("[[signalAbort]]"); const signalAbort = Symbol("[[signalAbort]]");
const remove = Symbol("[[remove]]"); const remove = Symbol("[[remove]]");
const abortReason = Symbol("[[abortReason]]"); const abortReason = Symbol("[[abortReason]]");
const abortAlgos = Symbol("[[abortAlgos]]"); const abortAlgos = Symbol("[[abortAlgos]]");
const dependent = Symbol("[[dependent]]");
const sourceSignals = Symbol("[[sourceSignals]]");
const dependentSignals = Symbol("[[dependentSignals]]");
const signal = Symbol("[[signal]]"); const signal = Symbol("[[signal]]");
const timerId = Symbol("[[timerId]]"); const timerId = Symbol("[[timerId]]");
const illegalConstructorKey = Symbol("illegalConstructorKey"); const illegalConstructorKey = Symbol("illegalConstructorKey");
class AbortSignal extends EventTarget { 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) { static abort(reason = undefined) {
if (reason !== undefined) { if (reason !== undefined) {
reason = webidl.converters.any(reason); reason = webidl.converters.any(reason);
@ -73,9 +123,7 @@ class AbortSignal extends EventTarget {
if (this.aborted) { if (this.aborted) {
return; return;
} }
if (this[abortAlgos] === null) { this[abortAlgos] ??= new SafeSet();
this[abortAlgos] = new SafeSet();
}
SetPrototypeAdd(this[abortAlgos], algorithm); SetPrototypeAdd(this[abortAlgos], algorithm);
} }
@ -91,12 +139,20 @@ class AbortSignal extends EventTarget {
const event = new Event("abort"); const event = new Event("abort");
setIsTrusted(event, true); setIsTrusted(event, true);
this.dispatchEvent(event); super.dispatchEvent(event);
if (algos !== null) { if (algos !== null) {
for (const algorithm of new SafeSetIterator(algos)) { for (const algorithm of new SafeSetIterator(algos)) {
algorithm(); 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) { [remove](algorithm) {
@ -104,12 +160,15 @@ class AbortSignal extends EventTarget {
} }
constructor(key = null) { constructor(key = null) {
if (key != illegalConstructorKey) { if (key !== illegalConstructorKey) {
throw new TypeError("Illegal constructor."); throw new TypeError("Illegal constructor.");
} }
super(); super();
this[abortReason] = undefined; this[abortReason] = undefined;
this[abortAlgos] = null; this[abortAlgos] = null;
this[dependent] = false;
this[sourceSignals] = null;
this[dependentSignals] = null;
this[timerId] = null; this[timerId] = null;
this[webidl.brand] = webidl.brand; this[webidl.brand] = webidl.brand;
} }
@ -138,15 +197,45 @@ class AbortSignal extends EventTarget {
// ops which would block the event loop. // ops which would block the event loop.
addEventListener(...args) { addEventListener(...args) {
super.addEventListener(...new SafeArrayIterator(args)); super.addEventListener(...new SafeArrayIterator(args));
if (this[timerId] !== null && listenerCount(this, "abort") > 0) { if (listenerCount(this, "abort") > 0) {
refTimer(this[timerId]); 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) { removeEventListener(...args) {
super.removeEventListener(...new SafeArrayIterator(args)); super.removeEventListener(...new SafeArrayIterator(args));
if (this[timerId] !== null && listenerCount(this, "abort") === 0) { if (listenerCount(this, "abort") === 0) {
unrefTimer(this[timerId]); 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); webidl.configureInterface(AbortController);
const AbortControllerPrototype = AbortController.prototype; const AbortControllerPrototype = AbortController.prototype;
webidl.converters["AbortSignal"] = webidl.createInterfaceConverter( webidl.converters.AbortSignal = webidl.createInterfaceConverter(
"AbortSignal", "AbortSignal",
AbortSignal.prototype, AbortSignal.prototype,
); );
webidl.converters["sequence<AbortSignal>"] = webidl.createSequenceConverter(
webidl.converters.AbortSignal,
);
function newSignal() { function newSignal() {
return new AbortSignal(illegalConstructorKey); return new AbortSignal(illegalConstructorKey);
} }
function follow(followingSignal, parentSignal) { function createDependentAbortSignal(signals, prefix) {
if (followingSignal.aborted) { signals = webidl.converters["sequence<AbortSignal>"](
return; 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); resultSignal[dependent] = true;
} else { resultSignal[sourceSignals] = new WeakRefSet();
parentSignal[add](() => followingSignal[signalAbort](parentSignal.reason)); 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 { export {
@ -201,7 +325,7 @@ export {
AbortSignal, AbortSignal,
AbortSignalPrototype, AbortSignalPrototype,
add, add,
follow, createDependentAbortSignal,
newSignal, newSignal,
remove, remove,
signalAbort, signalAbort,

View file

@ -442,6 +442,7 @@ declare var AbortSignal: {
readonly prototype: AbortSignal; readonly prototype: AbortSignal;
new (): never; new (): never;
abort(reason?: any): AbortSignal; abort(reason?: any): AbortSignal;
any(signals: AbortSignal[]): AbortSignal;
timeout(milliseconds: number): AbortSignal; timeout(milliseconds: number): AbortSignal;
}; };

View file

@ -2302,7 +2302,9 @@
"AbortSignal.any.html": true, "AbortSignal.any.html": true,
"AbortSignal.any.worker.html": true, "AbortSignal.any.worker.html": true,
"event.any.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": { "events": {
"AddEventListenerOptions-once.any.html": true, "AddEventListenerOptions-once.any.html": true,
@ -2364,7 +2366,6 @@
"EventTarget interface: operation addEventListener(DOMString, EventListener?, optional (AddEventListenerOptions or boolean))", "EventTarget interface: operation addEventListener(DOMString, EventListener?, optional (AddEventListenerOptions or boolean))",
"EventTarget interface: operation removeEventListener(DOMString, EventListener?, optional (EventListenerOptions or boolean))", "EventTarget interface: operation removeEventListener(DOMString, EventListener?, optional (EventListenerOptions or boolean))",
"AbortController interface: operation abort(optional any)", "AbortController interface: operation abort(optional any)",
"AbortSignal interface: operation any(sequence<AbortSignal>)",
"AbortSignal interface: attribute onabort", "AbortSignal interface: attribute onabort",
"NodeList interface: existence and properties of interface object", "NodeList interface: existence and properties of interface object",
"NodeList interface object length", "NodeList interface object length",