summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/tests/unit/timers_test.ts74
-rw-r--r--ext/web/02_event.js5
-rw-r--r--ext/web/03_abort_signal.js44
-rw-r--r--ext/web/lib.deno_web.d.ts1
-rw-r--r--tools/wpt/expectation.json12
5 files changed, 126 insertions, 10 deletions
diff --git a/cli/tests/unit/timers_test.ts b/cli/tests/unit/timers_test.ts
index ac0403bf7..5b9e1fa48 100644
--- a/cli/tests/unit/timers_test.ts
+++ b/cli/tests/unit/timers_test.ts
@@ -679,3 +679,77 @@ Deno.test({
Deno.refTimer(NaN);
},
});
+
+Deno.test({
+ name: "AbortSignal.timeout() with no listeners",
+ permissions: { run: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const signal = AbortSignal.timeout(2000);
+
+ // This unref timer expires before the signal, and if it does expire, then
+ // it means the signal has kept the event loop alive.
+ const timer = setTimeout(() => console.log("Unexpected!"), 1500);
+ Deno.unrefTimer(timer);
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "");
+ },
+});
+
+Deno.test({
+ name: "AbortSignal.timeout() with listeners",
+ permissions: { run: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const signal = AbortSignal.timeout(1000);
+ signal.addEventListener("abort", () => console.log("Event fired!"));
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "Event fired!\n");
+ },
+});
+
+Deno.test({
+ name: "AbortSignal.timeout() with removed listeners",
+ permissions: { run: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const signal = AbortSignal.timeout(2000);
+
+ const callback = () => console.log("Unexpected: Event fired");
+ signal.addEventListener("abort", callback);
+
+ setTimeout(() => {
+ console.log("Removing the listener.");
+ signal.removeEventListener("abort", callback);
+ }, 500);
+
+ Deno.unrefTimer(
+ setTimeout(() => console.log("Unexpected: Unref timer"), 1500)
+ );
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "Removing the listener.\n");
+ },
+});
+
+Deno.test({
+ name: "AbortSignal.timeout() with listener for a non-abort event",
+ permissions: { run: true },
+ fn: async () => {
+ const [statusCode, output] = await execCode(`
+ const signal = AbortSignal.timeout(2000);
+
+ signal.addEventListener("someOtherEvent", () => {
+ console.log("Unexpected: someOtherEvent called");
+ });
+
+ Deno.unrefTimer(
+ setTimeout(() => console.log("Unexpected: Unref timer"), 1500)
+ );
+ `);
+ assertEquals(statusCode, 0);
+ assertEquals(output, "");
+ },
+});
diff --git a/ext/web/02_event.js b/ext/web/02_event.js
index e85b4f5cb..122add211 100644
--- a/ext/web/02_event.js
+++ b/ext/web/02_event.js
@@ -868,6 +868,10 @@
return target?.[eventTargetData]?.mode ?? null;
}
+ function listenerCount(target, type) {
+ return getListeners(target)?.[type]?.length ?? 0;
+ }
+
function getDefaultTargetData() {
return {
assignedSlot: false,
@@ -1326,6 +1330,7 @@
window.__bootstrap.eventTarget = {
EventTarget,
setEventTargetData,
+ listenerCount,
};
window.__bootstrap.event = {
setIsTrusted,
diff --git a/ext/web/03_abort_signal.js b/ext/web/03_abort_signal.js
index 8b089d031..39de8d0fc 100644
--- a/ext/web/03_abort_signal.js
+++ b/ext/web/03_abort_signal.js
@@ -7,6 +7,7 @@
((window) => {
const webidl = window.__bootstrap.webidl;
const { setIsTrusted, defineEventHandler } = window.__bootstrap.event;
+ const { listenerCount } = window.__bootstrap.eventTarget;
const {
Set,
SetPrototypeAdd,
@@ -14,6 +15,7 @@
Symbol,
TypeError,
} = window.__bootstrap.primordials;
+ const { setTimeout, refTimer, unrefTimer } = window.__bootstrap.timers;
const add = Symbol("[[add]]");
const signalAbort = Symbol("[[signalAbort]]");
@@ -21,6 +23,7 @@
const abortReason = Symbol("[[abortReason]]");
const abortAlgos = Symbol("[[abortAlgos]]");
const signal = Symbol("[[signal]]");
+ const timerId = Symbol("[[timerId]]");
const illegalConstructorKey = Symbol("illegalConstructorKey");
@@ -34,6 +37,27 @@
return signal;
}
+ static timeout(millis) {
+ const prefix = "Failed to call 'AbortSignal.timeout'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ millis = webidl.converters["unsigned long long"](millis, {
+ enforceRange: true,
+ });
+
+ const signal = new AbortSignal(illegalConstructorKey);
+ signal[timerId] = setTimeout(
+ () => {
+ signal[timerId] = null;
+ signal[signalAbort](
+ new DOMException("Signal timed out.", "TimeoutError"),
+ );
+ },
+ millis,
+ );
+ unrefTimer(signal[timerId]);
+ return signal;
+ }
+
[add](algorithm) {
if (this.aborted) {
return;
@@ -73,6 +97,7 @@
super();
this[abortReason] = undefined;
this[abortAlgos] = null;
+ this[timerId] = null;
this[webidl.brand] = webidl.brand;
}
@@ -92,6 +117,25 @@
throw this[abortReason];
}
}
+
+ // `addEventListener` and `removeEventListener` have to be overriden in
+ // order to have the timer block the event loop while there are listeners.
+ // `[add]` and `[remove]` don't ref and unref the timer because they can
+ // only be used by Deno internals, which use it to essentially cancel async
+ // ops which would block the event loop.
+ addEventListener(...args) {
+ super.addEventListener(...args);
+ if (this[timerId] !== null && listenerCount(this, "abort") > 0) {
+ refTimer(this[timerId]);
+ }
+ }
+
+ removeEventListener(...args) {
+ super.removeEventListener(...args);
+ if (this[timerId] !== null && listenerCount(this, "abort") === 0) {
+ unrefTimer(this[timerId]);
+ }
+ }
}
defineEventHandler(AbortSignal.prototype, "abort");
diff --git a/ext/web/lib.deno_web.d.ts b/ext/web/lib.deno_web.d.ts
index e8f7f26cd..77c502fac 100644
--- a/ext/web/lib.deno_web.d.ts
+++ b/ext/web/lib.deno_web.d.ts
@@ -307,6 +307,7 @@ declare var AbortSignal: {
prototype: AbortSignal;
new (): AbortSignal;
abort(reason?: any): AbortSignal;
+ timeout(milliseconds: number): AbortSignal;
};
interface FileReaderEventMap {
diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json
index 60f41ef97..43367efe3 100644
--- a/tools/wpt/expectation.json
+++ b/tools/wpt/expectation.json
@@ -961,16 +961,8 @@
},
"dom": {
"abort": {
- "AbortSignal.any.html": [
- "AbortSignal.timeout() returns a non-aborted signal",
- "Signal returned by AbortSignal.timeout() times out",
- "AbortSignal timeouts fire in order"
- ],
- "AbortSignal.any.worker.html": [
- "AbortSignal.timeout() returns a non-aborted signal",
- "Signal returned by AbortSignal.timeout() times out",
- "AbortSignal timeouts fire in order"
- ],
+ "AbortSignal.any.html": true,
+ "AbortSignal.any.worker.html": true,
"event.any.html": true,
"event.any.worker.html": true
},