@gjsify/timers 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,325 @@
1
+ import { describe, it, expect } from '@gjsify/unit';
2
+ import timers from 'node:timers';
3
+
4
+ export default async () => {
5
+ await describe('timers', async () => {
6
+ await describe('setTimeout', async () => {
7
+ await it('should call callback after delay', async () => {
8
+ const result = await new Promise<string>((resolve) => {
9
+ timers.setTimeout(() => resolve('done'), 10);
10
+ });
11
+ expect(result).toBe('done');
12
+ });
13
+
14
+ await it('should pass arguments to callback', async () => {
15
+ const result = await new Promise<string>((resolve) => {
16
+ timers.setTimeout((a: string, b: string) => resolve(a + b), 10, 'hello', ' world');
17
+ });
18
+ expect(result).toBe('hello world');
19
+ });
20
+
21
+ await it('should return a Timeout object with ref/unref/hasRef', async () => {
22
+ const timeout = timers.setTimeout(() => {}, 1000);
23
+ expect(timeout.hasRef()).toBe(true);
24
+ timeout.unref();
25
+ expect(timeout.hasRef()).toBe(false);
26
+ timeout.ref();
27
+ expect(timeout.hasRef()).toBe(true);
28
+ timers.clearTimeout(timeout);
29
+ });
30
+
31
+ await it('should execute with 0 delay', async () => {
32
+ const result = await new Promise<string>((resolve) => {
33
+ timers.setTimeout(() => resolve('zero'), 0);
34
+ });
35
+ expect(result).toBe('zero');
36
+ });
37
+
38
+ await it('should treat negative delay as 0', async () => {
39
+ const result = await new Promise<string>((resolve) => {
40
+ timers.setTimeout(() => resolve('negative'), -100);
41
+ });
42
+ expect(result).toBe('negative');
43
+ });
44
+
45
+ await it('should execute multiple timeouts in order with same delay', async () => {
46
+ const order: number[] = [];
47
+ await new Promise<void>((resolve) => {
48
+ timers.setTimeout(() => order.push(1), 10);
49
+ timers.setTimeout(() => order.push(2), 10);
50
+ timers.setTimeout(() => {
51
+ order.push(3);
52
+ resolve();
53
+ }, 30);
54
+ });
55
+ expect(order[0]).toBe(1);
56
+ expect(order[1]).toBe(2);
57
+ expect(order[2]).toBe(3);
58
+ });
59
+
60
+ await it('should allow nested setTimeout', async () => {
61
+ const result = await new Promise<string>((resolve) => {
62
+ timers.setTimeout(() => {
63
+ timers.setTimeout(() => resolve('nested'), 10);
64
+ }, 10);
65
+ });
66
+ expect(result).toBe('nested');
67
+ });
68
+ });
69
+
70
+ await describe('clearTimeout', async () => {
71
+ await it('should cancel a pending timeout', async () => {
72
+ let called = false;
73
+ const timeout = timers.setTimeout(() => { called = true; }, 10);
74
+ timers.clearTimeout(timeout);
75
+ await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 50));
76
+ expect(called).toBe(false);
77
+ });
78
+
79
+ await it('clearTimeout(null) should not throw', async () => {
80
+ expect(() => timers.clearTimeout(null as any)).not.toThrow();
81
+ });
82
+
83
+ await it('clearTimeout(undefined) should not throw', async () => {
84
+ expect(() => timers.clearTimeout(undefined as any)).not.toThrow();
85
+ });
86
+ });
87
+
88
+ await describe('setInterval', async () => {
89
+ await it('should call callback repeatedly', async () => {
90
+ let count = 0;
91
+ const interval = timers.setInterval(() => { count++; }, 10);
92
+ await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 55));
93
+ timers.clearInterval(interval);
94
+ expect(count).toBeGreaterThan(1);
95
+ });
96
+
97
+ await it('should stop calling after clearInterval', async () => {
98
+ let count = 0;
99
+ const interval = timers.setInterval(() => { count++; }, 10);
100
+ await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 35));
101
+ timers.clearInterval(interval);
102
+ const countAfterClear = count;
103
+ await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 50));
104
+ expect(count).toBe(countAfterClear);
105
+ });
106
+
107
+ await it('should pass arguments to callback', async () => {
108
+ const result = await new Promise<string>((resolve) => {
109
+ const interval = timers.setInterval((a: string) => {
110
+ timers.clearInterval(interval);
111
+ resolve(a);
112
+ }, 10, 'arg');
113
+ });
114
+ expect(result).toBe('arg');
115
+ });
116
+ });
117
+
118
+ await describe('clearInterval', async () => {
119
+ await it('clearInterval(null) should not throw', async () => {
120
+ expect(() => timers.clearInterval(null as any)).not.toThrow();
121
+ });
122
+
123
+ await it('clearInterval(undefined) should not throw', async () => {
124
+ expect(() => timers.clearInterval(undefined as any)).not.toThrow();
125
+ });
126
+ });
127
+
128
+ await describe('setImmediate', async () => {
129
+ await it('should call callback on next tick', async () => {
130
+ const result = await new Promise<string>((resolve) => {
131
+ timers.setImmediate(() => resolve('immediate'));
132
+ });
133
+ expect(result).toBe('immediate');
134
+ });
135
+
136
+ await it('should pass arguments to callback', async () => {
137
+ const result = await new Promise<number>((resolve) => {
138
+ timers.setImmediate((a: number, b: number) => resolve(a + b), 1, 2);
139
+ });
140
+ expect(result).toBe(3);
141
+ });
142
+
143
+ await it('should have ref/unref/hasRef', async () => {
144
+ const immediate = timers.setImmediate(() => {});
145
+ expect(typeof immediate.ref).toBe('function');
146
+ expect(typeof immediate.unref).toBe('function');
147
+ expect(typeof immediate.hasRef).toBe('function');
148
+ timers.clearImmediate(immediate);
149
+ });
150
+ });
151
+
152
+ await describe('clearImmediate', async () => {
153
+ await it('should cancel a pending immediate', async () => {
154
+ let called = false;
155
+ const immediate = timers.setImmediate(() => { called = true; });
156
+ timers.clearImmediate(immediate);
157
+ await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 50));
158
+ expect(called).toBe(false);
159
+ });
160
+
161
+ await it('clearImmediate(null) should not throw', async () => {
162
+ expect(() => timers.clearImmediate(null as any)).not.toThrow();
163
+ });
164
+ });
165
+
166
+ await describe('Timeout properties', async () => {
167
+ await it('should have refresh method', async () => {
168
+ const timeout = timers.setTimeout(() => {}, 1000);
169
+ expect(typeof timeout.refresh).toBe('function');
170
+ timers.clearTimeout(timeout);
171
+ });
172
+
173
+ await it('refresh should return the same object', async () => {
174
+ const timeout = timers.setTimeout(() => {}, 1000);
175
+ const result = timeout.refresh();
176
+ expect(result === timeout).toBeTruthy();
177
+ timers.clearTimeout(timeout);
178
+ });
179
+
180
+ await it('should have close method (alias for clearTimeout)', async () => {
181
+ const timeout = timers.setTimeout(() => {}, 1000);
182
+ expect(typeof timeout.close).toBe('function');
183
+ timeout.close();
184
+ });
185
+
186
+ await it('refresh should reset the timer', async () => {
187
+ let called = false;
188
+ const timeout = timers.setTimeout(() => { called = true; }, 50);
189
+ // Refresh before it fires
190
+ await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 30));
191
+ timeout.refresh();
192
+ // Should not have fired yet after refresh
193
+ await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 30));
194
+ expect(called).toBe(false);
195
+ // Now wait for it to fire
196
+ await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 40));
197
+ expect(called).toBe(true);
198
+ });
199
+ });
200
+
201
+ await describe('module exports', async () => {
202
+ await it('should export setTimeout and clearTimeout', async () => {
203
+ expect(typeof timers.setTimeout).toBe('function');
204
+ expect(typeof timers.clearTimeout).toBe('function');
205
+ });
206
+
207
+ await it('should export setInterval and clearInterval', async () => {
208
+ expect(typeof timers.setInterval).toBe('function');
209
+ expect(typeof timers.clearInterval).toBe('function');
210
+ });
211
+
212
+ await it('should export setImmediate and clearImmediate', async () => {
213
+ expect(typeof timers.setImmediate).toBe('function');
214
+ expect(typeof timers.clearImmediate).toBe('function');
215
+ });
216
+
217
+ await it('should export active and unenroll (legacy)', async () => {
218
+ // Node.js exports these legacy functions
219
+ expect(typeof timers.active === 'function' || typeof timers.active === 'undefined').toBe(true);
220
+ expect(typeof timers.unenroll === 'function' || typeof timers.unenroll === 'undefined').toBe(true);
221
+ });
222
+ });
223
+
224
+ // ==================== Additional tests ====================
225
+
226
+ await describe('setTimeout additional', async () => {
227
+ await it('should not throw with very large delay', async () => {
228
+ const timeout = timers.setTimeout(() => {}, 2147483647);
229
+ expect(timeout).toBeDefined();
230
+ timers.clearTimeout(timeout);
231
+ });
232
+
233
+ await it('should handle string delay by coercing to number', async () => {
234
+ const result = await new Promise<string>((resolve) => {
235
+ timers.setTimeout(() => resolve('coerced'), '10' as any);
236
+ });
237
+ expect(result).toBe('coerced');
238
+ });
239
+ });
240
+
241
+ await describe('setInterval additional', async () => {
242
+ await it('should fire at least 3 times then be clearable', async () => {
243
+ let count = 0;
244
+ await new Promise<void>((resolve) => {
245
+ const interval = timers.setInterval(() => {
246
+ count++;
247
+ if (count >= 3) {
248
+ timers.clearInterval(interval);
249
+ resolve();
250
+ }
251
+ }, 15);
252
+ });
253
+ expect(count).toBeGreaterThan(2);
254
+ });
255
+
256
+ await it('should handle 0 interval without hanging', async () => {
257
+ let count = 0;
258
+ await new Promise<void>((resolve) => {
259
+ const interval = timers.setInterval(() => {
260
+ count++;
261
+ if (count >= 3) {
262
+ timers.clearInterval(interval);
263
+ resolve();
264
+ }
265
+ }, 0);
266
+ });
267
+ expect(count).toBeGreaterThan(2);
268
+ });
269
+ });
270
+
271
+ await describe('clearTimeout additional', async () => {
272
+ await it('clearTimeout on already fired timer should not throw', async () => {
273
+ const timeout = timers.setTimeout(() => {}, 5);
274
+ // Wait for the timer to fire
275
+ await new Promise<void>((resolve) => globalThis.setTimeout(resolve, 50));
276
+ // Clearing after it has fired should be safe
277
+ expect(() => timers.clearTimeout(timeout)).not.toThrow();
278
+ });
279
+ });
280
+
281
+ await describe('Timeout.refresh additional', async () => {
282
+ await it('refresh() on cleared timer should not throw', async () => {
283
+ const timeout = timers.setTimeout(() => {}, 1000);
284
+ timers.clearTimeout(timeout);
285
+ // Calling refresh on a cleared timer should not throw
286
+ expect(() => timeout.refresh()).not.toThrow();
287
+ });
288
+ });
289
+
290
+ await describe('setImmediate ordering', async () => {
291
+ await it('should execute before setTimeout(0)', async () => {
292
+ const order: string[] = [];
293
+ await new Promise<void>((resolve) => {
294
+ timers.setTimeout(() => {
295
+ order.push('timeout');
296
+ if (order.length === 2) resolve();
297
+ }, 0);
298
+ timers.setImmediate(() => {
299
+ order.push('immediate');
300
+ if (order.length === 2) resolve();
301
+ });
302
+ });
303
+ // setImmediate should fire before or at least at the same time as setTimeout(0)
304
+ // In Node.js, order can vary in the top level, but setImmediate is generally prioritized
305
+ expect(order.length).toBe(2);
306
+ expect(order[0]).toBe('immediate');
307
+ });
308
+
309
+ await it('multiple setImmediates should execute in order', async () => {
310
+ const order: number[] = [];
311
+ await new Promise<void>((resolve) => {
312
+ timers.setImmediate(() => order.push(1));
313
+ timers.setImmediate(() => order.push(2));
314
+ timers.setImmediate(() => {
315
+ order.push(3);
316
+ resolve();
317
+ });
318
+ });
319
+ expect(order[0]).toBe(1);
320
+ expect(order[1]).toBe(2);
321
+ expect(order[2]).toBe(3);
322
+ });
323
+ });
324
+ });
325
+ };
package/src/index.ts ADDED
@@ -0,0 +1,83 @@
1
+ // Node.js timers module for GJS
2
+ // Reference: Node.js lib/timers.js
3
+
4
+ import { Timeout, Immediate } from './timeout.js';
5
+
6
+ export { Timeout, Immediate };
7
+
8
+ /**
9
+ * Schedule a callback to be called after `delay` milliseconds.
10
+ * Returns a Timeout object with ref/unref/refresh methods.
11
+ */
12
+ function _setTimeout<T extends any[]>(callback: (...args: T) => void, delay = 0, ...args: T): Timeout {
13
+ return new Timeout(callback, delay, args, false);
14
+ }
15
+
16
+ /**
17
+ * Cancel a timeout created by setTimeout.
18
+ */
19
+ function _clearTimeout(timeout: Timeout | number | undefined): void {
20
+ if (timeout instanceof Timeout) {
21
+ timeout.close();
22
+ } else if (timeout != null) {
23
+ clearTimeout(timeout as any);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Schedule a callback to be called repeatedly every `delay` milliseconds.
29
+ * Returns a Timeout object with ref/unref/refresh methods.
30
+ */
31
+ function _setInterval<T extends any[]>(callback: (...args: T) => void, delay = 0, ...args: T): Timeout {
32
+ return new Timeout(callback, delay, args, true);
33
+ }
34
+
35
+ /**
36
+ * Cancel an interval created by setInterval.
37
+ */
38
+ function _clearInterval(timeout: Timeout | number | undefined): void {
39
+ if (timeout instanceof Timeout) {
40
+ timeout.close();
41
+ } else if (timeout != null) {
42
+ clearInterval(timeout as any);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Schedule a callback to be called on the next iteration of the event loop.
48
+ * Returns an Immediate object with ref/unref methods.
49
+ */
50
+ function _setImmediate<T extends any[]>(callback: (...args: T) => void, ...args: T): Immediate {
51
+ return new Immediate(callback, args);
52
+ }
53
+
54
+ /**
55
+ * Cancel an immediate created by setImmediate.
56
+ */
57
+ function _clearImmediate(immediate: Immediate | number | undefined): void {
58
+ if (immediate instanceof Immediate) {
59
+ immediate.close();
60
+ } else if (immediate != null) {
61
+ clearTimeout(immediate as any);
62
+ }
63
+ }
64
+
65
+ export {
66
+ _setTimeout as setTimeout,
67
+ _clearTimeout as clearTimeout,
68
+ _setInterval as setInterval,
69
+ _clearInterval as clearInterval,
70
+ _setImmediate as setImmediate,
71
+ _clearImmediate as clearImmediate,
72
+ };
73
+
74
+ export default {
75
+ setTimeout: _setTimeout,
76
+ clearTimeout: _clearTimeout,
77
+ setInterval: _setInterval,
78
+ clearInterval: _clearInterval,
79
+ setImmediate: _setImmediate,
80
+ clearImmediate: _clearImmediate,
81
+ Timeout,
82
+ Immediate,
83
+ };
@@ -0,0 +1,136 @@
1
+ import { describe, it, expect } from '@gjsify/unit';
2
+ import { setTimeout, setImmediate, setInterval } from 'node:timers/promises';
3
+
4
+ export default async () => {
5
+ await describe('timers/promises', async () => {
6
+ await describe('setTimeout', async () => {
7
+ await it('should resolve after delay', async () => {
8
+ const start = Date.now();
9
+ await setTimeout(20);
10
+ const elapsed = Date.now() - start;
11
+ expect(elapsed).toBeGreaterThan(10);
12
+ });
13
+
14
+ await it('should resolve with value', async () => {
15
+ const result = await setTimeout(10, 'hello');
16
+ expect(result).toBe('hello');
17
+ });
18
+
19
+ await it('should resolve with undefined when no value', async () => {
20
+ const result = await setTimeout(10);
21
+ expect(result).toBeUndefined();
22
+ });
23
+
24
+ await it('should reject when signal is already aborted', async () => {
25
+ const controller = new AbortController();
26
+ controller.abort();
27
+ let threw = false;
28
+ try {
29
+ await setTimeout(10, undefined, { signal: controller.signal });
30
+ } catch (e: any) {
31
+ threw = true;
32
+ expect(e.name).toBe('AbortError');
33
+ }
34
+ expect(threw).toBe(true);
35
+ });
36
+
37
+ await it('should reject when signal is aborted during wait', async () => {
38
+ const controller = new AbortController();
39
+ globalThis.setTimeout(() => controller.abort(), 10);
40
+ let threw = false;
41
+ try {
42
+ await setTimeout(1000, undefined, { signal: controller.signal });
43
+ } catch (e: any) {
44
+ threw = true;
45
+ expect(e.name).toBe('AbortError');
46
+ }
47
+ expect(threw).toBe(true);
48
+ });
49
+
50
+ await it('should accept 0 delay', async () => {
51
+ const result = await setTimeout(0, 'zero');
52
+ expect(result).toBe('zero');
53
+ });
54
+ });
55
+
56
+ await describe('setImmediate', async () => {
57
+ await it('should resolve with value', async () => {
58
+ const result = await setImmediate(42);
59
+ expect(result).toBe(42);
60
+ });
61
+
62
+ await it('should resolve with undefined when no value', async () => {
63
+ const result = await setImmediate();
64
+ expect(result).toBeUndefined();
65
+ });
66
+
67
+ await it('should reject when signal is already aborted', async () => {
68
+ const controller = new AbortController();
69
+ controller.abort();
70
+ let threw = false;
71
+ try {
72
+ await setImmediate(undefined, { signal: controller.signal });
73
+ } catch (e: any) {
74
+ threw = true;
75
+ expect(e.name).toBe('AbortError');
76
+ }
77
+ expect(threw).toBe(true);
78
+ });
79
+ });
80
+
81
+ await describe('setInterval', async () => {
82
+ await it('should be an async generator', async () => {
83
+ const controller = new AbortController();
84
+ const iter = setInterval(10, 'tick', { signal: controller.signal });
85
+ expect(typeof iter[Symbol.asyncIterator]).toBe('function');
86
+ controller.abort();
87
+ });
88
+
89
+ await it('should yield values at intervals', async () => {
90
+ const controller = new AbortController();
91
+ const values: string[] = [];
92
+ try {
93
+ for await (const val of setInterval(15, 'tick', { signal: controller.signal })) {
94
+ values.push(val);
95
+ if (values.length >= 3) {
96
+ controller.abort();
97
+ }
98
+ }
99
+ } catch (e: any) {
100
+ expect(e.name).toBe('AbortError');
101
+ }
102
+ expect(values.length).toBe(3);
103
+ expect(values[0]).toBe('tick');
104
+ });
105
+
106
+ await it('should reject immediately when signal is already aborted', async () => {
107
+ const controller = new AbortController();
108
+ controller.abort();
109
+ let threw = false;
110
+ try {
111
+ for await (const _ of setInterval(10, 'tick', { signal: controller.signal })) {
112
+ // Should not reach here
113
+ }
114
+ } catch (e: any) {
115
+ threw = true;
116
+ expect(e.name).toBe('AbortError');
117
+ }
118
+ expect(threw).toBe(true);
119
+ });
120
+ });
121
+
122
+ await describe('exports', async () => {
123
+ await it('should export setTimeout as a function', async () => {
124
+ expect(typeof setTimeout).toBe('function');
125
+ });
126
+
127
+ await it('should export setImmediate as a function', async () => {
128
+ expect(typeof setImmediate).toBe('function');
129
+ });
130
+
131
+ await it('should export setInterval as a function', async () => {
132
+ expect(typeof setInterval).toBe('function');
133
+ });
134
+ });
135
+ });
136
+ };
@@ -0,0 +1,89 @@
1
+ // Node.js timers/promises module for GJS
2
+ // Reference: Node.js lib/timers/promises.js
3
+
4
+ import { Timeout, Immediate } from './timeout.js';
5
+
6
+ /**
7
+ * Returns a promise that resolves after `delay` milliseconds.
8
+ * Supports AbortSignal for cancellation.
9
+ */
10
+ export function setTimeout<T = void>(delay = 0, value?: T, options?: { signal?: AbortSignal; ref?: boolean }): Promise<T> {
11
+ return new Promise<T>((resolve, reject) => {
12
+ if (options?.signal?.aborted) {
13
+ reject(options.signal.reason ?? new DOMException('The operation was aborted', 'AbortError'));
14
+ return;
15
+ }
16
+
17
+ const timeout = new Timeout(() => {
18
+ cleanup();
19
+ resolve(value as T);
20
+ }, delay, [], false);
21
+
22
+ if (options?.ref === false) timeout.unref();
23
+
24
+ let onAbort: (() => void) | undefined;
25
+
26
+ function cleanup() {
27
+ if (onAbort && options?.signal) {
28
+ options.signal.removeEventListener('abort', onAbort);
29
+ }
30
+ }
31
+
32
+ if (options?.signal) {
33
+ onAbort = () => {
34
+ timeout.close();
35
+ reject(options!.signal!.reason ?? new DOMException('The operation was aborted', 'AbortError'));
36
+ };
37
+ options.signal.addEventListener('abort', onAbort, { once: true });
38
+ }
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Returns a promise that resolves on the next event loop iteration.
44
+ * Supports AbortSignal for cancellation.
45
+ */
46
+ export function setImmediate<T = void>(value?: T, options?: { signal?: AbortSignal; ref?: boolean }): Promise<T> {
47
+ return setTimeout(0, value, options);
48
+ }
49
+
50
+ /**
51
+ * Returns an async iterable that yields at `delay` ms intervals.
52
+ * Supports AbortSignal for cancellation.
53
+ */
54
+ export async function* setInterval<T = void>(delay = 0, value?: T, options?: { signal?: AbortSignal; ref?: boolean }): AsyncGenerator<T> {
55
+ if (options?.signal?.aborted) {
56
+ throw options.signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
57
+ }
58
+
59
+ while (true) {
60
+ if (options?.signal?.aborted) {
61
+ throw options.signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
62
+ }
63
+
64
+ yield await new Promise<T>((resolve, reject) => {
65
+ const timeout = new Timeout(() => {
66
+ cleanup();
67
+ resolve(value as T);
68
+ }, delay, [], false);
69
+
70
+ if (options?.ref === false) timeout.unref();
71
+
72
+ let onAbort: (() => void) | undefined;
73
+
74
+ function cleanup() {
75
+ if (onAbort && options?.signal) {
76
+ options.signal.removeEventListener('abort', onAbort);
77
+ }
78
+ }
79
+
80
+ if (options?.signal) {
81
+ onAbort = () => {
82
+ timeout.close();
83
+ reject(options!.signal!.reason ?? new DOMException('The operation was aborted', 'AbortError'));
84
+ };
85
+ options.signal.addEventListener('abort', onAbort, { once: true });
86
+ }
87
+ });
88
+ }
89
+ }
package/src/test.mts ADDED
@@ -0,0 +1,8 @@
1
+ import '@gjsify/node-globals';
2
+ import { run } from '@gjsify/unit';
3
+
4
+ import testSuiteTimers from './index.spec.js';
5
+ import testSuitePromises from './promises.spec.js';
6
+ import extendedTestSuite from './extended.spec.js';
7
+
8
+ run({ testSuiteTimers, testSuitePromises, extendedTestSuite });