Skip to content
File

Blob: src/workerd/api/node/tests/timers-nodejs-test.js

javascript523 lines
1// Copyright (c) 2017-2025 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Adapted from Node.js. Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26import timers from 'node:timers';
27import timersPromises from 'node:timers/promises';
28import { throws, strictEqual, deepStrictEqual, rejects, ok } from 'node:assert';
29 
30export const testEnroll = {
31 async test() {
32 throws(() => timers.enroll(), /Not implemented/);
33 },
34};
35 
36// Tests are taken from:
37// https://github.com/nodejs/node/blob/b3641fe85d55525127c03be730596154705b798e/test/parallel/test-timers-immediate.js
38export const testSetImmediate = {
39 async test() {
40 {
41 const { promise, resolve, reject } = Promise.withResolvers();
42 let mainFinished = false;
43 timers.setImmediate(() => {
44 strictEqual(mainFinished, true);
45 timers.clearImmediate(immediateB);
46 resolve();
47 });
48 
49 const immediateB = timers.setImmediate(reject);
50 mainFinished = true;
51 
52 await promise;
53 }
54 
55 {
56 const { promise, resolve } = Promise.withResolvers();
57 timers.setImmediate(
58 (...args) => {
59 deepStrictEqual(args, [1, 2, 3]);
60 resolve();
61 },
62 1,
63 2,
64 3
65 );
66 await promise;
67 }
68 },
69};
70 
71export const testSetTimeout = {
72 async test() {
73 {
74 const { promise, resolve, reject } = Promise.withResolvers();
75 let mainFinished = false;
76 timers.setTimeout(() => {
77 strictEqual(mainFinished, true);
78 timers.clearTimeout(timeoutB);
79 resolve();
80 });
81 
82 const timeoutB = timers.setTimeout(reject);
83 mainFinished = true;
84 
85 await promise;
86 }
87 
88 {
89 const { promise, resolve } = Promise.withResolvers();
90 timers.setTimeout(
91 (...args) => {
92 deepStrictEqual(args, [1, 2, 3]);
93 resolve();
94 },
95 100,
96 1,
97 2,
98 3
99 );
100 await promise;
101 }
102 },
103};
104 
105export const testSetInterval = {
106 async test() {
107 {
108 const { promise, resolve, reject } = Promise.withResolvers();
109 let mainFinished = false;
110 const thisInterval = timers.setInterval(() => {
111 strictEqual(mainFinished, true);
112 timers.clearInterval(intervalB);
113 timers.clearInterval(thisInterval);
114 resolve();
115 }, 100);
116 
117 const intervalB = timers.setInterval(reject, 100);
118 mainFinished = true;
119 
120 await promise;
121 }
122 
123 {
124 const { promise, resolve } = Promise.withResolvers();
125 const thisInterval = timers.setInterval(
126 (...args) => {
127 deepStrictEqual(args, [1, 2, 3]);
128 timers.clearInterval(thisInterval);
129 resolve();
130 },
131 100,
132 1,
133 2,
134 3
135 );
136 await promise;
137 }
138 },
139};
140 
141export const testRefresh = {
142 async test() {
143 const { promise, resolve, reject } = Promise.withResolvers();
144 timers.clearTimeout(
145 timers
146 .setTimeout(() => {
147 reject();
148 }, 1)
149 .refresh()
150 );
151 
152 timers.setTimeout(() => {
153 resolve();
154 }, 2);
155 
156 await promise;
157 },
158};
159 
160// Tests are taken from
161// https://github.com/nodejs/node/blob/7bc2946293757389468f1fa09714860f8e1147b7/test/parallel/test-timers-immediate-promisified.js
162export const timersImmediatePromise = {
163 async test() {
164 {
165 const ac = new AbortController();
166 const signal = ac.signal;
167 const { promise, resolve } = Promise.withResolvers();
168 rejects(timersPromises.setImmediate(10, { signal }), /AbortError/).then(
169 resolve
170 );
171 ac.abort();
172 await promise;
173 }
174 
175 {
176 const signal = AbortSignal.abort(); // Abort in advance
177 await rejects(timersPromises.setImmediate(10, { signal }), /AbortError/);
178 }
179 
180 {
181 // Check that aborting after resolve will not reject.
182 const ac = new AbortController();
183 const signal = ac.signal;
184 await timersPromises.setImmediate(10, { signal }).then(() => {
185 ac.abort();
186 });
187 }
188 
189 {
190 await Promise.all(
191 [1, '', false, Infinity].map((i) =>
192 rejects(timersPromises.setImmediate(10, i), {
193 code: 'ERR_INVALID_ARG_TYPE',
194 })
195 )
196 );
197 
198 await Promise.all(
199 [1, '', false, Infinity, null, {}].map((signal) =>
200 rejects(timersPromises.setImmediate(10, { signal }), {
201 code: 'ERR_INVALID_ARG_TYPE',
202 })
203 )
204 );
205 
206 await Promise.all(
207 [1, '', Infinity, null, {}].map((ref) =>
208 rejects(timersPromises.setImmediate(10, { ref }), {
209 code: 'ERR_INVALID_ARG_TYPE',
210 })
211 )
212 );
213 }
214 
215 {
216 const signal = AbortSignal.abort('boom');
217 await rejects(timersPromises.setImmediate(undefined, { signal }), {
218 cause: 'boom',
219 });
220 }
221 },
222};
223 
224// Tests are taken from
225// https://github.com/nodejs/node/blob/7bc2946293757389468f1fa09714860f8e1147b7/test/parallel/test-timers-timeout-promisified.js
226export const timersTimeoutPromise = {
227 async test() {
228 {
229 const promise = timersPromises.setTimeout(1);
230 strictEqual(await promise, undefined);
231 }
232 
233 {
234 const promise = timersPromises.setTimeout(1, 'foobar');
235 strictEqual(await promise, 'foobar');
236 }
237 
238 {
239 const ac = new AbortController();
240 const signal = ac.signal;
241 const { promise, resolve } = Promise.withResolvers();
242 rejects(
243 timersPromises.setTimeout(10, undefined, { signal }),
244 /AbortError/
245 ).then(resolve);
246 ac.abort();
247 await promise;
248 }
249 
250 {
251 const signal = AbortSignal.abort(); // Abort in advance
252 await rejects(
253 timersPromises.setTimeout(10, undefined, { signal }),
254 /AbortError/
255 );
256 }
257 
258 {
259 // Check that aborting after resolve will not reject.
260 const ac = new AbortController();
261 const signal = ac.signal;
262 await timersPromises.setTimeout(10, undefined, { signal }).then(() => {
263 ac.abort();
264 });
265 }
266 
267 {
268 for (const delay of ['', false]) {
269 await rejects(
270 () => timersPromises.setTimeout(delay, null, {}),
271 /ERR_INVALID_ARG_TYPE/
272 );
273 }
274 
275 for (const options of [1, '', false, Infinity]) {
276 await rejects(
277 () => timersPromises.setTimeout(10, null, options),
278 /ERR_INVALID_ARG_TYPE/
279 );
280 }
281 
282 for (const signal of [1, '', false, Infinity, null, {}]) {
283 await rejects(
284 () => timersPromises.setTimeout(10, null, { signal }),
285 /ERR_INVALID_ARG_TYPE/
286 );
287 }
288 
289 for (const ref of [1, '', Infinity, null, {}]) {
290 await rejects(
291 () => timersPromises.setTimeout(10, null, { ref }),
292 /ERR_INVALID_ARG_TYPE/
293 );
294 }
295 }
296 
297 {
298 const signal = AbortSignal.abort('boom');
299 await rejects(timersPromises.setTimeout(1, undefined, { signal }), {
300 cause: 'boom',
301 });
302 }
303 },
304};
305 
306// Tests are taken from
307// https://github.com/nodejs/node/blob/7bc2946293757389468f1fa09714860f8e1147b7/test/parallel/test-timers-interval-promisified.js
308export const timersIntervalPromise = {
309 async test() {
310 {
311 const iterable = timersPromises.setInterval(1, undefined);
312 const iterator = iterable[Symbol.asyncIterator]();
313 const result = iterator.next();
314 ok(!result.done, 'iterator was wrongly marked as done');
315 strictEqual(result.value, undefined);
316 await iterator.return();
317 }
318 
319 {
320 const iterable = timersPromises.setInterval(1, 'foobar');
321 const iterator = iterable[Symbol.asyncIterator]();
322 const result = await iterator.next();
323 ok(!result.done, 'iterator was wronly marked as done');
324 strictEqual(result.value, 'foobar');
325 await iterator.return();
326 }
327 
328 {
329 const iterable = timersPromises.setInterval(1, 'foobar');
330 const iterator = iterable[Symbol.asyncIterator]();
331 const result = await iterator.next();
332 
333 ok(!result.done, 'iterator was wronly marked as done');
334 strictEqual(result.value, 'foobar');
335 await iterator.next();
336 }
337 
338 {
339 const signal = AbortSignal.abort(); // Abort in advance
340 
341 const iterable = timersPromises.setInterval(1, undefined, { signal });
342 const iterator = iterable[Symbol.asyncIterator]();
343 await rejects(iterator.next(), /AbortError/);
344 }
345 
346 {
347 const ac = new AbortController();
348 const { signal } = ac;
349 
350 const iterable = timersPromises.setInterval(100, undefined, { signal });
351 const iterator = iterable[Symbol.asyncIterator]();
352 
353 // This promise should take 100 seconds to resolve, so now aborting it should
354 // mean we abort early
355 const promise = iterator.next();
356 
357 ac.abort(); // Abort in after we have a next promise
358 
359 await rejects(promise, /AbortError/);
360 }
361 
362 {
363 // Check aborting after getting a value.
364 const ac = new AbortController();
365 const { signal } = ac;
366 
367 const iterable = timersPromises.setInterval(100, undefined, { signal });
368 const iterator = iterable[Symbol.asyncIterator]();
369 
370 const promise = iterator.next();
371 const abortPromise = promise.then(ac.abort()).then(() => iterator.next());
372 await rejects(abortPromise, /AbortError/);
373 }
374 
375 {
376 for (const ref of [1, '', Infinity, null, {}]) {
377 const iterable = timersPromises.setInterval(10, undefined, { ref });
378 await rejects(
379 () => iterable[Symbol.asyncIterator]().next(),
380 /ERR_INVALID_ARG_TYPE/
381 );
382 }
383 
384 for (const signal of [1, '', Infinity, null, {}]) {
385 const iterable = timersPromises.setInterval(10, undefined, { signal });
386 await rejects(
387 () => iterable[Symbol.asyncIterator]().next(),
388 /ERR_INVALID_ARG_TYPE/
389 );
390 }
391 
392 for (const options of [1, '', Infinity, null, true, false]) {
393 const iterable = timersPromises.setInterval(10, undefined, options);
394 await rejects(
395 () => iterable[Symbol.asyncIterator]().next(),
396 /ERR_INVALID_ARG_TYPE/
397 );
398 }
399 }
400 
401 {
402 async function runInterval(fn, intervalTime, signal) {
403 const input = 'foobar';
404 const interval = timersPromises.setInterval(intervalTime, input, {
405 signal,
406 });
407 let iteration = 0;
408 for await (const value of interval) {
409 strictEqual(value, input);
410 iteration++;
411 await fn(iteration);
412 }
413 }
414 
415 {
416 // Check that we call the correct amount of times.
417 const controller = new AbortController();
418 const { signal } = controller;
419 
420 let loopCount = 0;
421 const delay = 20;
422 const timeoutLoop = runInterval(
423 () => {
424 loopCount++;
425 if (loopCount === 5) controller.abort();
426 if (loopCount > 5) throw new Error('ran too many times');
427 },
428 delay,
429 signal
430 );
431 
432 await rejects(timeoutLoop, /AbortError/);
433 strictEqual(loopCount, 5);
434 }
435 
436 {
437 // Check that if we abort when we have some unresolved callbacks,
438 // we actually call them.
439 const controller = new AbortController();
440 const { signal } = controller;
441 const delay = 10;
442 let totalIterations = 0;
443 const timeoutLoop = runInterval(
444 async (iterationNumber) => {
445 await timersPromises.setTimeout(delay * 4);
446 if (iterationNumber <= 2) {
447 strictEqual(signal.aborted, false);
448 }
449 if (iterationNumber === 2) {
450 controller.abort();
451 }
452 if (iterationNumber > 2) {
453 strictEqual(signal.aborted, true);
454 }
455 if (iterationNumber > totalIterations) {
456 totalIterations = iterationNumber;
457 }
458 },
459 delay,
460 signal
461 );
462 
463 await timeoutLoop.catch(() => {
464 ok(totalIterations >= 3, `iterations was ${totalIterations} < 3`);
465 });
466 }
467 }
468 
469 {
470 // Check that the timing is correct
471 let pre = false;
472 let post = false;
473 
474 const time_unit = 50;
475 await Promise.all([
476 timersPromises.setTimeout(1).then(() => (pre = true)),
477 new Promise((res) => {
478 const iterable = timersPromises.setInterval(time_unit * 2);
479 const iterator = iterable[Symbol.asyncIterator]();
480 
481 iterator
482 .next()
483 .then(() => {
484 ok(pre, 'interval ran too early');
485 ok(!post, 'interval ran too late');
486 return iterator.next();
487 })
488 .then(() => {
489 ok(post, 'second interval ran too early');
490 return iterator.return();
491 })
492 .then(res);
493 }),
494 timersPromises.setTimeout(time_unit * 3).then(() => (post = true)),
495 ]);
496 }
497 },
498};
499 
500export const testImmediateFromOtherContext = {
501 async test(_, env) {
502 // Set up the global setImmediate...
503 await env.SUBREQUEST.fetch('http://example.com');
504 
505 // Try to access it from a different IoContext.
506 await env.SUBREQUEST.fetch('http://example.com');
507 },
508};
509 
510export default {
511 fetch() {
512 if (globalThis.IMMEDIATE_TEST === undefined) {
513 globalThis.IMMEDIATE_TEST = setImmediate(() => {});
514 globalThis.IMMEDIATE_TEST[Symbol.dispose]();
515 } else {
516 throws(() => globalThis.IMMEDIATE_TEST[Symbol.dispose](), {
517 message: /perform I\/O on behalf of a different request/,
518 });
519 }
520 return new Response();
521 },
522};