diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index fd62a9486f..e675b6347e 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -1022,6 +1022,18 @@ declare namespace Deno { * Release an advisory file-system lock for the provided file. */ export function funlockSync(rid: number): void; + + /** **UNSTABLE**: new API, yet to be vetted. + * + * Make the timer of the given id blocking the event loop from finishing + */ + export function refTimer(id: number): void; + + /** **UNSTABLE**: new API, yet to be vetted. + * + * Make the timer of the given id not blocking the event loop from finishing + */ + export function unrefTimer(id: number): void; } declare function fetch( diff --git a/cli/tests/unit/timers_test.ts b/cli/tests/unit/timers_test.ts index fc6f9af4da..ec9b6f757e 100644 --- a/cli/tests/unit/timers_test.ts +++ b/cli/tests/unit/timers_test.ts @@ -9,6 +9,8 @@ import { unreachable, } from "./test_util.ts"; +const decoder = new TextDecoder(); + Deno.test(async function functionParameterBindingSuccess() { const promise = deferred(); let count = 0; @@ -573,3 +575,121 @@ Deno.test( await p; }, ); + +async function execCode(code: string) { + const p = Deno.run({ + cmd: [ + Deno.execPath(), + "eval", + "--unstable", + code, + ], + stdout: "piped", + }); + const [status, output] = await Promise.all([p.status(), p.output()]); + p.close(); + return [status.code, decoder.decode(output)]; +} + +Deno.test({ + name: "unrefTimer", + permissions: { run: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const timer = setTimeout(() => console.log("1")); + Deno.unrefTimer(timer); + `); + assertEquals(statusCode, 0); + assertEquals(output, ""); + }, +}); + +Deno.test({ + name: "unrefTimer - mix ref and unref 1", + permissions: { run: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const timer1 = setTimeout(() => console.log("1"), 200); + const timer2 = setTimeout(() => console.log("2"), 400); + const timer3 = setTimeout(() => console.log("3"), 600); + Deno.unrefTimer(timer3); + `); + assertEquals(statusCode, 0); + assertEquals(output, "1\n2\n"); + }, +}); + +Deno.test({ + name: "unrefTimer - mix ref and unref 2", + permissions: { run: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const timer1 = setTimeout(() => console.log("1"), 200); + const timer2 = setTimeout(() => console.log("2"), 400); + const timer3 = setTimeout(() => console.log("3"), 600); + Deno.unrefTimer(timer1); + Deno.unrefTimer(timer2); + `); + assertEquals(statusCode, 0); + assertEquals(output, "1\n2\n3\n"); + }, +}); + +Deno.test({ + name: "unrefTimer - unref interval", + permissions: { run: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + let i = 0; + const timer1 = setInterval(() => { + console.log("1"); + i++; + if (i === 5) { + Deno.unrefTimer(timer1); + } + }, 10); + `); + assertEquals(statusCode, 0); + assertEquals(output, "1\n1\n1\n1\n1\n"); + }, +}); + +Deno.test({ + name: "unrefTimer - unref then ref 1", + permissions: { run: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const timer1 = setTimeout(() => console.log("1"), 10); + Deno.unrefTimer(timer1); + Deno.refTimer(timer1); + `); + assertEquals(statusCode, 0); + assertEquals(output, "1\n"); + }, +}); + +Deno.test({ + name: "unrefTimer - unref then ref", + permissions: { run: true }, + fn: async () => { + const [statusCode, output] = await execCode(` + const timer1 = setTimeout(() => { + console.log("1"); + Deno.refTimer(timer2); + }, 10); + const timer2 = setTimeout(() => console.log("2"), 20); + Deno.unrefTimer(timer2); + `); + assertEquals(statusCode, 0); + assertEquals(output, "1\n2\n"); + }, +}); + +Deno.test({ + name: "unrefTimer - invalid calls do nothing", + permissions: { run: true }, + fn: () => { + Deno.unrefTimer(NaN); + Deno.refTimer(NaN); + }, +}); diff --git a/ext/timers/01_timers.js b/ext/timers/01_timers.js index ee3d85661f..6bed6ba4c9 100644 --- a/ext/timers/01_timers.js +++ b/ext/timers/01_timers.js @@ -16,6 +16,7 @@ // deno-lint-ignore camelcase NumberPOSITIVE_INFINITY, PromisePrototypeThen, + SymbolFor, TypeError, } = window.__bootstrap.primordials; const { webidl } = window.__bootstrap; @@ -87,7 +88,7 @@ * The keys in this map correspond to the key ID's in the spec's map of active * timers. The values are the timeout's cancel rid. * - * @type {Map} + * @type {Map} */ const activeTimers = new Map(); @@ -112,20 +113,21 @@ // previousId be an implementation-defined integer than is greater than zero // and does not already exist in global's map of active timers. let id; - let cancelRid; + let timerInfo; if (prevId !== undefined) { // `prevId` is only passed for follow-up calls on intervals assert(repeat); id = prevId; - cancelRid = MapPrototypeGet(activeTimers, id); + timerInfo = MapPrototypeGet(activeTimers, id); } else { // TODO(@andreubotella): Deal with overflow. // https://github.com/whatwg/html/issues/7358 id = nextId++; - cancelRid = core.opSync("op_timer_handle"); + const cancelRid = core.opSync("op_timer_handle"); + timerInfo = { cancelRid, isRef: true, promiseId: -1 }; // Step 4 in "run steps after a timeout". - MapPrototypeSet(activeTimers, id, cancelRid); + MapPrototypeSet(activeTimers, id, timerInfo); } // 3. If the surrounding agent's event loop's currently running task is a @@ -175,7 +177,7 @@ } } else { // 6. Otherwise, remove global's map of active timers[id]. - core.tryClose(cancelRid); + core.tryClose(timerInfo.cancelRid); MapPrototypeDelete(activeTimers, id); } }, @@ -192,7 +194,7 @@ runAfterTimeout( () => ArrayPrototypePush(timerTasks, task), timeout, - cancelRid, + timerInfo, ); return id; @@ -219,9 +221,17 @@ * @param {() => void} cb Will be run after the timeout, if it hasn't been * cancelled. * @param {number} millis - * @param {number} cancelRid + * @param {{ cancelRid: number, isRef: boolean, promiseId: number }} timerInfo */ - function runAfterTimeout(cb, millis, cancelRid) { + function runAfterTimeout(cb, millis, timerInfo) { + const cancelRid = timerInfo.cancelRid; + const sleepPromise = core.opAsync("op_sleep", millis, cancelRid); + timerInfo.promiseId = + sleepPromise[SymbolFor("Deno.core.internalPromiseId")]; + if (!timerInfo.isRef) { + core.unrefOp(timerInfo.promiseId); + } + /** @type {ScheduledTimer} */ const timerObject = { millis, @@ -242,7 +252,7 @@ // 1. PromisePrototypeThen( - core.opAsync("op_sleep", millis, cancelRid), + sleepPromise, () => { // 2. Wait until any invocations of this algorithm that had the same // global and orderingIdentifier, that started before this one, and @@ -334,9 +344,9 @@ function clearTimeout(id = 0) { checkThis(this); id = webidl.converters.long(id); - const cancelHandle = MapPrototypeGet(activeTimers, id); - if (cancelHandle !== undefined) { - core.tryClose(cancelHandle); + const timerInfo = MapPrototypeGet(activeTimers, id); + if (timerInfo !== undefined) { + core.tryClose(timerInfo.cancelRid); MapPrototypeDelete(activeTimers, id); } } @@ -346,6 +356,24 @@ clearTimeout(id); } + function refTimer(id) { + const timerInfo = MapPrototypeGet(activeTimers, id); + if (timerInfo === undefined || timerInfo.isRef) { + return; + } + timerInfo.isRef = true; + core.refOp(timerInfo.promiseId); + } + + function unrefTimer(id) { + const timerInfo = MapPrototypeGet(activeTimers, id); + if (timerInfo === undefined || !timerInfo.isRef) { + return; + } + timerInfo.isRef = false; + core.unrefOp(timerInfo.promiseId); + } + window.__bootstrap.timers = { setTimeout, setInterval, @@ -354,5 +382,7 @@ handleTimerMacrotask, opNow, sleepSync, + refTimer, + unrefTimer, }; })(this); diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index f858a93eb3..e5e52b1f6f 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -139,5 +139,7 @@ flockSync: __bootstrap.fs.flockSync, funlock: __bootstrap.fs.funlock, funlockSync: __bootstrap.fs.funlockSync, + refTimer: __bootstrap.timers.refTimer, + unrefTimer: __bootstrap.timers.unrefTimer, }; })(this);