@evolu/common 8.0.0-next.4 → 8.0.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.
Files changed (237) hide show
  1. package/dist/src/Array.d.ts +279 -173
  2. package/dist/src/Array.d.ts.map +1 -1
  3. package/dist/src/Array.js +105 -101
  4. package/dist/src/Assert.d.ts +44 -44
  5. package/dist/src/Assert.d.ts.map +1 -1
  6. package/dist/src/Assert.js +44 -32
  7. package/dist/src/BigInt.d.ts +6 -3
  8. package/dist/src/BigInt.d.ts.map +1 -1
  9. package/dist/src/BigInt.js +5 -2
  10. package/dist/src/Brand.d.ts +27 -40
  11. package/dist/src/Brand.d.ts.map +1 -1
  12. package/dist/src/Buffer.d.ts +27 -11
  13. package/dist/src/Buffer.d.ts.map +1 -1
  14. package/dist/src/Cache.d.ts +9 -4
  15. package/dist/src/Cache.d.ts.map +1 -1
  16. package/dist/src/Cache.js +8 -3
  17. package/dist/src/Callbacks.d.ts +30 -19
  18. package/dist/src/Callbacks.d.ts.map +1 -1
  19. package/dist/src/Console.d.ts +145 -66
  20. package/dist/src/Console.d.ts.map +1 -1
  21. package/dist/src/Console.js +91 -43
  22. package/dist/src/Crypto.d.ts +85 -42
  23. package/dist/src/Crypto.d.ts.map +1 -1
  24. package/dist/src/Crypto.js +44 -20
  25. package/dist/src/Eq.d.ts +87 -21
  26. package/dist/src/Eq.d.ts.map +1 -1
  27. package/dist/src/Eq.js +67 -19
  28. package/dist/src/Error.d.ts +5 -3
  29. package/dist/src/Error.d.ts.map +1 -1
  30. package/dist/src/Error.js +16 -0
  31. package/dist/src/Function.d.ts +119 -58
  32. package/dist/src/Function.d.ts.map +1 -1
  33. package/dist/src/Function.js +86 -43
  34. package/dist/src/Http.d.ts +394 -0
  35. package/dist/src/Http.d.ts.map +1 -0
  36. package/dist/src/Http.js +102 -0
  37. package/dist/src/Identicon.d.ts +16 -7
  38. package/dist/src/Identicon.d.ts.map +1 -1
  39. package/dist/src/Identicon.js +14 -5
  40. package/dist/src/LeakDetector.d.ts +88 -0
  41. package/dist/src/LeakDetector.d.ts.map +1 -0
  42. package/dist/src/LeakDetector.js +86 -0
  43. package/dist/src/LockManager.d.ts +18 -9
  44. package/dist/src/LockManager.d.ts.map +1 -1
  45. package/dist/src/LockManager.js +7 -8
  46. package/dist/src/Lookup.d.ts +43 -13
  47. package/dist/src/Lookup.d.ts.map +1 -1
  48. package/dist/src/Lookup.js +28 -8
  49. package/dist/src/Microtask.d.ts +1 -1
  50. package/dist/src/Microtask.d.ts.map +1 -1
  51. package/dist/src/Microtask.js +3 -7
  52. package/dist/src/Number.d.ts +49 -13
  53. package/dist/src/Number.d.ts.map +1 -1
  54. package/dist/src/Number.js +34 -5
  55. package/dist/src/Object.d.ts +160 -40
  56. package/dist/src/Object.d.ts.map +1 -1
  57. package/dist/src/Object.js +92 -46
  58. package/dist/src/Option.d.ts +10 -7
  59. package/dist/src/Option.d.ts.map +1 -1
  60. package/dist/src/Option.js +9 -6
  61. package/dist/src/Order.d.ts +21 -21
  62. package/dist/src/Order.d.ts.map +1 -1
  63. package/dist/src/Order.js +18 -21
  64. package/dist/src/Platform.d.ts +31 -0
  65. package/dist/src/Platform.d.ts.map +1 -1
  66. package/dist/src/Platform.js +98 -0
  67. package/dist/src/Random.d.ts +32 -17
  68. package/dist/src/Random.d.ts.map +1 -1
  69. package/dist/src/Random.js +12 -8
  70. package/dist/src/Redacted.d.ts +43 -29
  71. package/dist/src/Redacted.d.ts.map +1 -1
  72. package/dist/src/Redacted.js +14 -3
  73. package/dist/src/Ref.d.ts +11 -3
  74. package/dist/src/Ref.d.ts.map +1 -1
  75. package/dist/src/Ref.js +2 -2
  76. package/dist/src/RefCount.d.ts +2 -6
  77. package/dist/src/RefCount.d.ts.map +1 -1
  78. package/dist/src/RefCount.js +33 -90
  79. package/dist/src/Relation.d.ts +70 -5
  80. package/dist/src/Relation.d.ts.map +1 -1
  81. package/dist/src/Relation.js +77 -2
  82. package/dist/src/Resource.d.ts +590 -155
  83. package/dist/src/Resource.d.ts.map +1 -1
  84. package/dist/src/Resource.js +592 -343
  85. package/dist/src/Result.d.ts +838 -191
  86. package/dist/src/Result.d.ts.map +1 -1
  87. package/dist/src/Result.js +163 -102
  88. package/dist/src/Schedule.d.ts +684 -231
  89. package/dist/src/Schedule.d.ts.map +1 -1
  90. package/dist/src/Schedule.js +766 -295
  91. package/dist/src/Set.d.ts +89 -40
  92. package/dist/src/Set.d.ts.map +1 -1
  93. package/dist/src/Set.js +50 -29
  94. package/dist/src/Sqlite.d.ts +29 -72
  95. package/dist/src/Sqlite.d.ts.map +1 -1
  96. package/dist/src/Sqlite.js +41 -46
  97. package/dist/src/StackTrace.d.ts +40 -0
  98. package/dist/src/StackTrace.d.ts.map +1 -0
  99. package/dist/src/StackTrace.js +140 -0
  100. package/dist/src/Store.d.ts +5 -4
  101. package/dist/src/Store.d.ts.map +1 -1
  102. package/dist/src/Store.js +5 -4
  103. package/dist/src/String.d.ts +7 -1
  104. package/dist/src/String.d.ts.map +1 -1
  105. package/dist/src/String.js +20 -7
  106. package/dist/src/Task.d.ts +3572 -1770
  107. package/dist/src/Task.d.ts.map +1 -1
  108. package/dist/src/Task.js +1879 -1274
  109. package/dist/src/Test.d.ts +17 -125
  110. package/dist/src/Test.d.ts.map +1 -1
  111. package/dist/src/Test.js +17 -68
  112. package/dist/src/Time.d.ts +108 -49
  113. package/dist/src/Time.d.ts.map +1 -1
  114. package/dist/src/Time.js +172 -73
  115. package/dist/src/Type.d.ts +3542 -1926
  116. package/dist/src/Type.d.ts.map +1 -1
  117. package/dist/src/Type.js +3070 -1784
  118. package/dist/src/Types.d.ts +200 -108
  119. package/dist/src/Types.d.ts.map +1 -1
  120. package/dist/src/Types.js +39 -8
  121. package/dist/src/WebSocket.d.ts +48 -23
  122. package/dist/src/WebSocket.d.ts.map +1 -1
  123. package/dist/src/WebSocket.js +15 -6
  124. package/dist/src/Worker.d.ts +94 -36
  125. package/dist/src/Worker.d.ts.map +1 -1
  126. package/dist/src/Worker.js +58 -24
  127. package/dist/src/index.d.ts +55 -52
  128. package/dist/src/index.d.ts.map +1 -1
  129. package/dist/src/index.js +3 -1
  130. package/dist/src/intl/cs.d.ts +94 -0
  131. package/dist/src/intl/cs.d.ts.map +1 -0
  132. package/dist/src/intl/cs.js +220 -0
  133. package/dist/src/intl/index.d.ts +7 -0
  134. package/dist/src/intl/index.d.ts.map +1 -0
  135. package/dist/src/intl/index.js +6 -0
  136. package/dist/src/local-first/Db.d.ts +12 -8
  137. package/dist/src/local-first/Db.d.ts.map +1 -1
  138. package/dist/src/local-first/Db.js +109 -132
  139. package/dist/src/local-first/Error.d.ts +4 -4
  140. package/dist/src/local-first/Evolu.d.ts +338 -92
  141. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  142. package/dist/src/local-first/Evolu.js +47 -55
  143. package/dist/src/local-first/LocalAuth.d.ts +4 -4
  144. package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
  145. package/dist/src/local-first/Owner.d.ts +61 -24
  146. package/dist/src/local-first/Owner.d.ts.map +1 -1
  147. package/dist/src/local-first/Owner.js +45 -8
  148. package/dist/src/local-first/Protocol.d.ts +44 -31
  149. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  150. package/dist/src/local-first/Protocol.js +36 -13
  151. package/dist/src/local-first/Query.d.ts +147 -62
  152. package/dist/src/local-first/Query.d.ts.map +1 -1
  153. package/dist/src/local-first/Query.js +112 -58
  154. package/dist/src/local-first/Relay.d.ts +44 -14
  155. package/dist/src/local-first/Relay.d.ts.map +1 -1
  156. package/dist/src/local-first/Schema.d.ts +92 -41
  157. package/dist/src/local-first/Schema.d.ts.map +1 -1
  158. package/dist/src/local-first/Schema.js +25 -8
  159. package/dist/src/local-first/Shared.d.ts +23 -21
  160. package/dist/src/local-first/Shared.d.ts.map +1 -1
  161. package/dist/src/local-first/Shared.js +100 -57
  162. package/dist/src/local-first/Storage.d.ts +53 -32
  163. package/dist/src/local-first/Storage.d.ts.map +1 -1
  164. package/dist/src/local-first/Storage.js +194 -234
  165. package/dist/src/local-first/Timestamp.d.ts +21 -21
  166. package/dist/src/local-first/Timestamp.d.ts.map +1 -1
  167. package/dist/src/local-first/Timestamp.js +2 -2
  168. package/dist/src/local-first/index.d.ts +11 -11
  169. package/package.json +14 -13
  170. package/src/Array.ts +306 -205
  171. package/src/Assert.ts +54 -69
  172. package/src/BigInt.ts +6 -3
  173. package/src/Brand.ts +27 -40
  174. package/src/Buffer.ts +28 -12
  175. package/src/Cache.ts +9 -4
  176. package/src/Callbacks.ts +30 -19
  177. package/src/Console.ts +156 -84
  178. package/src/Crypto.ts +83 -41
  179. package/src/Eq.ts +95 -22
  180. package/src/Error.ts +8 -4
  181. package/src/Function.ts +167 -59
  182. package/src/Http.ts +549 -0
  183. package/src/Identicon.ts +16 -7
  184. package/src/LeakDetector.ts +182 -0
  185. package/src/LockManager.ts +43 -39
  186. package/src/Lookup.ts +62 -28
  187. package/src/Microtask.ts +19 -23
  188. package/src/Number.ts +64 -12
  189. package/src/Object.ts +173 -49
  190. package/src/Option.ts +10 -7
  191. package/src/Order.ts +23 -21
  192. package/src/Platform.ts +136 -0
  193. package/src/Polyfills.ts +6 -12
  194. package/src/Random.ts +32 -19
  195. package/src/Redacted.ts +44 -30
  196. package/src/Ref.ts +14 -6
  197. package/src/RefCount.ts +38 -80
  198. package/src/Relation.ts +202 -10
  199. package/src/Resource.ts +1199 -591
  200. package/src/Result.ts +930 -271
  201. package/src/Schedule.ts +859 -343
  202. package/src/Set.ts +89 -40
  203. package/src/Sqlite.ts +144 -138
  204. package/src/StackTrace.ts +230 -0
  205. package/src/Store.ts +8 -7
  206. package/src/String.ts +22 -5
  207. package/src/Task.ts +5101 -2832
  208. package/src/Test.ts +20 -168
  209. package/src/Time.ts +293 -90
  210. package/src/Type.ts +10034 -4117
  211. package/src/Types.ts +227 -110
  212. package/src/WebSocket.ts +64 -36
  213. package/src/Worker.ts +180 -72
  214. package/src/index.ts +55 -52
  215. package/src/intl/cs.ts +388 -0
  216. package/src/intl/index.ts +7 -0
  217. package/src/local-first/Db.ts +152 -179
  218. package/src/local-first/Error.ts +4 -4
  219. package/src/local-first/Evolu.ts +495 -250
  220. package/src/local-first/LocalAuth.ts +6 -12
  221. package/src/local-first/Owner.ts +58 -21
  222. package/src/local-first/Protocol.ts +49 -31
  223. package/src/local-first/Query.ts +146 -61
  224. package/src/local-first/Relay.ts +51 -21
  225. package/src/local-first/Schema.ts +109 -51
  226. package/src/local-first/Shared.ts +266 -226
  227. package/src/local-first/Storage.ts +266 -264
  228. package/src/local-first/Timestamp.ts +25 -18
  229. package/src/local-first/index.ts +11 -11
  230. package/dist/src/Skiplist.d.ts +0 -28
  231. package/dist/src/Skiplist.d.ts.map +0 -1
  232. package/dist/src/Skiplist.js +0 -64
  233. package/dist/src/Tracer.d.ts +0 -48
  234. package/dist/src/Tracer.d.ts.map +0 -1
  235. package/dist/src/Tracer.js +0 -6
  236. package/src/Skiplist.ts +0 -108
  237. package/src/Tracer.ts +0 -54
package/src/Time.ts CHANGED
@@ -4,10 +4,17 @@
4
4
  * @module
5
5
  */
6
6
 
7
- import { assert } from "./Assert.js";
8
- import type { Brand } from "./Brand.js";
9
- import type { yieldNow } from "./Task.js";
10
- import { brand, type DateIso, lessThan, NonNegativeInt } from "./Type.js";
7
+ import { assert } from "./Assert.ts";
8
+ import type { Brand } from "./Brand.ts";
9
+ import type { yieldNow } from "./Task.ts";
10
+ import {
11
+ brand,
12
+ type DateIso,
13
+ lessThan,
14
+ type NonNaNNumber,
15
+ NonNegativeInt,
16
+ positive,
17
+ } from "./Type.ts";
11
18
  import type {
12
19
  Digit,
13
20
  Digit1To23,
@@ -16,20 +23,43 @@ import type {
16
23
  Digit1To6,
17
24
  Digit1To9,
18
25
  Digit1To99,
19
- } from "./Types.js";
26
+ } from "./Types.ts";
20
27
 
21
28
  /** Time and timer operations. */
22
29
  export interface Time {
23
- /** Returns current time as Unix epoch milliseconds. */
24
- readonly now: () => Millis;
30
+ readonly now: {
31
+ /** Returns current time as Unix epoch milliseconds. */
32
+ (): Millis;
25
33
 
26
- /** Returns current time as an ISO 8601 UTC string. */
27
- readonly nowDateIso: () => DateIso;
34
+ /** Returns current time as an ISO 8601 UTC string. */
35
+ (type: "DateIso"): DateIso;
36
+ };
37
+
38
+ readonly performance: {
39
+ /** Unix epoch timestamp used as the origin for `performance.now()`. */
40
+ readonly timeOrigin: PerformanceTimeOrigin;
41
+
42
+ /**
43
+ * Returns a high-resolution timestamp in milliseconds relative to
44
+ * `performance.timeOrigin`.
45
+ *
46
+ * Unlike {@link Time.now}, this value is not Unix epoch time and is
47
+ * unaffected by system clock adjustments. Like Node.js
48
+ * `process.hrtime.bigint()`, it is suitable for measuring elapsed time, but
49
+ * it uses the Web Performance API's number representation rather than
50
+ * nanoseconds as a bigint.
51
+ */
52
+ readonly now: () => PerformanceTime;
53
+ };
28
54
 
29
- /** Schedules a callback after the specified delay. */
30
- readonly setTimeout: (fn: () => void, delay: Duration) => TimeoutId;
55
+ /** Schedules a callback after the specified positive delay. */
56
+ readonly setTimeout: (fn: () => void, delay: PositiveDuration) => TimeoutId;
31
57
 
32
- /** Cancels a timeout scheduled with {@link Time.setTimeout}. */
58
+ /**
59
+ * Cancels a timeout scheduled with this instance's {@link Time.setTimeout}.
60
+ *
61
+ * Throws if the timeout was scheduled by another {@link Time} instance.
62
+ */
33
63
  readonly clearTimeout: (id: TimeoutId) => void;
34
64
  }
35
65
 
@@ -44,39 +74,127 @@ export interface TimeDep {
44
74
  */
45
75
  export type TimeoutId = Brand<"TimeoutId">;
46
76
 
77
+ interface TimeoutIdInternal {
78
+ readonly owner: symbol;
79
+ readonly clear: () => void;
80
+ }
81
+
47
82
  /**
48
- * Creates a {@link Time} using `Date.now()` and `globalThis.setTimeout`.
83
+ * Creates a {@link Time} using `Date.now()`, `performance`, and
84
+ * `globalThis.setTimeout`.
85
+ *
86
+ * Long timeouts are split into native timer chunks and tracked against an
87
+ * absolute wall-clock deadline so late callback execution and system suspension
88
+ * do not extend the requested delay. System clock adjustments can therefore
89
+ * shorten or lengthen long timeouts.
49
90
  *
50
91
  * Throws if the system clock returns an out-of-range value. This is intentional
51
92
  * — there's no reasonable fallback for a misconfigured clock.
52
93
  */
53
94
  export const createTime = (): Time => {
54
- const getNowMillis = (): Millis => Millis.orThrow(globalThis.Date.now());
95
+ const timeoutOwner = Symbol("Time");
96
+ function now(): Millis;
97
+ function now(type: "DateIso"): DateIso;
98
+ function now(type?: "DateIso"): Millis | DateIso {
99
+ const millis = getSystemNowMillis();
100
+ return type === "DateIso" ? millisToDateIso(millis) : millis;
101
+ }
55
102
 
56
103
  return {
57
- now: () => getNowMillis(),
104
+ now,
58
105
 
59
- nowDateIso: () => millisToDateIso(getNowMillis()),
106
+ performance: {
107
+ timeOrigin: globalThis.performance.timeOrigin as PerformanceTimeOrigin,
108
+ now: () => globalThis.performance.now() as PerformanceTime,
109
+ },
60
110
 
61
- setTimeout: (callback, delay) =>
62
- globalThis.setTimeout(
63
- callback,
64
- durationToMillis(delay),
65
- ) as unknown as TimeoutId,
111
+ setTimeout: (callback, duration) =>
112
+ scheduleNativeTimeout(timeoutOwner, callback, duration),
66
113
 
67
114
  clearTimeout: (id) => {
68
- globalThis.clearTimeout(id as unknown as number);
115
+ clearTimeoutId(timeoutOwner, id);
69
116
  },
70
117
  };
71
118
  };
72
119
 
120
+ const scheduleNativeTimeout = (
121
+ owner: symbol,
122
+ callback: () => void,
123
+ duration: PositiveDuration,
124
+ ): TimeoutId => {
125
+ const delay = durationToMillis(duration);
126
+ let cancelled = false;
127
+ let nativeId: ReturnType<typeof globalThis.setTimeout>;
128
+
129
+ if (delay <= maxNativeTimeoutMillis) {
130
+ nativeId = globalThis.setTimeout(() => {
131
+ if (cancelled) return;
132
+ cancelled = true;
133
+ callback();
134
+ }, delay);
135
+ } else {
136
+ // Recompute each chunk from one absolute deadline so time elapsed while the
137
+ // event loop is suspended, for example during system sleep, is not added again.
138
+ const deadline = getSystemNowMillis() + delay;
139
+
140
+ const onTimeout = (): void => {
141
+ if (cancelled) return;
142
+
143
+ const remaining = deadline - getSystemNowMillis();
144
+ if (remaining > 0) {
145
+ nativeId = globalThis.setTimeout(
146
+ onTimeout,
147
+ Math.min(remaining, maxNativeTimeoutMillis),
148
+ );
149
+ return;
150
+ }
151
+
152
+ cancelled = true;
153
+ callback();
154
+ };
155
+
156
+ nativeId = globalThis.setTimeout(onTimeout, maxNativeTimeoutMillis);
157
+ }
158
+
159
+ return {
160
+ owner,
161
+ clear: () => {
162
+ cancelled = true;
163
+ globalThis.clearTimeout(nativeId);
164
+ },
165
+ } as unknown as TimeoutId;
166
+ };
167
+
168
+ /**
169
+ * Maximum delay supported reliably by native timers: the largest positive
170
+ * signed 32-bit integer, approximately 24.9 days in milliseconds. Longer
171
+ * logical delays are scheduled in chunks to avoid native timer overflow.
172
+ */
173
+ const maxNativeTimeoutMillis = 2 ** 31 - 1;
174
+
175
+ const getSystemNowMillis = (): Millis => Millis.orThrow(globalThis.Date.now());
176
+
177
+ const clearTimeoutId = (owner: symbol, id: TimeoutId): void => {
178
+ const internal = id as unknown as TimeoutIdInternal;
179
+ assert(
180
+ internal.owner === owner,
181
+ "TimeoutId was created by another Time instance",
182
+ );
183
+ internal.clear();
184
+ };
185
+
73
186
  /**
74
187
  * Test {@link Time} with controllable timers.
75
188
  *
76
189
  * Call `advance(ms)` to move time forward and trigger any pending timeouts.
77
190
  */
78
191
  export interface TestTime extends Time {
79
- /** Advances time by the specified duration, triggering pending timeouts. */
192
+ /**
193
+ * Advances time by the specified duration, triggering pending timeouts.
194
+ *
195
+ * Timeout callback errors propagate and leave time at the callback's
196
+ * deadline. Throws if called while another advance is in progress.
197
+ */
80
198
  readonly advance: (duration: Duration) => void;
81
199
  }
82
200
 
@@ -91,20 +209,23 @@ export interface TestTimeDep {
91
209
  * called. Timeouts scheduled via `setTimeout` fire when time is advanced past
92
210
  * their deadline.
93
211
  *
94
- * Set `autoIncrement` to automatically increment time by 1ms after each `now()`
95
- * call. `"microtask"` increments after the current turn, while `"sync"`
96
- * increments immediately after each read. Omit it to keep time fixed until
97
- * `advance()` is called.
212
+ * Set `autoIncrement` to automatically increment time by 1ms after each
213
+ * wall-clock or performance `now()` call. `"microtask"` increments after the
214
+ * current turn, while `"sync"` increments immediately after each read. Omit it
215
+ * to keep time fixed until `advance()` is called.
98
216
  */
99
217
  export const testCreateTime = (options?: {
100
218
  readonly startAt?: Millis;
101
219
  readonly autoIncrement?: "microtask" | "sync";
102
220
  }): TestTime => {
103
- let now = options?.startAt ?? minMillis;
221
+ const startAt = options?.startAt ?? minMillis;
104
222
  const autoIncrement = options?.autoIncrement;
223
+ const timeoutOwner = Symbol("TestTime");
224
+ let now = startAt;
105
225
  let nextId = 1;
226
+ let advancing = false;
106
227
 
107
- const pending = new Map<number, { callback: () => void; runAt: number }>();
228
+ const pending = new Map<number, { callback: () => void; runAt: Millis }>();
108
229
  const incrementNow = (): void => {
109
230
  now = Millis.orThrow(now + 1);
110
231
  };
@@ -121,30 +242,65 @@ export const testCreateTime = (options?: {
121
242
  }
122
243
  return result;
123
244
  };
245
+ function getNow(): Millis;
246
+ function getNow(type: "DateIso"): DateIso;
247
+ function getNow(type?: "DateIso"): Millis | DateIso {
248
+ const millis = getNowMillis();
249
+ return type === "DateIso" ? millisToDateIso(millis) : millis;
250
+ }
124
251
 
125
252
  return {
126
- now: () => getNowMillis(),
253
+ now: getNow,
127
254
 
128
- nowDateIso: () => millisToDateIso(getNowMillis()),
255
+ performance: {
256
+ timeOrigin: Number(startAt) as PerformanceTimeOrigin,
257
+ now: () => (getNowMillis() - startAt) as PerformanceTime,
258
+ },
129
259
 
130
260
  setTimeout: (callback, delay) => {
261
+ const runAt = Millis.orThrow(now + durationToMillis(delay));
131
262
  const id = nextId++;
132
- pending.set(id, { callback, runAt: now + durationToMillis(delay) });
133
- return id as unknown as TimeoutId;
263
+ pending.set(id, { callback, runAt });
264
+ return {
265
+ owner: timeoutOwner,
266
+ clear: () => {
267
+ pending.delete(id);
268
+ },
269
+ } as unknown as TimeoutId;
134
270
  },
135
271
 
136
272
  clearTimeout: (id) => {
137
- pending.delete(id as unknown as number);
273
+ clearTimeoutId(timeoutOwner, id);
138
274
  },
139
275
 
140
276
  advance: (duration) => {
141
- now = Millis.orThrow(now + durationToMillis(duration));
142
-
143
- for (const [id, timeout] of pending) {
144
- if (timeout.runAt <= now) {
145
- pending.delete(id);
277
+ assert(!advancing, "TestTime.advance cannot be called while advancing");
278
+ const target = Millis.orThrow(now + durationToMillis(duration));
279
+ advancing = true;
280
+
281
+ try {
282
+ while (pending.size > 0) {
283
+ let earliestId: number | null = null;
284
+ let nextRunAt = Number.POSITIVE_INFINITY;
285
+
286
+ for (const [id, timeout] of pending) {
287
+ if (timeout.runAt <= target && timeout.runAt < nextRunAt) {
288
+ earliestId = id;
289
+ nextRunAt = timeout.runAt;
290
+ }
291
+ }
292
+
293
+ if (earliestId === null) break;
294
+
295
+ const timeout = pending.get(earliestId)!;
296
+ pending.delete(earliestId);
297
+ now = Millis.orThrow(Math.max(now, nextRunAt));
146
298
  timeout.callback();
147
299
  }
300
+
301
+ now = Millis.orThrow(Math.max(now, target));
302
+ } finally {
303
+ advancing = false;
148
304
  }
149
305
  },
150
306
  };
@@ -154,7 +310,7 @@ export const testCreateTime = (options?: {
154
310
  const maxMillisWithInfinity = 281474976710655;
155
311
 
156
312
  /**
157
- * Milliseconds timestamp, like `Date.now()`.
313
+ * Non-negative integer milliseconds used for timestamps and durations.
158
314
  *
159
315
  * The maximum value is 281474976710654 (281474976710655 - 1, reserved for
160
316
  * infinity). This enables efficient binary serialization, saving 2 bytes
@@ -169,7 +325,11 @@ export const Millis = /*#__PURE__*/ brand(
169
325
  "Millis",
170
326
  /*#__PURE__*/ lessThan(maxMillisWithInfinity)(NonNegativeInt),
171
327
  );
172
- export type Millis = typeof Millis.Type;
328
+ export type Millis = typeof Millis.Output;
329
+
330
+ /** Positive {@link Millis} value. */
331
+ export const PositiveMillis = /*#__PURE__*/ positive(Millis);
332
+ export type PositiveMillis = typeof PositiveMillis.Output;
173
333
 
174
334
  /** Minimum {@link Millis} value. */
175
335
  export const minMillis = 0 as Millis;
@@ -181,7 +341,7 @@ export const maxMillis = (maxMillisWithInfinity - 1) as Millis;
181
341
  * Converts a number to {@link Millis}, rounding to the nearest millisecond and
182
342
  * saturating overflow at {@link maxMillis}.
183
343
  */
184
- export const saturateMillis = (value: number): Millis =>
344
+ export const saturateMillis = (value: NonNaNNumber): Millis =>
185
345
  Millis.orNull(Math.max(0, Math.round(value))) ?? maxMillis;
186
346
 
187
347
  /**
@@ -193,12 +353,37 @@ export const saturateMillis = (value: number): Millis =>
193
353
  export const millisToDateIso = (value: Millis): DateIso =>
194
354
  new globalThis.Date(value).toISOString() as DateIso;
195
355
 
356
+ /** Unix epoch milliseconds used as the origin for {@link PerformanceTime}. */
357
+ export type PerformanceTimeOrigin = number & Brand<"PerformanceTimeOrigin">;
358
+
359
+ /** High-resolution milliseconds elapsed since {@link PerformanceTimeOrigin}. */
360
+ export type PerformanceTime = number & Brand<"PerformanceTime">;
361
+
362
+ /** Elapsed fractional milliseconds measured using {@link PerformanceTime}. */
363
+ export type PerformanceDuration = number & Brand<"PerformanceDuration">;
364
+
365
+ /**
366
+ * Returns the elapsed fractional milliseconds between two performance times.
367
+ *
368
+ * Throws if `end` precedes `start`.
369
+ */
370
+ export const performanceDurationBetween = (
371
+ start: PerformanceTime,
372
+ end: PerformanceTime,
373
+ ): PerformanceDuration => {
374
+ assert(end >= start, "Performance end time must not precede start time");
375
+ return (end - start) as PerformanceDuration;
376
+ };
377
+
196
378
  /**
197
379
  * Duration can be either a {@link DurationLiteral} or milliseconds as
198
380
  * {@link Millis}.
199
381
  */
200
382
  export type Duration = DurationLiteral | Millis;
201
383
 
384
+ /** Positive duration accepted by timer-based APIs. */
385
+ export type PositiveDuration = DurationLiteral | PositiveMillis;
386
+
202
387
  /**
203
388
  * Duration literal with compile-time validation.
204
389
  *
@@ -213,9 +398,9 @@ export type Duration = DurationLiteral | Millis;
213
398
  * - Months: not supported (variable length)
214
399
  * - Years: `1y`, `99y`, `1.5y` (1-99, 1.1-99.9)
215
400
  *
216
- * Each unit is limited to values that can't be expressed in the next larger
217
- * unit, ensuring every duration has exactly one canonical representation (e.g.,
218
- * 1000ms must be written as `"1s"`, not `"1000ms"`).
401
+ * Each unit uses a bounded range. Where units convert exactly, this avoids
402
+ * equivalent representations (e.g., 1000ms must be written as `"1s"`, not
403
+ * `"1000ms"`).
219
404
  *
220
405
  * Decimal values cover cases like 1.5s (1500ms) or 1.5h (90 minutes) without
221
406
  * allowing redundant forms. For precise values that don't fit (e.g., 1050ms),
@@ -299,17 +484,20 @@ export type DurationLiteralYears =
299
484
  * ### Example
300
485
  *
301
486
  * ```ts
302
- * durationToMillis("1ms"); // 1
303
- * durationToMillis("500ms"); // 500
304
- * durationToMillis("1.5s"); // 1500
305
- * durationToMillis("30s"); // 30000
306
- * durationToMillis("5m"); // 300000
307
- * durationToMillis("12h"); // 43200000
308
- * durationToMillis("7d"); // 604800000
309
- * durationToMillis(Millis.orThrow(5000)); // 5000 (already Millis)
487
+ * import { durationToMillis, Millis } from "@evolu/common";
488
+ *
489
+ * expect(durationToMillis("1.5s")).toBe(1500);
490
+ * expect(durationToMillis("5m")).toBe(300000);
491
+ * expect(durationToMillis("12h")).toBe(43200000);
492
+ * expect(durationToMillis("1w")).toBe(604800000);
493
+ * expect(durationToMillis(Millis.orThrow(5000))).toBe(5000);
310
494
  * ```
311
495
  */
312
- export const durationToMillis = (duration: Duration): Millis => {
496
+ export function durationToMillis(
497
+ duration: DurationLiteral | PositiveMillis,
498
+ ): PositiveMillis;
499
+ export function durationToMillis(duration: Duration): Millis;
500
+ export function durationToMillis(duration: Duration): Millis {
313
501
  if (typeof duration === "number") return duration;
314
502
 
315
503
  const num = parseFloat(duration);
@@ -320,23 +508,7 @@ export const durationToMillis = (duration: Duration): Millis => {
320
508
  return Millis.orThrow(
321
509
  Math.round(num * durationUnits[unit as keyof typeof durationUnits]),
322
510
  );
323
- };
324
-
325
- /**
326
- * Returns a Promise that resolves after the specified duration.
327
- *
328
- * Uses {@link Duration} parsing and `globalThis.setTimeout`.
329
- *
330
- * ### Example
331
- *
332
- * ```ts
333
- * await setTimeout("1ms");
334
- * ```
335
- */
336
- export const setTimeout = (duration: Duration): Promise<void> =>
337
- new Promise((resolve) => {
338
- globalThis.setTimeout(resolve, durationToMillis(duration));
339
- });
511
+ }
340
512
 
341
513
  const durationUnits = {
342
514
  ms: 1,
@@ -377,39 +549,70 @@ export const msLongTask = 50 as Millis;
377
549
  *
378
550
  * - Under 1 minute: `1.234s`
379
551
  * - Under 1 hour: `1m30.000s`
380
- * - 1 hour or more: `1h30m45.000s`
552
+ * - Under 1 day: `1h30m45.000s`
553
+ * - Under 1 week: `1d2h30m45.000s`
554
+ * - Under 1 year: `1w2d3h30m45.000s`
555
+ * - 1 year or more: `1y2w3d4h30m45.000s`
556
+ *
557
+ * Weeks are 7 days and years are 365 days.
381
558
  *
382
559
  * ### Example
383
560
  *
384
561
  * ```ts
385
- * formatMillisAsDuration(1234 as Millis); // "1.234s"
386
- * formatMillisAsDuration(90000 as Millis); // "1m30.000s"
387
- * formatMillisAsDuration(3661000 as Millis); // "1h1m1.000s"
562
+ * import { formatMillisAsDuration, Millis } from "@evolu/common";
563
+ *
564
+ * expect(formatMillisAsDuration(Millis.orThrow(1234))).toBe("1.234s");
565
+ * expect(formatMillisAsDuration(Millis.orThrow(90000))).toBe("1m30.000s");
566
+ * expect(formatMillisAsDuration(Millis.orThrow(90061000))).toBe(
567
+ * "1d1h1m1.000s",
568
+ * );
388
569
  * ```
389
570
  */
390
571
  export const formatMillisAsDuration = (millis: Millis): string => {
391
- const elapsed = millis / 1000;
392
- if (elapsed < 60) {
393
- return `${elapsed.toFixed(3)}s`;
394
- } else if (elapsed < 3600) {
395
- const minutes = Math.floor(elapsed / 60);
396
- const seconds = (elapsed % 60).toFixed(3);
397
- return `${minutes}m${seconds}s`;
398
- } else {
399
- const hours = Math.floor(elapsed / 3600);
400
- const minutes = Math.floor((elapsed % 3600) / 60);
401
- const seconds = ((elapsed % 3600) % 60).toFixed(3);
402
- return `${hours}h${minutes}m${seconds}s`;
403
- }
572
+ const seconds = ((millis % durationUnits.m) / durationUnits.s).toFixed(3);
573
+ if (millis < durationUnits.m) return `${seconds}s`;
574
+
575
+ const minutes = Math.floor(millis / durationUnits.m) % 60;
576
+ if (millis < durationUnits.h) return `${minutes}m${seconds}s`;
577
+
578
+ const hours = Math.floor(millis / durationUnits.h) % 24;
579
+ if (millis < durationUnits.d) return `${hours}h${minutes}m${seconds}s`;
580
+
581
+ const daysAfterYears = Math.floor(
582
+ (millis % durationUnits.y) / durationUnits.d,
583
+ );
584
+ const days = daysAfterYears % 7;
585
+ if (millis < durationUnits.w)
586
+ return `${days}d${hours}h${minutes}m${seconds}s`;
587
+
588
+ const weeks = Math.floor(daysAfterYears / 7);
589
+ if (millis < durationUnits.y)
590
+ return `${weeks}w${days}d${hours}h${minutes}m${seconds}s`;
591
+
592
+ const years = Math.floor(millis / durationUnits.y);
593
+ return `${years}y${weeks}w${days}d${hours}h${minutes}m${seconds}s`;
404
594
  };
405
595
 
406
596
  /**
407
- * Formats {@link Millis} as `HH:MM:SS.mmm`.
597
+ * Formats {@link Millis} as local time in `HH:MM:SS.mmm` format.
408
598
  *
409
599
  * ### Example
410
600
  *
411
601
  * ```ts
412
- * formatMillisAsClockTime(Millis.orThrow(Date.now())); // "14:32:15.234"
602
+ * import { formatMillisAsClockTime, Millis } from "@evolu/common";
603
+ *
604
+ * const timestamp = new globalThis.Date(
605
+ * 2026,
606
+ * 0,
607
+ * 28,
608
+ * 14,
609
+ * 32,
610
+ * 15,
611
+ * 234,
612
+ * ).getTime();
613
+ * expect(formatMillisAsClockTime(Millis.orThrow(timestamp))).toBe(
614
+ * "14:32:15.234",
615
+ * );
413
616
  * ```
414
617
  */
415
618
  export const formatMillisAsClockTime = (millis: Millis): string => {