Skip to content

Commit

Permalink
feat(ext/web): add AbortSignal.any() (#21087)
Browse files Browse the repository at this point in the history
Fixes #18944
  • Loading branch information
petamoriken committed Nov 13, 2023
1 parent 55e0483 commit 39223f7
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 43 deletions.
47 changes: 24 additions & 23 deletions ext/fetch/23_request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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) ||
Expand All @@ -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;
Expand All @@ -411,21 +410,21 @@ 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.");
}
finalBody = inputBody.createProxy();
}

// 41.
// 42.
request.body = finalBody;
}

Expand Down Expand Up @@ -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]),
);
}
Expand Down
160 changes: 142 additions & 18 deletions ext/web/03_abort_signal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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);
}

Expand All @@ -91,25 +139,36 @@ 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) {
this[abortAlgos] && SetPrototypeDelete(this[abortAlgos], algorithm);
}

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;
}
Expand Down Expand Up @@ -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]);
}
}
}
}
}
}
}
Expand Down Expand Up @@ -176,32 +265,67 @@ 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 {
AbortController,
AbortSignal,
AbortSignalPrototype,
add,
follow,
createDependentAbortSignal,
newSignal,
remove,
signalAbort,
Expand Down
1 change: 1 addition & 0 deletions ext/web/lib.deno_web.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ declare var AbortSignal: {
readonly prototype: AbortSignal;
new (): never;
abort(reason?: any): AbortSignal;
any(signals: AbortSignal[]): AbortSignal;
timeout(milliseconds: number): AbortSignal;
};

Expand Down
Loading

0 comments on commit 39223f7

Please sign in to comment.