Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 14 commits into from
Nov 13, 2023
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);
petamoriken marked this conversation as resolved.
Show resolved Hide resolved
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is currently supposed to be an empty set on construction, see https://dom.spec.whatwg.org/#abortsignal-abort-algorithms. That should also cascade on a bunch of "null-check plus re-initialization" which may be redundant now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done for performance reasons in #12165, though not sure how ideal this is. I wouldn't worry about this though as it works as expected.

this[dependent] = false;
this[sourceSignals] = null;
this[dependentSignals] = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in the specs these two are directly constructed as empty sets.

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