fix(ext/node): New async setInterval function to improve the nodejs compatibility (#26703)

Closes #26499
This commit is contained in:
/usr/bin/cat 2024-11-16 20:31:19 +05:30 committed by GitHub
parent abf06eb87f
commit 8d2960d7cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 237 additions and 2 deletions

View File

@ -15,10 +15,16 @@ import {
setUnrefTimeout, setUnrefTimeout,
Timeout, Timeout,
} from "ext:deno_node/internal/timers.mjs"; } from "ext:deno_node/internal/timers.mjs";
import { validateFunction } from "ext:deno_node/internal/validators.mjs"; import {
validateAbortSignal,
validateBoolean,
validateFunction,
validateObject,
} from "ext:deno_node/internal/validators.mjs";
import { promisify } from "ext:deno_node/internal/util.mjs"; import { promisify } from "ext:deno_node/internal/util.mjs";
export { setUnrefTimeout } from "ext:deno_node/internal/timers.mjs"; export { setUnrefTimeout } from "ext:deno_node/internal/timers.mjs";
import * as timers from "ext:deno_web/02_timers.js"; import * as timers from "ext:deno_web/02_timers.js";
import { AbortError } from "ext:deno_node/internal/errors.ts";
const clearTimeout_ = timers.clearTimeout; const clearTimeout_ = timers.clearTimeout;
const clearInterval_ = timers.clearInterval; const clearInterval_ = timers.clearInterval;
@ -89,10 +95,88 @@ export function clearImmediate(immediate: Immediate) {
clearTimeout_(immediate._immediateId); clearTimeout_(immediate._immediateId);
} }
async function* setIntervalAsync(
after: number,
value: number,
options: { signal?: AbortSignal; ref?: boolean } = { __proto__: null },
) {
validateObject(options, "options");
if (typeof options?.signal !== "undefined") {
validateAbortSignal(options.signal, "options.signal");
}
if (typeof options?.ref !== "undefined") {
validateBoolean(options.ref, "options.ref");
}
const { signal, ref = true } = options;
if (signal?.aborted) {
throw new AbortError(undefined, { cause: signal?.reason });
}
let onCancel: (() => void) | undefined = undefined;
let interval: Timeout | undefined = undefined;
try {
let notYielded = 0;
let callback: ((value?: object) => void) | undefined = undefined;
let rejectCallback: ((message?: string) => void) | undefined = undefined;
interval = new Timeout(
() => {
notYielded++;
if (callback) {
callback();
callback = undefined;
rejectCallback = undefined;
}
},
after,
[],
true,
ref,
);
if (signal) {
onCancel = () => {
clearInterval(interval);
if (rejectCallback) {
rejectCallback(signal.reason);
callback = undefined;
rejectCallback = undefined;
}
};
signal.addEventListener("abort", onCancel, { once: true });
}
while (!signal?.aborted) {
if (notYielded === 0) {
await new Promise((resolve: () => void, reject: () => void) => {
callback = resolve;
rejectCallback = reject;
});
}
for (; notYielded > 0; notYielded--) {
yield value;
}
}
} catch (error) {
if (signal?.aborted) {
throw new AbortError(undefined, { cause: signal?.reason });
}
throw error;
} finally {
if (interval) {
clearInterval(interval);
}
if (onCancel) {
signal?.removeEventListener("abort", onCancel);
}
}
}
export const promises = { export const promises = {
setTimeout: promisify(setTimeout), setTimeout: promisify(setTimeout),
setImmediate: promisify(setImmediate), setImmediate: promisify(setImmediate),
setInterval: promisify(setInterval), setInterval: setIntervalAsync,
}; };
promises.scheduler = { promises.scheduler = {

View File

@ -3,6 +3,7 @@
import { assert, fail } from "@std/assert"; import { assert, fail } from "@std/assert";
import * as timers from "node:timers"; import * as timers from "node:timers";
import * as timersPromises from "node:timers/promises"; import * as timersPromises from "node:timers/promises";
import { assertEquals } from "@std/assert";
Deno.test("[node/timers setTimeout]", () => { Deno.test("[node/timers setTimeout]", () => {
{ {
@ -108,3 +109,153 @@ Deno.test("[node/timers setImmediate returns Immediate object]", () => {
imm.hasRef(); imm.hasRef();
clearImmediate(imm); clearImmediate(imm);
}); });
Deno.test({
name: "setInterval yields correct values at expected intervals",
async fn() {
// Test configuration
const CONFIG = {
expectedValue: 42,
intervalMs: 100,
iterations: 3,
tolerancePercent: 10,
};
const { setInterval } = timersPromises;
const results: Array<{ value: number; timestamp: number }> = [];
const startTime = Date.now();
const iterator = setInterval(CONFIG.intervalMs, CONFIG.expectedValue);
for await (const value of iterator) {
results.push({
value,
timestamp: Date.now(),
});
if (results.length === CONFIG.iterations) {
break;
}
}
const values = results.map((r) => r.value);
assertEquals(
values,
Array(CONFIG.iterations).fill(CONFIG.expectedValue),
`Each iteration should yield ${CONFIG.expectedValue}`,
);
const intervals = results.slice(1).map((result, index) => ({
interval: result.timestamp - results[index].timestamp,
iterationNumber: index + 1,
}));
const toleranceMs = (CONFIG.tolerancePercent / 100) * CONFIG.intervalMs;
const expectedRange = {
min: CONFIG.intervalMs - toleranceMs,
max: CONFIG.intervalMs + toleranceMs,
};
intervals.forEach(({ interval, iterationNumber }) => {
const isWithinTolerance = interval >= expectedRange.min &&
interval <= expectedRange.max;
assertEquals(
isWithinTolerance,
true,
`Iteration ${iterationNumber}: Interval ${interval}ms should be within ` +
`${expectedRange.min}ms and ${expectedRange.max}ms ` +
`(${CONFIG.tolerancePercent}% tolerance of ${CONFIG.intervalMs}ms)`,
);
});
const totalDuration = results[results.length - 1].timestamp - startTime;
const expectedDuration = CONFIG.intervalMs * CONFIG.iterations;
const isDurationReasonable =
totalDuration >= (expectedDuration - toleranceMs) &&
totalDuration <= (expectedDuration + toleranceMs);
assertEquals(
isDurationReasonable,
true,
`Total duration ${totalDuration}ms should be close to ${expectedDuration}ms ` +
`(within ${toleranceMs}ms tolerance)`,
);
const timestamps = results.map((r) => r.timestamp);
const areTimestampsOrdered = timestamps.every((timestamp, i) =>
i === 0 || timestamp > timestamps[i - 1]
);
assertEquals(
areTimestampsOrdered,
true,
"Timestamps should be strictly increasing",
);
},
});
Deno.test({
name: "setInterval with AbortSignal stops after expected duration",
async fn() {
const INTERVAL_MS = 500;
const TOTAL_DURATION_MS = 3000;
const TOLERANCE_MS = 500;
const abortController = new AbortController();
const { setInterval } = timersPromises;
// Set up abort after specified duration
const abortTimeout = timers.setTimeout(() => {
abortController.abort();
}, TOTAL_DURATION_MS);
// Track iterations and timing
const startTime = Date.now();
const iterations: number[] = [];
try {
for await (
const _timestamp of setInterval(INTERVAL_MS, undefined, {
signal: abortController.signal,
})
) {
iterations.push(Date.now() - startTime);
}
} catch (error) {
if (error instanceof Error && error.name !== "AbortError") {
throw error;
}
} finally {
timers.clearTimeout(abortTimeout);
}
// Validate timing
const totalDuration = iterations[iterations.length - 1];
const isWithinTolerance =
totalDuration >= (TOTAL_DURATION_MS - TOLERANCE_MS) &&
totalDuration <= (TOTAL_DURATION_MS + TOLERANCE_MS);
assertEquals(
isWithinTolerance,
true,
`Total duration ${totalDuration}ms should be within ±${TOLERANCE_MS}ms of ${TOTAL_DURATION_MS}ms`,
);
// Validate interval consistency
const intervalDeltas = iterations.slice(1).map((time, i) =>
time - iterations[i]
);
intervalDeltas.forEach((delta, i) => {
const isIntervalValid = delta >= (INTERVAL_MS - 50) &&
delta <= (INTERVAL_MS + 50);
assertEquals(
isIntervalValid,
true,
`Interval ${
i + 1
} duration (${delta}ms) should be within ±50ms of ${INTERVAL_MS}ms`,
);
});
},
});