Skip to content
File

Blob: src/node/internal/internal_timers_promises.ts

typescript246 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 * as timers from 'node-internal:internal_timers';
27import { ERR_INVALID_THIS, AbortError } from 'node-internal:internal_errors';
28import {
29 validateNumber,
30 validateAbortSignal,
31 validateBoolean,
32 validateObject,
33} from 'node-internal:validators';
34 
35const kScheduler = Symbol.for('kScheduler');
36 
37type OnCancelCallback = (() => void) | undefined;
38 
39export async function setTimeout<T = void>(
40 delay?: number,
41 value?: T,
42 options: { signal?: AbortSignal | undefined; ref?: boolean | undefined } = {}
43): Promise<T> {
44 if (delay !== undefined) {
45 validateNumber(delay, 'delay');
46 }
47 
48 validateObject(options, 'options');
49 
50 // Ref options is a no-op.
51 const { signal, ref } = options;
52 
53 if (signal !== undefined) {
54 validateAbortSignal(signal, 'options.signal');
55 }
56 
57 // This is required due to consistency/compat reasons, even if it's a no-op.
58 if (ref !== undefined) {
59 validateBoolean(ref, 'options.ref');
60 }
61 
62 if (signal?.aborted) {
63 throw new AbortError(undefined, { cause: signal.reason });
64 }
65 
66 const { promise, resolve, reject } = Promise.withResolvers<T>();
67 let onCancel: OnCancelCallback;
68 
69 const timer = timers.setTimeout(() => {
70 resolve(value as T);
71 if (onCancel) {
72 signal?.removeEventListener('abort', onCancel);
73 }
74 }, delay ?? 1);
75 
76 if (signal) {
77 onCancel = (): void => {
78 timers.clearTimeout(timer);
79 reject(new AbortError(undefined, { cause: signal.reason }));
80 };
81 signal.addEventListener('abort', onCancel, { once: true });
82 }
83 
84 return promise;
85}
86 
87export async function setImmediate<T = void>(
88 value?: T,
89 options: { signal?: AbortSignal | undefined; ref?: boolean | undefined } = {}
90): Promise<T> {
91 validateObject(options, 'options');
92 
93 // Ref options is a no-op.
94 const { signal, ref } = options;
95 
96 if (signal !== undefined) {
97 validateAbortSignal(signal, 'options.signal');
98 }
99 
100 // This is required due to consistency/compat reasons, even if it's a no-op.
101 if (ref !== undefined) {
102 validateBoolean(ref, 'options.ref');
103 }
104 
105 if (signal?.aborted) {
106 throw new AbortError(undefined, { cause: signal.reason });
107 }
108 
109 const { promise, resolve, reject } = Promise.withResolvers<T>();
110 let onCancel: OnCancelCallback;
111 
112 const timer = timers.setImmediate(() => {
113 resolve(value as T);
114 if (onCancel) {
115 signal?.removeEventListener('abort', onCancel);
116 }
117 });
118 
119 if (signal) {
120 onCancel = (): void => {
121 timers.clearImmediate(timer);
122 reject(new AbortError(undefined, { cause: signal.reason }));
123 };
124 signal.addEventListener('abort', onCancel, { once: true });
125 }
126 
127 return promise;
128}
129 
130export async function* setInterval<T = void>(
131 delay?: number,
132 value?: T,
133 options: {
134 signal?: AbortSignal | undefined;
135 ref?: boolean | undefined;
136 } = {}
137): AsyncGenerator<T, undefined> {
138 if (delay !== undefined) {
139 validateNumber(delay, 'delay');
140 }
141 
142 validateObject(options, 'options');
143 
144 // Ref options is a no-op.
145 const { signal, ref } = options;
146 
147 if (signal !== undefined) {
148 validateAbortSignal(signal, 'options.signal');
149 }
150 
151 if (ref !== undefined) {
152 validateBoolean(ref, 'options.ref');
153 }
154 
155 if (signal?.aborted) {
156 throw new AbortError(undefined, { cause: signal.reason });
157 }
158 
159 let onCancel: OnCancelCallback;
160 let interval: timers.Timeout;
161 try {
162 let notYielded = 0;
163 let callback: ((promise?: Promise<void>) => void) | undefined;
164 interval = new timers.Timeout(
165 () => {
166 notYielded++;
167 callback?.();
168 callback = undefined;
169 },
170 delay,
171 undefined,
172 true,
173 ref
174 );
175 
176 if (signal) {
177 onCancel = (): void => {
178 timers.clearInterval(interval);
179 callback?.(
180 Promise.reject(new AbortError(undefined, { cause: signal.reason }))
181 );
182 callback = undefined;
183 };
184 signal.addEventListener('abort', onCancel, { once: true });
185 }
186 
187 while (!signal?.aborted) {
188 if (notYielded === 0) {
189 await new Promise((resolve) => (callback = resolve));
190 }
191 for (; notYielded > 0; notYielded--) {
192 yield value as T;
193 }
194 }
195 throw new AbortError(undefined, { cause: signal.reason });
196 } finally {
197 // @ts-expect-error TS2454 TS detects invalid use before assignment.
198 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
199 if (interval) {
200 timers.clearInterval(interval);
201 }
202 if (onCancel) {
203 signal?.removeEventListener('abort', onCancel);
204 }
205 }
206}
207 
208declare global {
209 interface Scheduler {
210 wait: (delay: number, options?: { signal?: AbortSignal }) => Promise<void>;
211 }
212}
213 
214// TODO(@jasnell): Scheduler is an API currently being discussed by WICG
215// for Web Platform standardization: https://github.com/WICG/scheduling-apis
216// The scheduler.yield() and scheduler.wait() methods correspond roughly to
217// the awaitable setTimeout and setImmediate implementations here. This api
218// should be considered to be experimental until the spec for these are
219// finalized. Note, also, that Scheduler is expected to be defined as a global,
220// but while the API is experimental we shouldn't expose it as such.
221class Scheduler {
222 [kScheduler] = true;
223 
224 yield(): Promise<void> {
225 if (!this[kScheduler]) throw new ERR_INVALID_THIS('Scheduler');
226 return setImmediate();
227 }
228 
229 wait(delay: number, options?: { signal?: AbortSignal }): Promise<void> {
230 if (!this[kScheduler]) throw new ERR_INVALID_THIS('Scheduler');
231 // TODO(soon): The cast through `unknown` is needed because the local `class Scheduler`
232 // shadows the global `Scheduler` interface augmentation, causing TS to see two incompatible
233 // `Scheduler` types. Refactor to avoid the name collision (e.g. rename the local class).
234 return (
235 globalThis.scheduler as unknown as {
236 wait: (
237 delay: number,
238 options?: { signal?: AbortSignal }
239 ) => Promise<void>;
240 }
241 ).wait(delay, options);
242 }
243}
244 
245export const scheduler = new Scheduler();