@evolu/common 8.6.2 → 8.8.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.
- package/dist/src/Array.d.ts +21 -21
- package/dist/src/Array.js +5 -5
- package/dist/src/Assert.d.ts +478 -141
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +644 -99
- package/dist/src/Binary.d.ts +254 -0
- package/dist/src/Binary.d.ts.map +1 -0
- package/dist/src/Binary.js +958 -0
- package/dist/src/Console.d.ts +2 -10
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +6 -12
- package/dist/src/Crypto.d.ts +5 -5
- package/dist/src/Crypto.js +1 -1
- package/dist/src/Eq.d.ts +35 -40
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +51 -87
- package/dist/src/Function.d.ts +9 -14
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +7 -9
- package/dist/src/Http.d.ts +6 -7
- package/dist/src/Http.d.ts.map +1 -1
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +1 -0
- package/dist/src/LockManager.d.ts +1 -1
- package/dist/src/LockManager.js +1 -1
- package/dist/src/Lookup.d.ts +3 -2
- package/dist/src/Lookup.d.ts.map +1 -1
- package/dist/src/Lookup.js +4 -3
- package/dist/src/Number.d.ts +1 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +1 -1
- package/dist/src/Object.d.ts +4 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +5 -4
- package/dist/src/Option.d.ts +1 -2
- package/dist/src/Option.d.ts.map +1 -1
- package/dist/src/Option.js +1 -2
- package/dist/src/Platform.js +2 -0
- package/dist/src/Polyfills.js +4 -1
- package/dist/src/Random.d.ts +1 -1
- package/dist/src/Result.d.ts +49 -54
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +17 -22
- package/dist/src/Set.d.ts +6 -6
- package/dist/src/Set.js +2 -2
- package/dist/src/Sqlite.js +1 -1
- package/dist/src/String.js +3 -2
- package/dist/src/Task.d.ts +32 -32
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +20 -22
- package/dist/src/Test.d.ts +31 -3
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +46 -3
- package/dist/src/Time.d.ts +3 -11
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +10 -18
- package/dist/src/Type.d.ts +87 -85
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +70 -63
- package/dist/src/Types.d.ts +12 -12
- package/dist/src/Types.js +1 -1
- package/dist/src/WebSocket.d.ts +4 -7
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/Worker.d.ts +1 -1
- package/dist/src/Worker.js +7 -7
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/intl/_en.d.ts.map +1 -1
- package/dist/src/intl/_en.js +1 -1
- package/dist/src/intl/ar.d.ts.map +1 -1
- package/dist/src/intl/ar.js +1 -1
- package/dist/src/intl/bn.d.ts.map +1 -1
- package/dist/src/intl/bn.js +1 -1
- package/dist/src/intl/ca.d.ts.map +1 -1
- package/dist/src/intl/ca.js +1 -1
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +1 -1
- package/dist/src/intl/da.d.ts.map +1 -1
- package/dist/src/intl/da.js +1 -1
- package/dist/src/intl/de.d.ts.map +1 -1
- package/dist/src/intl/de.js +1 -1
- package/dist/src/intl/el.d.ts.map +1 -1
- package/dist/src/intl/el.js +1 -1
- package/dist/src/intl/es.d.ts.map +1 -1
- package/dist/src/intl/es.js +1 -1
- package/dist/src/intl/fa.d.ts.map +1 -1
- package/dist/src/intl/fa.js +1 -1
- package/dist/src/intl/fi.d.ts.map +1 -1
- package/dist/src/intl/fi.js +1 -1
- package/dist/src/intl/fil.d.ts.map +1 -1
- package/dist/src/intl/fil.js +1 -1
- package/dist/src/intl/fr.d.ts.map +1 -1
- package/dist/src/intl/fr.js +1 -1
- package/dist/src/intl/he.d.ts.map +1 -1
- package/dist/src/intl/he.js +1 -1
- package/dist/src/intl/hi.d.ts.map +1 -1
- package/dist/src/intl/hi.js +1 -1
- package/dist/src/intl/hr.d.ts.map +1 -1
- package/dist/src/intl/hr.js +1 -1
- package/dist/src/intl/hu.d.ts.map +1 -1
- package/dist/src/intl/hu.js +1 -1
- package/dist/src/intl/id.d.ts.map +1 -1
- package/dist/src/intl/id.js +1 -1
- package/dist/src/intl/it.d.ts.map +1 -1
- package/dist/src/intl/it.js +1 -1
- package/dist/src/intl/ja.d.ts.map +1 -1
- package/dist/src/intl/ja.js +1 -1
- package/dist/src/intl/ko.d.ts.map +1 -1
- package/dist/src/intl/ko.js +1 -1
- package/dist/src/intl/ml.d.ts.map +1 -1
- package/dist/src/intl/ml.js +1 -1
- package/dist/src/intl/mr.d.ts.map +1 -1
- package/dist/src/intl/mr.js +1 -1
- package/dist/src/intl/ms.d.ts.map +1 -1
- package/dist/src/intl/ms.js +1 -1
- package/dist/src/intl/nb.d.ts.map +1 -1
- package/dist/src/intl/nb.js +1 -1
- package/dist/src/intl/nl.d.ts.map +1 -1
- package/dist/src/intl/nl.js +1 -1
- package/dist/src/intl/pa.d.ts.map +1 -1
- package/dist/src/intl/pa.js +1 -1
- package/dist/src/intl/pl.d.ts.map +1 -1
- package/dist/src/intl/pl.js +1 -1
- package/dist/src/intl/pt-BR.d.ts.map +1 -1
- package/dist/src/intl/pt-BR.js +1 -1
- package/dist/src/intl/pt.d.ts.map +1 -1
- package/dist/src/intl/pt.js +1 -1
- package/dist/src/intl/ro.d.ts.map +1 -1
- package/dist/src/intl/ro.js +1 -1
- package/dist/src/intl/sk.d.ts.map +1 -1
- package/dist/src/intl/sk.js +1 -1
- package/dist/src/intl/sl.d.ts.map +1 -1
- package/dist/src/intl/sl.js +1 -1
- package/dist/src/intl/sv.d.ts.map +1 -1
- package/dist/src/intl/sv.js +1 -1
- package/dist/src/intl/sw.d.ts.map +1 -1
- package/dist/src/intl/sw.js +1 -1
- package/dist/src/intl/ta.d.ts.map +1 -1
- package/dist/src/intl/ta.js +1 -1
- package/dist/src/intl/te.d.ts.map +1 -1
- package/dist/src/intl/te.js +1 -1
- package/dist/src/intl/th.d.ts.map +1 -1
- package/dist/src/intl/th.js +1 -1
- package/dist/src/intl/tr.d.ts.map +1 -1
- package/dist/src/intl/tr.js +1 -1
- package/dist/src/intl/uk.d.ts.map +1 -1
- package/dist/src/intl/uk.js +1 -1
- package/dist/src/intl/ur.d.ts.map +1 -1
- package/dist/src/intl/ur.js +1 -1
- package/dist/src/intl/vi.d.ts.map +1 -1
- package/dist/src/intl/vi.js +1 -1
- package/dist/src/intl/zh-CN.d.ts.map +1 -1
- package/dist/src/intl/zh-CN.js +1 -1
- package/dist/src/intl/zh-TW.d.ts.map +1 -1
- package/dist/src/intl/zh-TW.js +1 -1
- package/dist/src/local-first/Evolu.d.ts +12 -12
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +2 -2
- package/dist/src/local-first/Protocol.d.ts +28 -76
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +45 -229
- package/dist/src/local-first/Query.d.ts +11 -11
- package/dist/src/local-first/Query.js +6 -6
- package/dist/src/local-first/Relay.d.ts +2 -2
- package/dist/src/local-first/Schema.d.ts +3 -5
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +2 -4
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +5 -5
- package/dist/src/local-first/Storage.js +4 -4
- package/dist/src/local-first/Timestamp.d.ts +9 -0
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +13 -10
- package/package.json +7 -5
- package/src/Array.test.ts +809 -0
- package/src/Array.ts +21 -21
- package/src/Assert.test.ts +1181 -0
- package/src/Assert.ts +971 -227
- package/src/BigInt.test.ts +28 -0
- package/src/Binary.test.ts +1128 -0
- package/src/Binary.ts +1377 -0
- package/src/Brand.test.ts +90 -0
- package/src/Cache.test.ts +183 -0
- package/src/Callbacks.test.ts +66 -0
- package/src/Console.test.ts +809 -0
- package/src/Console.ts +9 -17
- package/src/Crypto.test.ts +138 -0
- package/src/Crypto.ts +5 -5
- package/src/Eq.test.ts +333 -0
- package/src/Eq.ts +110 -176
- package/src/Error.test.ts +118 -0
- package/src/Function.test.ts +204 -0
- package/src/Function.ts +10 -15
- package/src/Http.test.ts +816 -0
- package/src/Http.ts +6 -7
- package/src/LeakDetector.test.ts +210 -0
- package/src/LeakDetector.ts +1 -0
- package/src/LockManager.ts +2 -2
- package/src/Lookup.test.ts +418 -0
- package/src/Lookup.ts +4 -3
- package/src/Microtask.test.ts +140 -0
- package/src/Number.test.ts +218 -0
- package/src/Number.ts +2 -2
- package/src/Object.test.ts +293 -0
- package/src/Object.ts +5 -4
- package/src/Option.test.ts +104 -0
- package/src/Option.ts +1 -2
- package/src/Order.test.ts +56 -0
- package/src/Platform.ts +2 -0
- package/src/Polyfills.ts +3 -4
- package/src/Random.test.ts +89 -0
- package/src/Random.ts +1 -1
- package/src/Ref.test.ts +139 -0
- package/src/RefCount.test.ts +184 -0
- package/src/Relation.test.ts +505 -0
- package/src/Resource.test.ts +4623 -0
- package/src/Result.test.ts +1829 -0
- package/src/Result.ts +49 -54
- package/src/Schedule.test.ts +1554 -0
- package/src/Set.test.ts +279 -0
- package/src/Set.ts +6 -6
- package/src/Sqlite.ts +1 -1
- package/src/Store.test.ts +288 -0
- package/src/String.test.ts +54 -0
- package/src/String.ts +3 -3
- package/src/Task.test.ts +10820 -0
- package/src/Task.ts +38 -43
- package/src/Test.test.ts +88 -0
- package/src/Test.ts +49 -3
- package/src/Time.test.ts +926 -0
- package/src/Time.ts +13 -21
- package/src/Type.base64.test.ts +88 -0
- package/src/Type.test.ts +20465 -0
- package/src/Type.ts +114 -111
- package/src/Types.test.ts +324 -0
- package/src/Types.ts +13 -13
- package/src/WebSocket.ts +4 -7
- package/src/Worker.test.ts +755 -0
- package/src/Worker.ts +13 -18
- package/src/index.ts +1 -1
- package/src/intl/_en.ts +1 -1
- package/src/intl/ar.ts +1 -1
- package/src/intl/bn.ts +1 -1
- package/src/intl/ca.ts +1 -1
- package/src/intl/cs.ts +1 -1
- package/src/intl/da.ts +1 -1
- package/src/intl/de.ts +1 -1
- package/src/intl/el.ts +1 -1
- package/src/intl/es.ts +1 -1
- package/src/intl/fa.ts +1 -1
- package/src/intl/fi.ts +1 -1
- package/src/intl/fil.ts +1 -1
- package/src/intl/fr.ts +1 -1
- package/src/intl/he.ts +1 -1
- package/src/intl/hi.ts +1 -1
- package/src/intl/hr.ts +1 -1
- package/src/intl/hu.ts +1 -1
- package/src/intl/id.ts +1 -1
- package/src/intl/intl.test.ts +3106 -0
- package/src/intl/it.ts +1 -1
- package/src/intl/ja.ts +1 -1
- package/src/intl/ko.ts +1 -1
- package/src/intl/ml.ts +1 -1
- package/src/intl/mr.ts +1 -1
- package/src/intl/ms.ts +1 -1
- package/src/intl/nb.ts +1 -1
- package/src/intl/nl.ts +1 -1
- package/src/intl/pa.ts +1 -1
- package/src/intl/pl.ts +1 -1
- package/src/intl/pt-BR.ts +1 -1
- package/src/intl/pt.ts +1 -1
- package/src/intl/ro.ts +1 -1
- package/src/intl/sk.ts +1 -1
- package/src/intl/sl.ts +1 -1
- package/src/intl/sv.ts +1 -1
- package/src/intl/sw.ts +1 -1
- package/src/intl/ta.ts +1 -1
- package/src/intl/te.ts +1 -1
- package/src/intl/th.ts +1 -1
- package/src/intl/tr.ts +1 -1
- package/src/intl/uk.ts +1 -1
- package/src/intl/ur.ts +1 -1
- package/src/intl/vi.ts +1 -1
- package/src/intl/zh-CN.ts +1 -1
- package/src/intl/zh-TW.ts +1 -1
- package/src/local-first/Evolu.test.ts +1921 -0
- package/src/local-first/Evolu.ts +18 -14
- package/src/local-first/Owner.test.ts +83 -0
- package/src/local-first/Protocol.test.ts +1037 -0
- package/src/local-first/Protocol.ts +75 -290
- package/src/local-first/Query.test.ts +349 -0
- package/src/local-first/Query.ts +11 -11
- package/src/local-first/Relay.ts +2 -2
- package/src/local-first/Schema.ts +3 -5
- package/src/local-first/Shared.test.ts +1828 -0
- package/src/local-first/Shared.ts +9 -8
- package/src/local-first/Storage.ts +4 -4
- package/src/local-first/Timestamp.test.ts +374 -0
- package/src/local-first/Timestamp.ts +22 -10
- package/dist/src/Buffer.d.ts +0 -122
- package/dist/src/Buffer.d.ts.map +0 -1
- package/dist/src/Buffer.js +0 -69
- package/src/Buffer.ts +0 -206
package/src/Time.test.ts
ADDED
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
import { afterEach, describe, it, mock } from "node:test";
|
|
2
|
+
import {
|
|
3
|
+
assertEqual,
|
|
4
|
+
assertFalse,
|
|
5
|
+
assertLength,
|
|
6
|
+
assertSame,
|
|
7
|
+
assertThrowsInstanceOf,
|
|
8
|
+
assertThrowsSame,
|
|
9
|
+
assertTrue,
|
|
10
|
+
} from "./Assert.ts";
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
Duration,
|
|
14
|
+
PerformanceDuration,
|
|
15
|
+
PerformanceTime,
|
|
16
|
+
PerformanceTimeOrigin,
|
|
17
|
+
PositiveDuration,
|
|
18
|
+
} from "./Time.ts";
|
|
19
|
+
import {
|
|
20
|
+
createTime,
|
|
21
|
+
DurationLiteral,
|
|
22
|
+
DurationLiteralDays,
|
|
23
|
+
DurationLiteralHours,
|
|
24
|
+
DurationLiteralMilliseconds,
|
|
25
|
+
DurationLiteralMinutes,
|
|
26
|
+
DurationLiteralSeconds,
|
|
27
|
+
DurationLiteralWeeks,
|
|
28
|
+
DurationLiteralYears,
|
|
29
|
+
durationToMillis,
|
|
30
|
+
formatMillisAsClockTime,
|
|
31
|
+
formatMillisAsDuration,
|
|
32
|
+
maxMillis,
|
|
33
|
+
Millis,
|
|
34
|
+
millisToDateIso,
|
|
35
|
+
performanceDurationBetween,
|
|
36
|
+
PositiveMillis,
|
|
37
|
+
saturateMillis,
|
|
38
|
+
testCreateTime,
|
|
39
|
+
} from "./Time.ts";
|
|
40
|
+
import { assertType, type DateIso, NonNaNNumber } from "./Type.ts";
|
|
41
|
+
|
|
42
|
+
const negativeMillisCause = {
|
|
43
|
+
type: "NonNegative",
|
|
44
|
+
value: -1,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const assertThrowsWithCause = (run: () => unknown, cause: unknown): void => {
|
|
48
|
+
const error = assertThrowsInstanceOf(run, Error);
|
|
49
|
+
assertEqual(error.message, "getOrThrow");
|
|
50
|
+
assertEqual(error.cause, cause);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe("Time", () => {
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
mock.restoreAll();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("createTime", () => {
|
|
59
|
+
it("now returns current time", () => {
|
|
60
|
+
mock.method(Date, "now", () => 123);
|
|
61
|
+
|
|
62
|
+
assertEqual(createTime().now(), 123);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('now with "DateIso" returns current time as ISO string', () => {
|
|
66
|
+
const time = createTime();
|
|
67
|
+
const result: DateIso = time.now("DateIso");
|
|
68
|
+
assertTrue(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(result));
|
|
69
|
+
const parsed = Date.parse(result);
|
|
70
|
+
assertTrue(parsed >= Date.now() - 100);
|
|
71
|
+
assertTrue(parsed <= Date.now() + 100);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("performance exposes the native clock", () => {
|
|
75
|
+
mock.method(performance, "now", () => 123.456);
|
|
76
|
+
|
|
77
|
+
const time = createTime();
|
|
78
|
+
const now: PerformanceTime = time.performance.now();
|
|
79
|
+
const timeOrigin: PerformanceTimeOrigin = time.performance.timeOrigin;
|
|
80
|
+
|
|
81
|
+
assertEqual(now, 123.456);
|
|
82
|
+
assertEqual(timeOrigin, performance.timeOrigin);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe("setTimeout", () => {
|
|
86
|
+
it("fires after the delay", () => {
|
|
87
|
+
let now = 1000;
|
|
88
|
+
const callbacks: Array<() => void> = [];
|
|
89
|
+
const setTimeout = mock.method(
|
|
90
|
+
globalThis,
|
|
91
|
+
"setTimeout",
|
|
92
|
+
(callback: () => void) => {
|
|
93
|
+
callbacks.push(callback);
|
|
94
|
+
return callbacks.length as unknown as ReturnType<
|
|
95
|
+
typeof globalThis.setTimeout
|
|
96
|
+
>;
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
mock.method(Date, "now", () => now);
|
|
100
|
+
const callback = mock.fn<() => void>();
|
|
101
|
+
|
|
102
|
+
createTime().setTimeout(callback, "10ms");
|
|
103
|
+
|
|
104
|
+
assertLength(setTimeout.mock.calls, 1);
|
|
105
|
+
assertSame(setTimeout.mock.calls[0].arguments[0], callbacks[0]);
|
|
106
|
+
assertEqual(setTimeout.mock.calls[0].arguments[1], 10);
|
|
107
|
+
assertEqual(callback.mock.callCount(), 0);
|
|
108
|
+
|
|
109
|
+
now += 10;
|
|
110
|
+
callbacks[0]();
|
|
111
|
+
|
|
112
|
+
assertEqual(callback.mock.callCount(), 1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("native-range timeout ignores wall-clock changes", () => {
|
|
116
|
+
let now = 1000;
|
|
117
|
+
const callbacks: Array<() => void> = [];
|
|
118
|
+
const setTimeout = mock.method(
|
|
119
|
+
globalThis,
|
|
120
|
+
"setTimeout",
|
|
121
|
+
(scheduledCallback: () => void) => {
|
|
122
|
+
callbacks.push(scheduledCallback);
|
|
123
|
+
return callbacks.length as unknown as ReturnType<
|
|
124
|
+
typeof globalThis.setTimeout
|
|
125
|
+
>;
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
mock.method(Date, "now", () => now);
|
|
129
|
+
const callback = mock.fn<() => void>();
|
|
130
|
+
|
|
131
|
+
createTime().setTimeout(callback, "10ms");
|
|
132
|
+
now -= 1000;
|
|
133
|
+
callbacks[0]();
|
|
134
|
+
|
|
135
|
+
assertEqual(setTimeout.mock.callCount(), 1);
|
|
136
|
+
assertEqual(callback.mock.callCount(), 1);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("maximum native delay uses one native timer", () => {
|
|
140
|
+
const maxNativeTimeoutMillis = 2 ** 31 - 1;
|
|
141
|
+
const callbacks: Array<() => void> = [];
|
|
142
|
+
const setTimeout = mock.method(
|
|
143
|
+
globalThis,
|
|
144
|
+
"setTimeout",
|
|
145
|
+
(scheduledCallback: () => void) => {
|
|
146
|
+
callbacks.push(scheduledCallback);
|
|
147
|
+
return callbacks.length as unknown as ReturnType<
|
|
148
|
+
typeof globalThis.setTimeout
|
|
149
|
+
>;
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
const dateNow = mock.method(Date, "now");
|
|
153
|
+
const callback = mock.fn<() => void>();
|
|
154
|
+
|
|
155
|
+
createTime().setTimeout(
|
|
156
|
+
callback,
|
|
157
|
+
PositiveMillis.orThrow(maxNativeTimeoutMillis),
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
assertLength(setTimeout.mock.calls, 1);
|
|
161
|
+
assertSame(setTimeout.mock.calls[0].arguments[0], callbacks[0]);
|
|
162
|
+
assertEqual(
|
|
163
|
+
setTimeout.mock.calls[0].arguments[1],
|
|
164
|
+
maxNativeTimeoutMillis,
|
|
165
|
+
);
|
|
166
|
+
assertEqual(dateNow.mock.callCount(), 0);
|
|
167
|
+
|
|
168
|
+
callbacks[0]();
|
|
169
|
+
|
|
170
|
+
assertEqual(setTimeout.mock.callCount(), 1);
|
|
171
|
+
assertEqual(callback.mock.callCount(), 1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("accounts for elapsed time while a long timeout is suspended", () => {
|
|
175
|
+
const maxNativeTimeoutMillis = 2 ** 31 - 1;
|
|
176
|
+
let now = 1000;
|
|
177
|
+
const callbacks: Array<() => void> = [];
|
|
178
|
+
const delays: Array<number | undefined> = [];
|
|
179
|
+
const callback = mock.fn<() => void>();
|
|
180
|
+
|
|
181
|
+
mock.method(Date, "now", () => now);
|
|
182
|
+
mock.method(
|
|
183
|
+
globalThis,
|
|
184
|
+
"setTimeout",
|
|
185
|
+
(scheduledCallback: () => void, delay?: number) => {
|
|
186
|
+
callbacks.push(scheduledCallback);
|
|
187
|
+
delays.push(delay);
|
|
188
|
+
return callbacks.length as unknown as ReturnType<typeof setTimeout>;
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
createTime().setTimeout(
|
|
193
|
+
callback,
|
|
194
|
+
PositiveMillis.orThrow(maxNativeTimeoutMillis + 100),
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
assertEqual(delays, [maxNativeTimeoutMillis]);
|
|
198
|
+
|
|
199
|
+
// Simulate the native timer firing 50ms late after event-loop suspension.
|
|
200
|
+
now += maxNativeTimeoutMillis + 50;
|
|
201
|
+
callbacks[0]();
|
|
202
|
+
|
|
203
|
+
// Only 50ms remains; do not add the elapsed 50ms again.
|
|
204
|
+
assertEqual(delays, [maxNativeTimeoutMillis, 50]);
|
|
205
|
+
assertEqual(callback.mock.callCount(), 0);
|
|
206
|
+
|
|
207
|
+
now += 50;
|
|
208
|
+
callbacks[1]();
|
|
209
|
+
|
|
210
|
+
assertEqual(callback.mock.callCount(), 1);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("rejects an invalid clock when scheduling a long timeout", () => {
|
|
214
|
+
const setTimeout = mock.method(
|
|
215
|
+
globalThis,
|
|
216
|
+
"setTimeout",
|
|
217
|
+
() => 1 as unknown as ReturnType<typeof globalThis.setTimeout>,
|
|
218
|
+
);
|
|
219
|
+
mock.method(Date, "now", () => -1);
|
|
220
|
+
|
|
221
|
+
assertThrowsWithCause(
|
|
222
|
+
() =>
|
|
223
|
+
createTime().setTimeout(
|
|
224
|
+
() => undefined,
|
|
225
|
+
PositiveMillis.orThrow(2 ** 31),
|
|
226
|
+
),
|
|
227
|
+
negativeMillisCause,
|
|
228
|
+
);
|
|
229
|
+
assertEqual(setTimeout.mock.callCount(), 0);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("rejects an invalid clock while processing a long timeout", () => {
|
|
233
|
+
const callbacks: Array<() => void> = [];
|
|
234
|
+
let now = 1000;
|
|
235
|
+
mock.method(Date, "now", () => now);
|
|
236
|
+
mock.method(
|
|
237
|
+
globalThis,
|
|
238
|
+
"setTimeout",
|
|
239
|
+
(scheduledCallback: () => void) => {
|
|
240
|
+
callbacks.push(scheduledCallback);
|
|
241
|
+
return callbacks.length as unknown as ReturnType<typeof setTimeout>;
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
createTime().setTimeout(
|
|
245
|
+
() => undefined,
|
|
246
|
+
PositiveMillis.orThrow(2 ** 31),
|
|
247
|
+
);
|
|
248
|
+
now = -1;
|
|
249
|
+
|
|
250
|
+
assertThrowsWithCause(() => callbacks[0](), negativeMillisCause);
|
|
251
|
+
assertLength(callbacks, 1);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("clearTimeout cancels the active chunk of a long delay", () => {
|
|
255
|
+
const maxNativeTimeoutMillis = 2 ** 31 - 1;
|
|
256
|
+
let now = 1000;
|
|
257
|
+
const callbacks: Array<() => void> = [];
|
|
258
|
+
const callback = mock.fn<() => void>();
|
|
259
|
+
const clearTimeout = mock.method(
|
|
260
|
+
globalThis,
|
|
261
|
+
"clearTimeout",
|
|
262
|
+
() => undefined,
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
mock.method(Date, "now", () => now);
|
|
266
|
+
mock.method(
|
|
267
|
+
globalThis,
|
|
268
|
+
"setTimeout",
|
|
269
|
+
(scheduledCallback: () => void) => {
|
|
270
|
+
callbacks.push(scheduledCallback);
|
|
271
|
+
return callbacks.length as unknown as ReturnType<typeof setTimeout>;
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const time = createTime();
|
|
276
|
+
const id = time.setTimeout(
|
|
277
|
+
callback,
|
|
278
|
+
PositiveMillis.orThrow(maxNativeTimeoutMillis + 100),
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
now += maxNativeTimeoutMillis;
|
|
282
|
+
callbacks[0]();
|
|
283
|
+
time.clearTimeout(id);
|
|
284
|
+
callbacks[1]();
|
|
285
|
+
|
|
286
|
+
assertLength(clearTimeout.mock.calls, 1);
|
|
287
|
+
assertEqual(clearTimeout.mock.calls[0].arguments[0], 2);
|
|
288
|
+
assertEqual(callback.mock.callCount(), 0);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("clearTimeout cancels a single native timeout", () => {
|
|
292
|
+
const callbacks: Array<() => void> = [];
|
|
293
|
+
const callback = mock.fn<() => void>();
|
|
294
|
+
const clearTimeout = mock.method(
|
|
295
|
+
globalThis,
|
|
296
|
+
"clearTimeout",
|
|
297
|
+
() => undefined,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
mock.method(
|
|
301
|
+
globalThis,
|
|
302
|
+
"setTimeout",
|
|
303
|
+
(scheduledCallback: () => void) => {
|
|
304
|
+
callbacks.push(scheduledCallback);
|
|
305
|
+
return callbacks.length as unknown as ReturnType<typeof setTimeout>;
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
const time = createTime();
|
|
310
|
+
const id = time.setTimeout(callback, "10ms");
|
|
311
|
+
|
|
312
|
+
time.clearTimeout(id);
|
|
313
|
+
callbacks[0]();
|
|
314
|
+
|
|
315
|
+
assertLength(clearTimeout.mock.calls, 1);
|
|
316
|
+
assertEqual(clearTimeout.mock.calls[0].arguments[0], 1);
|
|
317
|
+
assertEqual(callback.mock.callCount(), 0);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("clearTimeout rejects an id created by another Time instance", () => {
|
|
321
|
+
mock.method(
|
|
322
|
+
globalThis,
|
|
323
|
+
"setTimeout",
|
|
324
|
+
() => 1 as unknown as ReturnType<typeof setTimeout>,
|
|
325
|
+
);
|
|
326
|
+
const firstTime = createTime();
|
|
327
|
+
const secondTime = createTime();
|
|
328
|
+
const id = firstTime.setTimeout(() => undefined, "10ms");
|
|
329
|
+
|
|
330
|
+
const error = assertThrowsInstanceOf(
|
|
331
|
+
() => secondTime.clearTimeout(id),
|
|
332
|
+
Error,
|
|
333
|
+
);
|
|
334
|
+
assertTrue(
|
|
335
|
+
error.message.includes(
|
|
336
|
+
"TimeoutId was created by another Time instance",
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe("testCreateTime", () => {
|
|
344
|
+
it("advances time only when advance() is called", () => {
|
|
345
|
+
const time = testCreateTime();
|
|
346
|
+
|
|
347
|
+
assertEqual(time.now(), 0);
|
|
348
|
+
// Still 0, no auto-increment
|
|
349
|
+
assertEqual(time.now(), 0);
|
|
350
|
+
|
|
351
|
+
time.advance("1ms");
|
|
352
|
+
assertEqual(time.now(), 1);
|
|
353
|
+
|
|
354
|
+
time.advance("100ms");
|
|
355
|
+
assertEqual(time.now(), 101);
|
|
356
|
+
|
|
357
|
+
time.advance("1s");
|
|
358
|
+
assertEqual(time.now(), 1101);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("with autoIncrement returns monotonically increasing values", async () => {
|
|
362
|
+
const time = testCreateTime({ autoIncrement: "microtask" });
|
|
363
|
+
const first = time.now();
|
|
364
|
+
|
|
365
|
+
await Promise.resolve();
|
|
366
|
+
|
|
367
|
+
const second = time.now();
|
|
368
|
+
|
|
369
|
+
await Promise.resolve();
|
|
370
|
+
|
|
371
|
+
const third = time.now();
|
|
372
|
+
|
|
373
|
+
assertEqual(first, 0);
|
|
374
|
+
assertEqual(second, 1);
|
|
375
|
+
assertEqual(third, 2);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("with sync autoIncrement increments within the same turn", () => {
|
|
379
|
+
const time = testCreateTime({ autoIncrement: "sync" });
|
|
380
|
+
|
|
381
|
+
assertEqual(time.now(), 0);
|
|
382
|
+
assertEqual(time.now(), 1);
|
|
383
|
+
assertEqual(time.now("DateIso"), "1970-01-01T00:00:00.002Z");
|
|
384
|
+
assertEqual(time.now(), 3);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('now with "DateIso" respects autoIncrement', async () => {
|
|
388
|
+
const time = testCreateTime({
|
|
389
|
+
autoIncrement: "microtask",
|
|
390
|
+
startAt: Date.UTC(2026, 0, 28, 14, 30, 0, 0) as Millis,
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const first = time.now("DateIso");
|
|
394
|
+
|
|
395
|
+
await Promise.resolve();
|
|
396
|
+
|
|
397
|
+
const second = time.now("DateIso");
|
|
398
|
+
|
|
399
|
+
assertEqual(first, "2026-01-28T14:30:00.000Z");
|
|
400
|
+
assertEqual(second, "2026-01-28T14:30:00.001Z");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('now with "DateIso" returns ISO string for current time', () => {
|
|
404
|
+
const time = testCreateTime({
|
|
405
|
+
startAt: Date.UTC(2026, 0, 28, 14, 30, 0, 0) as Millis,
|
|
406
|
+
});
|
|
407
|
+
assertEqual(time.now("DateIso"), "2026-01-28T14:30:00.000Z");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("performance starts at its time origin and advances with time", () => {
|
|
411
|
+
const time = testCreateTime({ startAt: Millis.orThrow(1000) });
|
|
412
|
+
|
|
413
|
+
assertEqual(time.performance.timeOrigin, 1000);
|
|
414
|
+
assertEqual(time.performance.now(), 0);
|
|
415
|
+
|
|
416
|
+
time.advance("100ms");
|
|
417
|
+
|
|
418
|
+
assertEqual(time.performance.now(), 100);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("performance respects sync autoIncrement", () => {
|
|
422
|
+
const time = testCreateTime({ autoIncrement: "sync" });
|
|
423
|
+
|
|
424
|
+
assertEqual(time.performance.now(), 0);
|
|
425
|
+
assertEqual(time.now(), 1);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("setTimeout fires callback when time is advanced past deadline", () => {
|
|
429
|
+
const time = testCreateTime();
|
|
430
|
+
let called = false;
|
|
431
|
+
|
|
432
|
+
time.setTimeout(() => {
|
|
433
|
+
called = true;
|
|
434
|
+
}, "100ms");
|
|
435
|
+
|
|
436
|
+
assertFalse(called);
|
|
437
|
+
|
|
438
|
+
time.advance("50ms");
|
|
439
|
+
assertFalse(called);
|
|
440
|
+
|
|
441
|
+
time.advance("50ms");
|
|
442
|
+
assertTrue(called);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("setTimeout rejects a deadline after maxMillis", () => {
|
|
446
|
+
const time = testCreateTime({ startAt: maxMillis });
|
|
447
|
+
|
|
448
|
+
assertThrowsWithCause(() => time.setTimeout(() => undefined, "1ms"), {
|
|
449
|
+
type: "LessThan281474976710655",
|
|
450
|
+
value: maxMillis + 1,
|
|
451
|
+
max: maxMillis + 1,
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it("clearTimeout cancels pending timeout", () => {
|
|
456
|
+
const time = testCreateTime();
|
|
457
|
+
let called = false;
|
|
458
|
+
|
|
459
|
+
const id = time.setTimeout(() => {
|
|
460
|
+
called = true;
|
|
461
|
+
}, "100ms");
|
|
462
|
+
|
|
463
|
+
time.clearTimeout(id);
|
|
464
|
+
time.advance("200ms");
|
|
465
|
+
|
|
466
|
+
assertFalse(called);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("clearTimeout rejects an id created by another Time instance", () => {
|
|
470
|
+
const firstTime = testCreateTime();
|
|
471
|
+
const secondTime = testCreateTime();
|
|
472
|
+
const secondCallback = mock.fn<() => void>();
|
|
473
|
+
const id = firstTime.setTimeout(() => undefined, "100ms");
|
|
474
|
+
secondTime.setTimeout(secondCallback, "100ms");
|
|
475
|
+
|
|
476
|
+
const error = assertThrowsInstanceOf(
|
|
477
|
+
() => secondTime.clearTimeout(id),
|
|
478
|
+
Error,
|
|
479
|
+
);
|
|
480
|
+
assertTrue(
|
|
481
|
+
error.message.includes(
|
|
482
|
+
"TimeoutId was created by another Time instance",
|
|
483
|
+
),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
secondTime.advance("100ms");
|
|
487
|
+
assertEqual(secondCallback.mock.callCount(), 1);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("multiple timeouts fire in deadline order", () => {
|
|
491
|
+
const time = testCreateTime();
|
|
492
|
+
const order: Array<number> = [];
|
|
493
|
+
|
|
494
|
+
time.setTimeout(() => {
|
|
495
|
+
order.push(1);
|
|
496
|
+
}, "100ms");
|
|
497
|
+
time.setTimeout(() => {
|
|
498
|
+
order.push(2);
|
|
499
|
+
}, "50ms");
|
|
500
|
+
time.setTimeout(() => {
|
|
501
|
+
order.push(3);
|
|
502
|
+
}, "150ms");
|
|
503
|
+
|
|
504
|
+
time.advance("200ms");
|
|
505
|
+
|
|
506
|
+
assertEqual(order, [2, 1, 3]);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("timeouts with identical deadlines fire in scheduling order", () => {
|
|
510
|
+
const time = testCreateTime();
|
|
511
|
+
const order: Array<number> = [];
|
|
512
|
+
|
|
513
|
+
time.setTimeout(() => {
|
|
514
|
+
order.push(1);
|
|
515
|
+
}, "50ms");
|
|
516
|
+
time.setTimeout(() => {
|
|
517
|
+
order.push(2);
|
|
518
|
+
}, "50ms");
|
|
519
|
+
time.setTimeout(() => {
|
|
520
|
+
order.push(3);
|
|
521
|
+
}, "50ms");
|
|
522
|
+
|
|
523
|
+
time.advance("50ms");
|
|
524
|
+
|
|
525
|
+
assertEqual(order, [1, 2, 3]);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("timeout callbacks observe their deadlines", () => {
|
|
529
|
+
const time = testCreateTime();
|
|
530
|
+
const observedTimes: Array<Millis> = [];
|
|
531
|
+
|
|
532
|
+
time.setTimeout(() => {
|
|
533
|
+
observedTimes.push(time.now());
|
|
534
|
+
}, "100ms");
|
|
535
|
+
time.setTimeout(() => {
|
|
536
|
+
observedTimes.push(time.now());
|
|
537
|
+
}, "50ms");
|
|
538
|
+
|
|
539
|
+
time.advance("200ms");
|
|
540
|
+
|
|
541
|
+
assertEqual(observedTimes, [50, 100]);
|
|
542
|
+
assertEqual(time.now(), 200);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("advance fires timeouts scheduled by callbacks within its target", () => {
|
|
546
|
+
const time = testCreateTime();
|
|
547
|
+
const observedTimes: Array<Millis> = [];
|
|
548
|
+
|
|
549
|
+
time.setTimeout(() => {
|
|
550
|
+
observedTimes.push(time.now());
|
|
551
|
+
time.setTimeout(() => {
|
|
552
|
+
observedTimes.push(time.now());
|
|
553
|
+
}, "50ms");
|
|
554
|
+
}, "50ms");
|
|
555
|
+
|
|
556
|
+
time.advance("200ms");
|
|
557
|
+
|
|
558
|
+
assertEqual(observedTimes, [50, 100]);
|
|
559
|
+
assertEqual(time.now(), 200);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
it("an earlier timeout can cancel a later timeout", () => {
|
|
563
|
+
const time = testCreateTime();
|
|
564
|
+
const callback = mock.fn<() => void>();
|
|
565
|
+
const laterId = time.setTimeout(callback, "100ms");
|
|
566
|
+
time.setTimeout(() => time.clearTimeout(laterId), "50ms");
|
|
567
|
+
|
|
568
|
+
time.advance("200ms");
|
|
569
|
+
|
|
570
|
+
assertEqual(callback.mock.callCount(), 0);
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
it("a throwing callback aborts advance at its deadline", () => {
|
|
574
|
+
const time = testCreateTime();
|
|
575
|
+
const error = new Error("callback failed");
|
|
576
|
+
const laterCallback = mock.fn<() => void>();
|
|
577
|
+
|
|
578
|
+
time.setTimeout(() => {
|
|
579
|
+
throw error;
|
|
580
|
+
}, "50ms");
|
|
581
|
+
time.setTimeout(laterCallback, "100ms");
|
|
582
|
+
|
|
583
|
+
assertThrowsSame(() => time.advance("200ms"), error);
|
|
584
|
+
assertEqual(time.now(), 50);
|
|
585
|
+
assertEqual(laterCallback.mock.callCount(), 0);
|
|
586
|
+
|
|
587
|
+
time.advance("150ms");
|
|
588
|
+
|
|
589
|
+
assertEqual(laterCallback.mock.callCount(), 1);
|
|
590
|
+
assertEqual(time.now(), 200);
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("advance rejects reentrant calls", () => {
|
|
594
|
+
const time = testCreateTime();
|
|
595
|
+
|
|
596
|
+
time.setTimeout(() => {
|
|
597
|
+
const error = assertThrowsInstanceOf(() => time.advance("1ms"), Error);
|
|
598
|
+
assertTrue(
|
|
599
|
+
error.message.includes(
|
|
600
|
+
"TestTime.advance cannot be called while advancing",
|
|
601
|
+
),
|
|
602
|
+
);
|
|
603
|
+
}, "50ms");
|
|
604
|
+
|
|
605
|
+
time.advance("100ms");
|
|
606
|
+
|
|
607
|
+
assertEqual(time.now(), 100);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("advance preserves auto-incremented time", () => {
|
|
611
|
+
const time = testCreateTime({ autoIncrement: "sync" });
|
|
612
|
+
const observedTimes: Array<Millis> = [];
|
|
613
|
+
|
|
614
|
+
time.setTimeout(() => {
|
|
615
|
+
observedTimes.push(time.now());
|
|
616
|
+
}, "50ms");
|
|
617
|
+
time.setTimeout(() => {
|
|
618
|
+
observedTimes.push(time.now());
|
|
619
|
+
}, "50ms");
|
|
620
|
+
|
|
621
|
+
time.advance("50ms");
|
|
622
|
+
|
|
623
|
+
assertEqual(observedTimes, [50, 51]);
|
|
624
|
+
assertEqual(time.now(), 52);
|
|
625
|
+
});
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
describe("PositiveMillis", () => {
|
|
629
|
+
it("accepts only positive millis", () => {
|
|
630
|
+
const millis: Millis = PositiveMillis.orThrow(1);
|
|
631
|
+
assertType<typeof millis, Millis>();
|
|
632
|
+
assertTrue(PositiveMillis.is(1));
|
|
633
|
+
assertTrue(PositiveMillis.is(maxMillis));
|
|
634
|
+
assertFalse(PositiveMillis.is(0));
|
|
635
|
+
});
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
describe("saturateMillis", () => {
|
|
639
|
+
it("requires NonNaNNumber", () => {
|
|
640
|
+
saturateMillis(NonNaNNumber.orThrow(1));
|
|
641
|
+
// @ts-expect-error - Numbers require validation.
|
|
642
|
+
saturateMillis(1);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it("rounds to the nearest millisecond", () => {
|
|
646
|
+
assertEqual(saturateMillis(NonNaNNumber.orThrow(1.4)), 1);
|
|
647
|
+
assertEqual(saturateMillis(NonNaNNumber.orThrow(1.5)), 2);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it("saturates negative values at min millis", () => {
|
|
651
|
+
assertEqual(saturateMillis(NonNaNNumber.orThrow(-1)), 0);
|
|
652
|
+
assertEqual(
|
|
653
|
+
saturateMillis(NonNaNNumber.orThrow(Number.NEGATIVE_INFINITY)),
|
|
654
|
+
0,
|
|
655
|
+
);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("saturates overflow at maxMillis", () => {
|
|
659
|
+
assertEqual(
|
|
660
|
+
saturateMillis(NonNaNNumber.orThrow(maxMillis + 1)),
|
|
661
|
+
maxMillis,
|
|
662
|
+
);
|
|
663
|
+
assertEqual(
|
|
664
|
+
saturateMillis(NonNaNNumber.orThrow(Number.POSITIVE_INFINITY)),
|
|
665
|
+
maxMillis,
|
|
666
|
+
);
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
describe("millisToDateIso", () => {
|
|
671
|
+
it("millisToDateIso returns current time as ISO string", () => {
|
|
672
|
+
const time = createTime();
|
|
673
|
+
const result = millisToDateIso(time.now());
|
|
674
|
+
// Verify it's a valid ISO string
|
|
675
|
+
assertTrue(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(result));
|
|
676
|
+
// And it's close to now
|
|
677
|
+
const parsed = Date.parse(result);
|
|
678
|
+
assertTrue(parsed >= Date.now() - 100);
|
|
679
|
+
assertTrue(parsed <= Date.now() + 100);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("millisToDateIso returns ISO string for current time", () => {
|
|
683
|
+
const time = testCreateTime({
|
|
684
|
+
startAt: Date.UTC(2026, 0, 28, 14, 30, 0, 0) as Millis,
|
|
685
|
+
});
|
|
686
|
+
assertEqual(millisToDateIso(time.now()), "2026-01-28T14:30:00.000Z");
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
describe("performanceDurationBetween", () => {
|
|
691
|
+
it("preserves fractional milliseconds", () => {
|
|
692
|
+
const result = performanceDurationBetween(
|
|
693
|
+
100.125 as PerformanceTime,
|
|
694
|
+
100.375 as PerformanceTime,
|
|
695
|
+
);
|
|
696
|
+
|
|
697
|
+
assertType<typeof result, PerformanceDuration>();
|
|
698
|
+
assertEqual(result, 0.25);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
it("rejects an end time before the start time", () => {
|
|
702
|
+
const error = assertThrowsInstanceOf(
|
|
703
|
+
() =>
|
|
704
|
+
performanceDurationBetween(
|
|
705
|
+
100.375 as PerformanceTime,
|
|
706
|
+
100.125 as PerformanceTime,
|
|
707
|
+
),
|
|
708
|
+
Error,
|
|
709
|
+
);
|
|
710
|
+
assertTrue(
|
|
711
|
+
error.message.includes(
|
|
712
|
+
"Performance end time must not precede start time",
|
|
713
|
+
),
|
|
714
|
+
);
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
describe("PositiveDuration", () => {
|
|
719
|
+
it("accepts only positive durations", () => {
|
|
720
|
+
const time = testCreateTime();
|
|
721
|
+
const duration: PositiveDuration = PositiveMillis.orThrow(1);
|
|
722
|
+
|
|
723
|
+
time.setTimeout(() => undefined, duration);
|
|
724
|
+
// @ts-expect-error - Zero Millis is not a positive duration.
|
|
725
|
+
time.setTimeout(() => undefined, 0 as Millis);
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
describe("DurationLiteral", () => {
|
|
730
|
+
it("valid durations", () => {
|
|
731
|
+
// Milliseconds
|
|
732
|
+
assertType<"1ms" extends DurationLiteral ? true : false, true>();
|
|
733
|
+
assertType<"500ms" extends DurationLiteral ? true : false, true>();
|
|
734
|
+
assertType<"999ms" extends DurationLiteral ? true : false, true>();
|
|
735
|
+
// Seconds (integer and decimal)
|
|
736
|
+
assertType<"1s" extends DurationLiteral ? true : false, true>();
|
|
737
|
+
assertType<"59s" extends DurationLiteral ? true : false, true>();
|
|
738
|
+
assertType<"1.5s" extends DurationLiteral ? true : false, true>();
|
|
739
|
+
assertType<"59.9s" extends DurationLiteral ? true : false, true>();
|
|
740
|
+
// Minutes (integer and decimal)
|
|
741
|
+
assertType<"1m" extends DurationLiteral ? true : false, true>();
|
|
742
|
+
assertType<"59m" extends DurationLiteral ? true : false, true>();
|
|
743
|
+
assertType<"1.5m" extends DurationLiteral ? true : false, true>();
|
|
744
|
+
// Hours (integer and decimal)
|
|
745
|
+
assertType<"1h" extends DurationLiteral ? true : false, true>();
|
|
746
|
+
assertType<"23h" extends DurationLiteral ? true : false, true>();
|
|
747
|
+
assertType<"1.5h" extends DurationLiteral ? true : false, true>();
|
|
748
|
+
// Days (integer and decimal, max 6)
|
|
749
|
+
assertType<"1d" extends DurationLiteral ? true : false, true>();
|
|
750
|
+
assertType<"6d" extends DurationLiteral ? true : false, true>();
|
|
751
|
+
assertType<"1.5d" extends DurationLiteral ? true : false, true>();
|
|
752
|
+
// Weeks (integer and decimal)
|
|
753
|
+
assertType<"1w" extends DurationLiteral ? true : false, true>();
|
|
754
|
+
assertType<"51w" extends DurationLiteral ? true : false, true>();
|
|
755
|
+
assertType<"1.5w" extends DurationLiteral ? true : false, true>();
|
|
756
|
+
// Years (integer and decimal)
|
|
757
|
+
assertType<"1y" extends DurationLiteral ? true : false, true>();
|
|
758
|
+
assertType<"99y" extends DurationLiteral ? true : false, true>();
|
|
759
|
+
assertType<"1.5y" extends DurationLiteral ? true : false, true>();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("invalid durations", () => {
|
|
763
|
+
assertType<"invalid" extends DurationLiteral ? true : false, false>();
|
|
764
|
+
assertType<"-1s" extends DurationLiteral ? true : false, false>();
|
|
765
|
+
assertType<"0ms" extends DurationLiteral ? true : false, false>();
|
|
766
|
+
assertType<"0s" extends DurationLiteral ? true : false, false>();
|
|
767
|
+
assertType<"0.5s" extends DurationLiteral ? true : false, false>();
|
|
768
|
+
assertType<"01d" extends DurationLiteral ? true : false, false>();
|
|
769
|
+
assertType<"60s" extends DurationLiteral ? true : false, false>();
|
|
770
|
+
assertType<"60m" extends DurationLiteral ? true : false, false>();
|
|
771
|
+
assertType<"24h" extends DurationLiteral ? true : false, false>();
|
|
772
|
+
assertType<"7d" extends DurationLiteral ? true : false, false>();
|
|
773
|
+
assertType<"52w" extends DurationLiteral ? true : false, false>();
|
|
774
|
+
assertType<"100y" extends DurationLiteral ? true : false, false>();
|
|
775
|
+
assertType<"1000ms" extends DurationLiteral ? true : false, false>();
|
|
776
|
+
assertType<"1.0s" extends DurationLiteral ? true : false, false>();
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
it("validates durations at runtime", () => {
|
|
780
|
+
assertTrue(DurationLiteralMilliseconds.is("999ms"));
|
|
781
|
+
assertFalse(DurationLiteralMilliseconds.is("1000ms"));
|
|
782
|
+
assertTrue(DurationLiteralSeconds.is("59.9s"));
|
|
783
|
+
assertFalse(DurationLiteralSeconds.is("60s"));
|
|
784
|
+
assertTrue(DurationLiteralMinutes.is("59.9m"));
|
|
785
|
+
assertFalse(DurationLiteralMinutes.is("60m"));
|
|
786
|
+
assertTrue(DurationLiteralHours.is("23.9h"));
|
|
787
|
+
assertFalse(DurationLiteralHours.is("24h"));
|
|
788
|
+
assertTrue(DurationLiteralDays.is("6.9d"));
|
|
789
|
+
assertFalse(DurationLiteralDays.is("7d"));
|
|
790
|
+
assertTrue(DurationLiteralWeeks.is("51.9w"));
|
|
791
|
+
assertFalse(DurationLiteralWeeks.is("52w"));
|
|
792
|
+
assertTrue(DurationLiteralYears.is("99.9y"));
|
|
793
|
+
assertFalse(DurationLiteralYears.is("100y"));
|
|
794
|
+
assertTrue(DurationLiteral.is("1.5s"));
|
|
795
|
+
assertFalse(DurationLiteral.is("0.5s"));
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
describe("durationToMillis", () => {
|
|
800
|
+
it("preserves positive duration type", () => {
|
|
801
|
+
{
|
|
802
|
+
const actual = durationToMillis("1ms");
|
|
803
|
+
assertType<typeof actual, PositiveMillis>();
|
|
804
|
+
}
|
|
805
|
+
{
|
|
806
|
+
const actual = durationToMillis(PositiveMillis.orThrow(1));
|
|
807
|
+
assertType<typeof actual, PositiveMillis>();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const duration: Duration = 0 as Millis;
|
|
811
|
+
{
|
|
812
|
+
const actual = durationToMillis(duration);
|
|
813
|
+
assertType<typeof actual, Millis>();
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it("converts DurationLiteral to milliseconds", () => {
|
|
818
|
+
// Milliseconds
|
|
819
|
+
assertEqual(durationToMillis("1ms"), 1);
|
|
820
|
+
assertEqual(durationToMillis("500ms"), 500);
|
|
821
|
+
assertEqual(durationToMillis("999ms"), 999);
|
|
822
|
+
// Seconds (integer and decimal)
|
|
823
|
+
assertEqual(durationToMillis("1s"), 1000);
|
|
824
|
+
assertEqual(durationToMillis("30s"), 30000);
|
|
825
|
+
assertEqual(durationToMillis("59s"), 59000);
|
|
826
|
+
assertEqual(durationToMillis("1.5s"), 1500);
|
|
827
|
+
// Minutes (integer and decimal)
|
|
828
|
+
assertEqual(durationToMillis("1m"), 60000);
|
|
829
|
+
assertEqual(durationToMillis("30m"), 30 * 60000);
|
|
830
|
+
assertEqual(durationToMillis("1.5m"), 90000);
|
|
831
|
+
// Hours (integer and decimal)
|
|
832
|
+
assertEqual(durationToMillis("1h"), 3600000);
|
|
833
|
+
assertEqual(durationToMillis("23h"), 23 * 3600000);
|
|
834
|
+
assertEqual(durationToMillis("1.5h"), 5400000);
|
|
835
|
+
// Days (integer and decimal, max 6)
|
|
836
|
+
assertEqual(durationToMillis("1d"), 86400000);
|
|
837
|
+
assertEqual(durationToMillis("6d"), 6 * 86400000);
|
|
838
|
+
assertEqual(durationToMillis("1.5d"), 129600000);
|
|
839
|
+
// Weeks (integer and decimal)
|
|
840
|
+
assertEqual(durationToMillis("1w"), 604800000);
|
|
841
|
+
assertEqual(durationToMillis("51w"), 51 * 604800000);
|
|
842
|
+
assertEqual(durationToMillis("1.5w"), 907200000);
|
|
843
|
+
// Years (integer and decimal)
|
|
844
|
+
assertEqual(durationToMillis("1y"), 31536000000);
|
|
845
|
+
assertEqual(durationToMillis("99y"), 99 * 31536000000);
|
|
846
|
+
assertEqual(durationToMillis("1.5y"), 47304000000);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
it("passes through Millis unchanged", () => {
|
|
850
|
+
assertEqual(durationToMillis(0 as Millis), 0);
|
|
851
|
+
assertEqual(durationToMillis(5000 as Millis), 5000);
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
describe("formatMillisAsDuration", () => {
|
|
856
|
+
it("formats sub-minute durations", () => {
|
|
857
|
+
assertEqual(formatMillisAsDuration(0 as Millis), "0.000s");
|
|
858
|
+
assertEqual(formatMillisAsDuration(1 as Millis), "0.001s");
|
|
859
|
+
assertEqual(formatMillisAsDuration(1234 as Millis), "1.234s");
|
|
860
|
+
assertEqual(formatMillisAsDuration(59999 as Millis), "59.999s");
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it("formats minute-range durations", () => {
|
|
864
|
+
assertEqual(formatMillisAsDuration(60000 as Millis), "1m0.000s");
|
|
865
|
+
assertEqual(formatMillisAsDuration(90000 as Millis), "1m30.000s");
|
|
866
|
+
assertEqual(formatMillisAsDuration(3599999 as Millis), "59m59.999s");
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
it("formats hour-range durations", () => {
|
|
870
|
+
assertEqual(formatMillisAsDuration(3600000 as Millis), "1h0m0.000s");
|
|
871
|
+
assertEqual(formatMillisAsDuration(3661000 as Millis), "1h1m1.000s");
|
|
872
|
+
assertEqual(formatMillisAsDuration(5400000 as Millis), "1h30m0.000s");
|
|
873
|
+
assertEqual(formatMillisAsDuration(86399999 as Millis), "23h59m59.999s");
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it("formats day, week, and year durations", () => {
|
|
877
|
+
assertEqual(
|
|
878
|
+
formatMillisAsDuration(durationToMillis("1d")),
|
|
879
|
+
"1d0h0m0.000s",
|
|
880
|
+
);
|
|
881
|
+
assertEqual(
|
|
882
|
+
formatMillisAsDuration(durationToMillis("1w")),
|
|
883
|
+
"1w0d0h0m0.000s",
|
|
884
|
+
);
|
|
885
|
+
assertEqual(
|
|
886
|
+
formatMillisAsDuration(durationToMillis("1y")),
|
|
887
|
+
"1y0w0d0h0m0.000s",
|
|
888
|
+
);
|
|
889
|
+
assertEqual(
|
|
890
|
+
formatMillisAsDuration(
|
|
891
|
+
Millis.orThrow(
|
|
892
|
+
durationToMillis("1y") +
|
|
893
|
+
2 * durationToMillis("1w") +
|
|
894
|
+
3 * durationToMillis("1d") +
|
|
895
|
+
durationToMillis("4h") +
|
|
896
|
+
durationToMillis("5m") +
|
|
897
|
+
durationToMillis("6s"),
|
|
898
|
+
),
|
|
899
|
+
),
|
|
900
|
+
"1y2w3d4h5m6.000s",
|
|
901
|
+
);
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
describe("formatMillisAsClockTime", () => {
|
|
906
|
+
it("formats local time as HH:MM:SS.mmm", () => {
|
|
907
|
+
const timestamp = new Date(
|
|
908
|
+
2026,
|
|
909
|
+
0,
|
|
910
|
+
28,
|
|
911
|
+
14,
|
|
912
|
+
32,
|
|
913
|
+
15,
|
|
914
|
+
234,
|
|
915
|
+
).getTime() as Millis;
|
|
916
|
+
|
|
917
|
+
assertEqual(formatMillisAsClockTime(timestamp), "14:32:15.234");
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it("pads single digits", () => {
|
|
921
|
+
const timestamp = new Date(2026, 0, 1, 0, 1, 2, 3).getTime() as Millis;
|
|
922
|
+
|
|
923
|
+
assertEqual(formatMillisAsClockTime(timestamp), "00:01:02.003");
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
});
|