1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-21 21:50:00 -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.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; return target?.[eventTargetData]?.mode ?? null;
} }
function listenerCount(target, type) {
return getListeners(target)?.[type]?.length ?? 0;
}
function getDefaultTargetData() { function getDefaultTargetData() {
return { return {
assignedSlot: false, assignedSlot: false,
@ -1326,6 +1330,7 @@
window.__bootstrap.eventTarget = { window.__bootstrap.eventTarget = {
EventTarget, EventTarget,
setEventTargetData, setEventTargetData,
listenerCount,
}; };
window.__bootstrap.event = { window.__bootstrap.event = {
setIsTrusted, setIsTrusted,

View file

@ -7,6 +7,7 @@
((window) => { ((window) => {
const webidl = window.__bootstrap.webidl; const webidl = window.__bootstrap.webidl;
const { setIsTrusted, defineEventHandler } = window.__bootstrap.event; const { setIsTrusted, defineEventHandler } = window.__bootstrap.event;
const { listenerCount } = window.__bootstrap.eventTarget;
const { const {
Set, Set,
SetPrototypeAdd, SetPrototypeAdd,
@ -14,6 +15,7 @@
Symbol, Symbol,
TypeError, TypeError,
} = window.__bootstrap.primordials; } = window.__bootstrap.primordials;
const { setTimeout, refTimer, unrefTimer } = window.__bootstrap.timers;
const add = Symbol("[[add]]"); const add = Symbol("[[add]]");
const signalAbort = Symbol("[[signalAbort]]"); const signalAbort = Symbol("[[signalAbort]]");
@ -21,6 +23,7 @@
const abortReason = Symbol("[[abortReason]]"); const abortReason = Symbol("[[abortReason]]");
const abortAlgos = Symbol("[[abortAlgos]]"); const abortAlgos = Symbol("[[abortAlgos]]");
const signal = Symbol("[[signal]]"); const signal = Symbol("[[signal]]");
const timerId = Symbol("[[timerId]]");
const illegalConstructorKey = Symbol("illegalConstructorKey"); const illegalConstructorKey = Symbol("illegalConstructorKey");
@ -34,6 +37,27 @@
return signal; 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) { [add](algorithm) {
if (this.aborted) { if (this.aborted) {
return; return;
@ -73,6 +97,7 @@
super(); super();
this[abortReason] = undefined; this[abortReason] = undefined;
this[abortAlgos] = null; this[abortAlgos] = null;
this[timerId] = null;
this[webidl.brand] = webidl.brand; this[webidl.brand] = webidl.brand;
} }
@ -92,6 +117,25 @@
throw this[abortReason]; 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"); defineEventHandler(AbortSignal.prototype, "abort");

View file

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

View file

@ -961,16 +961,8 @@
}, },
"dom": { "dom": {
"abort": { "abort": {
"AbortSignal.any.html": [ "AbortSignal.any.html": true,
"AbortSignal.timeout() returns a non-aborted signal", "AbortSignal.any.worker.html": true,
"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"
],
"event.any.html": true, "event.any.html": true,
"event.any.worker.html": true "event.any.worker.html": true
}, },