1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

feat(ext/web): Add AbortSignal.timeout() (#13687)

This commit is contained in:
Andreu Botella 2022-03-14 20:19:22 +01:00 committed by GitHub
parent 5eb0e4c2df
commit 9f494dc405
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 126 additions and 10 deletions

View file

@ -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, "");
},
});

View file

@ -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,

View file

@ -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");

View file

@ -307,6 +307,7 @@ declare var AbortSignal: {
prototype: AbortSignal;
new (): AbortSignal;
abort(reason?: any): AbortSignal;
timeout(milliseconds: number): AbortSignal;
};
interface FileReaderEventMap {

View file

@ -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
},