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
151 changes: 141 additions & 10 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,115 @@ 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
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;
}
}

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;
}

static abort(reason = undefined) {
if (reason !== undefined) {
reason = webidl.converters.any(reason);
Expand Down Expand Up @@ -73,9 +162,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 +178,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 +236,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,10 +304,13 @@ 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);
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
5 changes: 3 additions & 2 deletions tools/wpt/expectation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand Down