summaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
authorKenta Moriuchi <moriken@kimamass.com>2023-11-13 09:04:11 +0900
committerGitHub <noreply@github.com>2023-11-13 01:04:11 +0100
commit39223f709bcb86069f3aa8eab7a4be80304128e6 (patch)
treef20331d487de0e776f483c9f67b8cc85eaddd88b /ext
parent55e04836261c577804bae4bbf7a49c53022880bd (diff)
feat(ext/web): add `AbortSignal.any()` (#21087)
Fixes #18944
Diffstat (limited to 'ext')
-rw-r--r--ext/fetch/23_request.js47
-rw-r--r--ext/web/03_abort_signal.js160
-rw-r--r--ext/web/lib.deno_web.d.ts1
3 files changed, 167 insertions, 41 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;
};