@evolu/common 6.0.1-preview.2 → 6.0.1-preview.21

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 (154) hide show
  1. package/dist/src/Assert.d.ts +6 -3
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +7 -4
  4. package/dist/src/Brand.d.ts +75 -0
  5. package/dist/src/Brand.d.ts.map +1 -0
  6. package/dist/src/Brand.js +1 -0
  7. package/dist/src/Buffer.d.ts +1 -1
  8. package/dist/src/Buffer.d.ts.map +1 -1
  9. package/dist/src/Buffer.js +1 -1
  10. package/dist/src/CallbackRegistry.d.ts +53 -0
  11. package/dist/src/CallbackRegistry.d.ts.map +1 -0
  12. package/dist/src/CallbackRegistry.js +25 -0
  13. package/dist/src/Console.d.ts +31 -6
  14. package/dist/src/Console.d.ts.map +1 -1
  15. package/dist/src/Console.js +72 -9
  16. package/dist/src/Crypto.d.ts +61 -34
  17. package/dist/src/Crypto.d.ts.map +1 -1
  18. package/dist/src/Crypto.js +29 -42
  19. package/dist/src/Evolu/Db.d.ts +159 -66
  20. package/dist/src/Evolu/Db.d.ts.map +1 -1
  21. package/dist/src/Evolu/Db.js +300 -705
  22. package/dist/src/Evolu/Diff.d.ts +3 -3
  23. package/dist/src/Evolu/Diff.d.ts.map +1 -1
  24. package/dist/src/Evolu/Diff.js +7 -5
  25. package/dist/src/Evolu/Evolu.d.ts +200 -115
  26. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  27. package/dist/src/Evolu/Evolu.js +287 -157
  28. package/dist/src/Evolu/Internal.d.ts +0 -2
  29. package/dist/src/Evolu/Internal.d.ts.map +1 -1
  30. package/dist/src/Evolu/Internal.js +0 -2
  31. package/dist/src/Evolu/LocalAuth.d.ts +144 -0
  32. package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
  33. package/dist/src/Evolu/LocalAuth.js +171 -0
  34. package/dist/src/Evolu/Owner.d.ts +209 -124
  35. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  36. package/dist/src/Evolu/Owner.js +118 -104
  37. package/dist/src/Evolu/Platform.d.ts +9 -7
  38. package/dist/src/Evolu/Platform.d.ts.map +1 -1
  39. package/dist/src/Evolu/Protocol.d.ts +256 -211
  40. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  41. package/dist/src/Evolu/Protocol.js +558 -359
  42. package/dist/src/Evolu/Public.d.ts +6 -8
  43. package/dist/src/Evolu/Public.d.ts.map +1 -1
  44. package/dist/src/Evolu/Public.js +2 -3
  45. package/dist/src/Evolu/PublicKysely.js +3 -3
  46. package/dist/src/Evolu/Query.d.ts +2 -1
  47. package/dist/src/Evolu/Query.d.ts.map +1 -1
  48. package/dist/src/Evolu/Relay.d.ts +72 -4
  49. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  50. package/dist/src/Evolu/Relay.js +124 -13
  51. package/dist/src/Evolu/Schema.d.ts +129 -73
  52. package/dist/src/Evolu/Schema.d.ts.map +1 -1
  53. package/dist/src/Evolu/Schema.js +169 -89
  54. package/dist/src/Evolu/Storage.d.ts +159 -14
  55. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  56. package/dist/src/Evolu/Storage.js +42 -32
  57. package/dist/src/Evolu/Sync.d.ts +68 -13
  58. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  59. package/dist/src/Evolu/Sync.js +414 -20
  60. package/dist/src/Evolu/Timestamp.d.ts +35 -27
  61. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  62. package/dist/src/Evolu/Timestamp.js +27 -18
  63. package/dist/src/ManyToManyMap.d.ts +74 -10
  64. package/dist/src/ManyToManyMap.d.ts.map +1 -1
  65. package/dist/src/ManyToManyMap.js +41 -6
  66. package/dist/src/Number.d.ts +2 -1
  67. package/dist/src/Number.d.ts.map +1 -1
  68. package/dist/src/Random.d.ts +3 -2
  69. package/dist/src/Random.d.ts.map +1 -1
  70. package/dist/src/RefCountedResourceManager.d.ts +119 -0
  71. package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
  72. package/dist/src/RefCountedResourceManager.js +197 -0
  73. package/dist/src/Result.d.ts +176 -51
  74. package/dist/src/Result.d.ts.map +1 -1
  75. package/dist/src/Result.js +30 -241
  76. package/dist/src/Sqlite.d.ts +21 -4
  77. package/dist/src/Sqlite.d.ts.map +1 -1
  78. package/dist/src/Sqlite.js +50 -8
  79. package/dist/src/Task.d.ts +511 -0
  80. package/dist/src/Task.d.ts.map +1 -0
  81. package/dist/src/Task.js +410 -0
  82. package/dist/src/Time.d.ts +59 -0
  83. package/dist/src/Time.d.ts.map +1 -1
  84. package/dist/src/Time.js +87 -4
  85. package/dist/src/Type.d.ts +474 -327
  86. package/dist/src/Type.d.ts.map +1 -1
  87. package/dist/src/Type.js +502 -436
  88. package/dist/src/Types.d.ts +0 -74
  89. package/dist/src/Types.d.ts.map +1 -1
  90. package/dist/src/WebSocket.d.ts +5 -2
  91. package/dist/src/WebSocket.d.ts.map +1 -1
  92. package/dist/src/WebSocket.js +12 -13
  93. package/dist/src/Worker.d.ts +39 -11
  94. package/dist/src/Worker.d.ts.map +1 -1
  95. package/dist/src/Worker.js +22 -4
  96. package/dist/src/index.d.ts +3 -3
  97. package/dist/src/index.d.ts.map +1 -1
  98. package/dist/src/index.js +3 -3
  99. package/package.json +14 -13
  100. package/src/Assert.ts +8 -7
  101. package/src/Brand.ts +75 -0
  102. package/src/Buffer.ts +1 -1
  103. package/src/CallbackRegistry.ts +84 -0
  104. package/src/Console.ts +91 -11
  105. package/src/Crypto.ts +94 -79
  106. package/src/Evolu/Db.ts +528 -1027
  107. package/src/Evolu/Diff.ts +7 -5
  108. package/src/Evolu/Evolu.ts +682 -329
  109. package/src/Evolu/Internal.ts +0 -2
  110. package/src/Evolu/LocalAuth.ts +422 -0
  111. package/src/Evolu/Owner.ts +282 -227
  112. package/src/Evolu/Platform.ts +9 -9
  113. package/src/Evolu/Protocol.ts +788 -640
  114. package/src/Evolu/Public.ts +7 -14
  115. package/src/Evolu/PublicKysely.ts +3 -3
  116. package/src/Evolu/Query.ts +2 -1
  117. package/src/Evolu/Relay.ts +238 -18
  118. package/src/Evolu/Schema.ts +391 -191
  119. package/src/Evolu/Storage.ts +275 -55
  120. package/src/Evolu/Sync.ts +708 -37
  121. package/src/Evolu/Timestamp.ts +38 -35
  122. package/src/ManyToManyMap.ts +127 -24
  123. package/src/Number.ts +2 -6
  124. package/src/Random.ts +3 -2
  125. package/src/RefCountedResourceManager.ts +368 -0
  126. package/src/Result.ts +183 -53
  127. package/src/Sqlite.ts +59 -23
  128. package/src/Task.ts +779 -0
  129. package/src/Time.ts +168 -4
  130. package/src/Type.ts +708 -660
  131. package/src/Types.ts +0 -76
  132. package/src/WebSocket.ts +23 -17
  133. package/src/Worker.ts +72 -23
  134. package/src/index.ts +3 -3
  135. package/dist/src/Callbacks.d.ts +0 -20
  136. package/dist/src/Callbacks.d.ts.map +0 -1
  137. package/dist/src/Callbacks.js +0 -18
  138. package/dist/src/Evolu/Config.d.ts +0 -69
  139. package/dist/src/Evolu/Config.d.ts.map +0 -1
  140. package/dist/src/Evolu/Config.js +0 -9
  141. package/dist/src/Evolu/Kysely.d.ts +0 -6
  142. package/dist/src/Evolu/Kysely.d.ts.map +0 -1
  143. package/dist/src/Evolu/Kysely.js +0 -21
  144. package/dist/src/NanoId.d.ts +0 -27
  145. package/dist/src/NanoId.d.ts.map +0 -1
  146. package/dist/src/NanoId.js +0 -6
  147. package/dist/src/Promise.d.ts +0 -180
  148. package/dist/src/Promise.d.ts.map +0 -1
  149. package/dist/src/Promise.js +0 -176
  150. package/src/Callbacks.ts +0 -43
  151. package/src/Evolu/Config.ts +0 -83
  152. package/src/Evolu/Kysely.ts +0 -38
  153. package/src/NanoId.ts +0 -39
  154. package/src/Promise.ts +0 -295
@@ -0,0 +1,410 @@
1
+ /**
2
+ * ⚡ Lazy, cancellable Promise that returns Result instead of throwing
3
+ *
4
+ * @module
5
+ */
6
+ import { isNonEmptyArray, shiftArray } from "./Array.js";
7
+ import { err, ok } from "./Result.js";
8
+ import { durationToNonNegativeInt } from "./Time.js";
9
+ import { PositiveInt } from "./Type.js";
10
+ /** Narrower check to detect AbortError objects at runtime. */
11
+ const isAbortError = (error) => typeof error === "object" &&
12
+ error !== null &&
13
+ error.type === "AbortError";
14
+ /**
15
+ * Combines user signal from context with an internal signal.
16
+ *
17
+ * If the context has a signal, combines both signals using AbortSignal.any().
18
+ * Otherwise, returns just the internal signal.
19
+ */
20
+ const combineSignal = (context, internalSignal) => context?.signal
21
+ ? AbortSignal.any([context.signal, internalSignal])
22
+ : internalSignal;
23
+ /**
24
+ * Converts async function returning {@link Result} to a {@link Task}.
25
+ *
26
+ * ### Example
27
+ *
28
+ * ```ts
29
+ * interface FetchError {
30
+ * readonly type: "FetchError";
31
+ * readonly error: unknown;
32
+ * }
33
+ *
34
+ * // Task version of fetch with proper error handling and cancellation support.
35
+ * const fetch = (url: string) =>
36
+ * toTask((context) =>
37
+ * tryAsync(
38
+ * () => globalThis.fetch(url, { signal: context?.signal ?? null }),
39
+ * (error): FetchError => ({ type: "FetchError", error }),
40
+ * ),
41
+ * );
42
+ *
43
+ * // `satisfies` shows the expected type signature.
44
+ * fetch satisfies (url: string) => Task<Response, FetchError>;
45
+ *
46
+ * const result1 = await fetch("https://api.example.com/data")();
47
+ * result1 satisfies Result<Response, FetchError>;
48
+ *
49
+ * // With AbortController
50
+ * const controller = new AbortController();
51
+ * const result2 = await fetch("https://api.example.com/data")(controller);
52
+ * result2 satisfies Result<Response, FetchError | AbortError>;
53
+ * ```
54
+ */
55
+ export const toTask = (fn) =>
56
+ // Note: Not using async to avoid Promise wrapper overhead in fast path
57
+ ((context) => {
58
+ const signal = context?.signal;
59
+ // Fast path when no signal – return promise directly
60
+ if (!signal) {
61
+ // Preserve future context fields (e.g., tracing) even without a signal
62
+ return fn(context);
63
+ }
64
+ if (signal.aborted) {
65
+ return Promise.resolve(err({ type: "AbortError", reason: signal.reason }));
66
+ }
67
+ // Use Promise.withResolvers for clean abort handling and cleanup
68
+ const { promise: abortPromise, resolve: resolveAbort } = Promise.withResolvers();
69
+ const handleAbort = () => {
70
+ resolveAbort(err({ type: "AbortError", reason: signal.reason }));
71
+ };
72
+ signal.addEventListener("abort", handleAbort, { once: true });
73
+ // No finally: we expect no throws in normal flow; Result path removes listener.
74
+ // Unexpected throws indicate a bug and are allowed to crash (no recovery here).
75
+ return Promise.race([
76
+ abortPromise,
77
+ fn(context).then((result) => {
78
+ signal.removeEventListener("abort", handleAbort);
79
+ return result;
80
+ }),
81
+ ]);
82
+ });
83
+ /**
84
+ * Creates a {@link Task} that waits for the specified duration.
85
+ *
86
+ * ### Example
87
+ *
88
+ * ```ts
89
+ * const result1 = await wait("10ms")();
90
+ * result1 satisfies Result<void, never>;
91
+ *
92
+ * // With AbortController
93
+ * const controller = new AbortController();
94
+ * const result2 = await wait("10ms")(controller);
95
+ * result2 satisfies Result<void, AbortError>;
96
+ * ```
97
+ */
98
+ export const wait = (duration) => toTask((context) => new Promise((resolve) => {
99
+ const ms = durationToNonNegativeInt(duration);
100
+ const timeoutSignal = AbortSignal.timeout(ms);
101
+ const signal = combineSignal(context, timeoutSignal);
102
+ // Listen for abort - either from timeout completion or external abort
103
+ signal.addEventListener("abort", () => {
104
+ resolve(ok());
105
+ }, { once: true });
106
+ }));
107
+ /**
108
+ * Adds timeout behavior to a {@link Task}.
109
+ *
110
+ * ### Example
111
+ *
112
+ * ```ts
113
+ * interface FetchError {
114
+ * readonly type: "FetchError";
115
+ * readonly error: unknown;
116
+ * }
117
+ *
118
+ * // Task version of fetch with proper error handling and cancellation support.
119
+ * const fetch = (url: string) =>
120
+ * toTask((context) =>
121
+ * tryAsync(
122
+ * () => globalThis.fetch(url, { signal: context?.signal ?? null }),
123
+ * (error): FetchError => ({ type: "FetchError", error }),
124
+ * ),
125
+ * );
126
+ *
127
+ * // `satisfies` shows the expected type signature.
128
+ * fetch satisfies (url: string) => Task<Response, FetchError>;
129
+ *
130
+ * const fetchWithTimeout = (url: string) => timeout("2m", fetch(url));
131
+ *
132
+ * const result1 = await fetchWithTimeout("https://api.example.com/data")();
133
+ * result1 satisfies Result<Response, FetchError | TimeoutError>;
134
+ *
135
+ * // With AbortController
136
+ * const controller = new AbortController();
137
+ * const result2 = await fetchWithTimeout("https://api.example.com/data")(
138
+ * controller,
139
+ * );
140
+ * result2 satisfies Result<
141
+ * Response,
142
+ * FetchError | TimeoutError | AbortError
143
+ * >;
144
+ * ```
145
+ */
146
+ export const timeout = (duration, task) => toTask(async (context) => {
147
+ const timeoutMs = durationToNonNegativeInt(duration);
148
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
149
+ const signal = combineSignal(context, timeoutSignal);
150
+ const result = await task({ signal });
151
+ if (timeoutSignal.aborted) {
152
+ return err({ type: "TimeoutError", timeoutMs });
153
+ }
154
+ return result;
155
+ });
156
+ /**
157
+ * Adds retry logic with exponential backoff and jitter to a {@link Task}.
158
+ *
159
+ * ### Example
160
+ *
161
+ * ```ts
162
+ * interface FetchError {
163
+ * readonly type: "FetchError";
164
+ * readonly error: unknown;
165
+ * }
166
+ *
167
+ * // Task version of fetch with proper error handling and cancellation support.
168
+ * const fetch = (url: string) =>
169
+ * toTask((context) =>
170
+ * tryAsync(
171
+ * () => globalThis.fetch(url, { signal: context?.signal ?? null }),
172
+ * (error): FetchError => ({ type: "FetchError", error }),
173
+ * ),
174
+ * );
175
+ *
176
+ * // `satisfies` shows the expected type signature.
177
+ * fetch satisfies (url: string) => Task<Response, FetchError>;
178
+ *
179
+ * const fetchWithRetry = (url: string) =>
180
+ * retry({ retries: PositiveInt.orThrow(3) }, fetch(url));
181
+ *
182
+ * const result1 = await fetchWithRetry("https://api.example.com/data")();
183
+ * result1 satisfies Result<Response, FetchError | RetryError<FetchError>>;
184
+ *
185
+ * // With AbortController
186
+ * const controller = new AbortController();
187
+ * const result2 = await fetchWithRetry("https://api.example.com/data")(
188
+ * controller,
189
+ * );
190
+ * result2 satisfies Result<
191
+ * Response,
192
+ * FetchError | RetryError<FetchError> | AbortError
193
+ * >;
194
+ * ```
195
+ */
196
+ export const retry = ({ retries, initialDelay = "1s", maxDelay = "30s", factor = 2, jitter = 0.5, retryable = (error) => !isAbortError(error), onRetry, }, task) => toTask(async (context) => {
197
+ const initialDelayMs = durationToNonNegativeInt(initialDelay);
198
+ const maxDelayMs = durationToNonNegativeInt(maxDelay);
199
+ const maxRetries = PositiveInt.orThrow(retries);
200
+ let attempt = 0;
201
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
202
+ while (true) {
203
+ const result = await task(context);
204
+ if (result.ok) {
205
+ return result;
206
+ }
207
+ // Never retry on AbortError; propagate it directly
208
+ if (isAbortError(result.error)) {
209
+ return err(result.error);
210
+ }
211
+ attempt += 1;
212
+ if (attempt > maxRetries || !retryable(result.error)) {
213
+ return err({
214
+ type: "RetryError",
215
+ cause: result.error,
216
+ attempts: attempt,
217
+ });
218
+ }
219
+ // Calculate delay with exponential backoff
220
+ const exponentialDelay = initialDelayMs * Math.pow(factor, attempt - 1);
221
+ const cappedDelay = Math.min(exponentialDelay, maxDelayMs);
222
+ // Apply jitter to prevent thundering herd problem
223
+ const randomFactor = 1 - jitter + Math.random() * jitter * 2;
224
+ const delay = Math.floor(cappedDelay * randomFactor);
225
+ if (onRetry) {
226
+ onRetry(result.error, attempt, delay);
227
+ }
228
+ // Wait before retry
229
+ const delayResult = await wait(delay)(context);
230
+ if (!delayResult.ok) {
231
+ // If delay was aborted, return AbortError (will be handled by toTask)
232
+ return delayResult;
233
+ }
234
+ }
235
+ });
236
+ /**
237
+ * Creates a semaphore that limits concurrent async Tasks to the specified
238
+ * count.
239
+ *
240
+ * A semaphore controls access to a resource by maintaining a count of available
241
+ * permits. Tasks acquire a permit before executing and release it when
242
+ * complete.
243
+ *
244
+ * For mutual exclusion (exactly one Task at a time), consider using
245
+ * {@link createMutex} instead.
246
+ *
247
+ * ### Example
248
+ *
249
+ * ```ts
250
+ * // Allow maximum 3 concurrent Tasks
251
+ * const semaphore = createSemaphore(PositiveInt.orThrow(3));
252
+ *
253
+ * let currentConcurrent = 0;
254
+ * const events: Array<string> = [];
255
+ *
256
+ * const fetchData = (id: number) =>
257
+ * toTask<number, never>(async (context) => {
258
+ * currentConcurrent++;
259
+ * events.push(`start ${id} (concurrent: ${currentConcurrent})`);
260
+ *
261
+ * await wait("10ms")(context);
262
+ *
263
+ * currentConcurrent--;
264
+ * events.push(`end ${id} (concurrent: ${currentConcurrent})`);
265
+ * return ok(id * 10);
266
+ * });
267
+ *
268
+ * // These will execute with at most 3 running concurrently
269
+ * const results = await Promise.all([
270
+ * semaphore.withPermit(fetchData(1))(),
271
+ * semaphore.withPermit(fetchData(2))(),
272
+ * semaphore.withPermit(fetchData(3))(),
273
+ * semaphore.withPermit(fetchData(4))(), // waits for one above to complete
274
+ * semaphore.withPermit(fetchData(5))(), // waits for permit
275
+ * ]);
276
+ *
277
+ * expect(results.map(getOrThrow)).toEqual([10, 20, 30, 40, 50]);
278
+ * expect(events).toMatchInlineSnapshot(`
279
+ * [
280
+ * "start 1 (concurrent: 1)",
281
+ * "start 2 (concurrent: 2)",
282
+ * "start 3 (concurrent: 3)",
283
+ * "end 1 (concurrent: 2)",
284
+ * "start 4 (concurrent: 3)",
285
+ * "end 2 (concurrent: 2)",
286
+ * "start 5 (concurrent: 3)",
287
+ * "end 3 (concurrent: 2)",
288
+ * "end 4 (concurrent: 1)",
289
+ * "end 5 (concurrent: 0)",
290
+ * ]
291
+ * `);
292
+ * ```
293
+ */
294
+ export const createSemaphore = (maxConcurrent) => {
295
+ let isDisposed = false;
296
+ let availablePermits = maxConcurrent;
297
+ const waitingQueue = [];
298
+ const semaphoreController = new AbortController();
299
+ const acquire = () => {
300
+ if (availablePermits > 0) {
301
+ availablePermits--;
302
+ return Promise.resolve();
303
+ }
304
+ return new Promise((resolve) => {
305
+ waitingQueue.push(resolve);
306
+ });
307
+ };
308
+ const release = () => {
309
+ if (isNonEmptyArray(waitingQueue)) {
310
+ shiftArray(waitingQueue)();
311
+ }
312
+ else {
313
+ availablePermits++;
314
+ }
315
+ };
316
+ return {
317
+ withPermit: (task) => toTask(async (context) => {
318
+ await acquire();
319
+ // Check if semaphore was disposed while waiting
320
+ if (isDisposed) {
321
+ return err({
322
+ type: "AbortError",
323
+ reason: "Semaphore disposed",
324
+ });
325
+ }
326
+ const signal = combineSignal(context, semaphoreController.signal);
327
+ const result = await task({ signal });
328
+ release();
329
+ return result;
330
+ }),
331
+ [Symbol.dispose]: () => {
332
+ if (isDisposed)
333
+ return;
334
+ isDisposed = true;
335
+ // Cancel all running and waiting tasks
336
+ semaphoreController.abort("Semaphore disposed");
337
+ // Release all waiting tasks so they can continue and check isDisposed
338
+ while (isNonEmptyArray(waitingQueue)) {
339
+ shiftArray(waitingQueue)();
340
+ }
341
+ },
342
+ };
343
+ };
344
+ /**
345
+ * Creates a new mutex for ensuring mutual exclusion.
346
+ *
347
+ * A mutex is a {@link createSemaphore} with exactly one permit, ensuring that
348
+ * only one Task can execute at a time.
349
+ *
350
+ * ### Example
351
+ *
352
+ * ```ts
353
+ * const mutex = createMutex();
354
+ *
355
+ * const updateTask = (id: number) =>
356
+ * toTask((context) =>
357
+ * tryAsync(
358
+ * () => updateSharedResource(id, context),
359
+ * (error): UpdateError => ({ type: "UpdateError", error }),
360
+ * ),
361
+ * );
362
+ *
363
+ * // These Tasks will execute one at a time
364
+ * const results = await Promise.all([
365
+ * mutex.withLock(updateTask(1))(),
366
+ * mutex.withLock(updateTask(2))(),
367
+ * mutex.withLock(updateTask(3))(),
368
+ * ]);
369
+ * ```
370
+ */
371
+ export const createMutex = () => {
372
+ const mutex = createSemaphore(1);
373
+ return {
374
+ withLock: mutex.withPermit,
375
+ [Symbol.dispose]: mutex[Symbol.dispose],
376
+ };
377
+ };
378
+ // TODO: Add tracing support
379
+ // - Extend TaskContext with optional tracing field
380
+ // - Add traced(name, task) helper that wraps Task execution
381
+ // - Collect span data (name, timing, parent-child relationships, status)
382
+ // - Support OpenTelemetry export format with proper traceId/spanId generation
383
+ // - Automatic parent-child span relationships through context propagation
384
+ /**
385
+ * Schedule a task to run after all interactions (animations, gestures,
386
+ * navigation) have completed.
387
+ *
388
+ * This uses `requestIdleCallback` when available, otherwise falls back to
389
+ * `setTimeout(0)` for cross-platform compatibility.
390
+ *
391
+ * ### Example
392
+ *
393
+ * ```ts
394
+ * const processDataTask: Task<void, ProcessError> = toTask(async () => {
395
+ * // Heavy processing work
396
+ * return ok();
397
+ * });
398
+ *
399
+ * // Schedule the task to run when idle
400
+ * void requestIdleTask(processDataTask)();
401
+ * ```
402
+ */
403
+ export const requestIdleTask = (task) => toTask(async (context) => new Promise((resolve) => {
404
+ idleCallback(() => {
405
+ void task(context).then(resolve);
406
+ });
407
+ }));
408
+ const idleCallback = typeof globalThis.requestIdleCallback === "function"
409
+ ? globalThis.requestIdleCallback
410
+ : (callback) => setTimeout(callback, 0);
@@ -3,9 +3,11 @@
3
3
  *
4
4
  * @module
5
5
  */
6
+ import { DateIso, NonNegativeInt } from "./Type.js";
6
7
  /** Retrieves the current time in milliseconds, similar to `Date.now()`. */
7
8
  export interface Time {
8
9
  readonly now: () => number;
10
+ readonly nowIso: () => DateIso;
9
11
  }
10
12
  export interface TimeDep {
11
13
  readonly time: Time;
@@ -17,4 +19,61 @@ export declare const createTime: () => Time;
17
19
  * a queueMicrotask.
18
20
  */
19
21
  export declare const createTestTime: () => Time;
22
+ /** Single digit 0-9. Used internally for {@link DurationString} validation. */
23
+ export type D = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";
24
+ /**
25
+ * Minutes and seconds 1-59. Used internally for {@link DurationString}
26
+ * validation. Uses single digits for 1-9, full numbers for 10-59.
27
+ */
28
+ export type MmSs = Exclude<D, "0"> | `1${D}` | `2${D}` | `3${D}` | `4${D}` | `5${D}`;
29
+ /** Hours 1-23. Used internally for {@link DurationString} validation. */
30
+ export type Hours = Exclude<D, "0"> | `1${D}` | `2${"0" | "1" | "2" | "3"}`;
31
+ /** Days 1-99. Used internally for {@link DurationString} validation. */
32
+ export type Days = Exclude<D, "0"> | `${Exclude<D, "0">}${D}`;
33
+ /**
34
+ * Template literal type for compile-time validated duration strings.
35
+ *
36
+ * Allowed patterns: basic units (ms, s, m, h, d) and logical combinations
37
+ * (s+ms, m+s, h+m, d+h).
38
+ *
39
+ * Supported formats:
40
+ *
41
+ * - Milliseconds: `0ms`, `1ms`, `500ms`, `999ms`
42
+ * - Seconds: `5s`, `30s` (1-59, single digit for 1-9)
43
+ * - Minutes: `5m`, `30m` (1-59, single digit for 1-9)
44
+ * - Hours: `1h`, `12h`, `23h` (1-23)
45
+ * - Days: `1d`, `30d`, `99d` (1-99)
46
+ * - Combinations: `1s 250ms`, `30m 15s`, `2h 45m`, `7d 12h`
47
+ *
48
+ * Note: Duration strings are for developer experience only - they provide
49
+ * readable, compile-time validated expressions but should never be persisted or
50
+ * parsed from users as they are not localized. Always convert to NonNegativeInt
51
+ * (milliseconds) for storage and APIs.
52
+ */
53
+ export type DurationString = `${D}ms` | `${D}${D}ms` | `${D}${D}${D}ms` | `${MmSs}s` | `${MmSs}m` | `${Hours}h` | `${Days}d` | `${MmSs}s ${D}ms` | `${MmSs}s ${D}${D}ms` | `${MmSs}s ${D}${D}${D}ms` | `${MmSs}m ${MmSs}s` | `${Hours}h ${MmSs}m` | `${Days}d ${Hours}h`;
54
+ /**
55
+ * Duration can be either a {@link DurationString} or milliseconds as
56
+ * {@link NonNegativeInt}.
57
+ */
58
+ export type Duration = DurationString | NonNegativeInt;
59
+ /**
60
+ * Converts a duration to milliseconds.
61
+ *
62
+ * Accepts either a {@link DurationString} (e.g., "5m", "1h 30m") or milliseconds
63
+ * as {@link NonNegativeInt}.
64
+ *
65
+ * ### Example
66
+ *
67
+ * ```ts
68
+ * durationToNonNegativeInt("0ms"); // 0 ✅
69
+ * durationToNonNegativeInt("500ms"); // 500 ✅
70
+ * durationToNonNegativeInt("30s"); // 30000 ✅
71
+ * durationToNonNegativeInt("5m"); // 300000 ✅
72
+ * durationToNonNegativeInt("12h"); // 43200000 ✅
73
+ * durationToNonNegativeInt("7d"); // 604800000 ✅
74
+ * durationToNonNegativeInt("2h 45m"); // 9900000 ✅
75
+ * durationToNonNegativeInt(5000); // 5000 ✅ (already milliseconds)
76
+ * ```
77
+ */
78
+ export declare const durationToNonNegativeInt: (duration: Duration) => NonNegativeInt;
20
79
  //# sourceMappingURL=Time.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Time.d.ts","sourceRoot":"","sources":["../../src/Time.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,2EAA2E;AAC3E,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;CACrB;AAED,+CAA+C;AAC/C,eAAO,MAAM,UAAU,QAAO,IAE5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAO,IAWjC,CAAC"}
1
+ {"version":3,"file":"Time.d.ts","sourceRoot":"","sources":["../../src/Time.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEpD,2EAA2E;AAC3E,MAAM,WAAW,IAAI;IACnB,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;CACrB;AAED,+CAA+C;AAC/C,eAAO,MAAM,UAAU,QAAO,IAS7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAO,IAcjC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,MAAM,CAAC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AAE1E;;;GAGG;AACH,MAAM,MAAM,IAAI,GACZ,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,GACf,IAAI,CAAC,EAAE,GACP,IAAI,CAAC,EAAE,GACP,IAAI,CAAC,EAAE,GACP,IAAI,CAAC,EAAE,GACP,IAAI,CAAC,EAAE,CAAC;AAEZ,yEAAyE;AACzE,MAAM,MAAM,KAAK,GACb,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,GACf,IAAI,CAAC,EAAE,GACP,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,EAAE,CAAC;AAEhC,wEAAwE;AACxE,MAAM,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,OAAO,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,cAAc,GACtB,GAAG,CAAC,IAAI,GACR,GAAG,CAAC,GAAG,CAAC,IAAI,GACZ,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAChB,GAAG,IAAI,GAAG,GACV,GAAG,IAAI,GAAG,GACV,GAAG,KAAK,GAAG,GACX,GAAG,IAAI,GAAG,GACV,GAAG,IAAI,KAAK,CAAC,IAAI,GACjB,GAAG,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,GACrB,GAAG,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GACzB,GAAG,IAAI,KAAK,IAAI,GAAG,GACnB,GAAG,KAAK,KAAK,IAAI,GAAG,GACpB,GAAG,IAAI,KAAK,KAAK,GAAG,CAAC;AAEzB;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,cAAc,GAAG,cAAc,CAAC;AAEvD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,wBAAwB,GACnC,UAAU,QAAQ,KACjB,cA+DF,CAAC"}
package/dist/src/Time.js CHANGED
@@ -3,17 +3,25 @@
3
3
  *
4
4
  * @module
5
5
  */
6
+ import { DateIso } from "./Type.js";
6
7
  /** Creates a {@link Time} using Date.now(). */
7
- export const createTime = () => ({
8
- now: () => Date.now(),
9
- });
8
+ export const createTime = () => {
9
+ const time = {
10
+ now: () => {
11
+ const iso = time.nowIso();
12
+ return new globalThis.Date(iso).getTime();
13
+ },
14
+ nowIso: () => DateIso.orThrow(new globalThis.Date().toISOString()),
15
+ };
16
+ return time;
17
+ };
10
18
  /**
11
19
  * Creates a {@link Time} that returns a monotonically increasing number based on
12
20
  * a queueMicrotask.
13
21
  */
14
22
  export const createTestTime = () => {
15
23
  let now = 0;
16
- return {
24
+ const time = {
17
25
  now: () => {
18
26
  const current = now;
19
27
  queueMicrotask(() => {
@@ -21,5 +29,80 @@ export const createTestTime = () => {
21
29
  });
22
30
  return current;
23
31
  },
32
+ nowIso: () => DateIso.orThrow(new globalThis.Date(time.now()).toISOString()),
33
+ };
34
+ return time;
35
+ };
36
+ /**
37
+ * Converts a duration to milliseconds.
38
+ *
39
+ * Accepts either a {@link DurationString} (e.g., "5m", "1h 30m") or milliseconds
40
+ * as {@link NonNegativeInt}.
41
+ *
42
+ * ### Example
43
+ *
44
+ * ```ts
45
+ * durationToNonNegativeInt("0ms"); // 0 ✅
46
+ * durationToNonNegativeInt("500ms"); // 500 ✅
47
+ * durationToNonNegativeInt("30s"); // 30000 ✅
48
+ * durationToNonNegativeInt("5m"); // 300000 ✅
49
+ * durationToNonNegativeInt("12h"); // 43200000 ✅
50
+ * durationToNonNegativeInt("7d"); // 604800000 ✅
51
+ * durationToNonNegativeInt("2h 45m"); // 9900000 ✅
52
+ * durationToNonNegativeInt(5000); // 5000 ✅ (already milliseconds)
53
+ * ```
54
+ */
55
+ export const durationToNonNegativeInt = (duration) => {
56
+ // If it's already a NonNegativeInt (milliseconds), return as-is
57
+ if (typeof duration === "number") {
58
+ return duration;
59
+ }
60
+ // Parse duration string without regex to avoid ReDoS vulnerabilities
61
+ const units = {
62
+ ms: 1,
63
+ s: 1000,
64
+ m: 60000,
65
+ h: 3600000,
66
+ d: 86400000, // 24 * 60 * 60 * 1000
24
67
  };
68
+ let total = 0;
69
+ let i = 0;
70
+ while (i < duration.length) {
71
+ // Skip whitespace
72
+ while (i < duration.length && duration[i] === " ") {
73
+ i++;
74
+ }
75
+ if (i >= duration.length)
76
+ break;
77
+ // Parse number
78
+ let numStr = "";
79
+ while (i < duration.length && duration[i] >= "0" && duration[i] <= "9") {
80
+ numStr += duration[i];
81
+ i++;
82
+ }
83
+ if (numStr === "")
84
+ break;
85
+ // Parse unit (ms or single char s/m/h/d)
86
+ let unit = "";
87
+ if (i < duration.length) {
88
+ if (duration[i] === "m" &&
89
+ i + 1 < duration.length &&
90
+ duration[i + 1] === "s") {
91
+ unit = "ms";
92
+ i += 2;
93
+ }
94
+ else if (duration[i] === "s" ||
95
+ duration[i] === "m" ||
96
+ duration[i] === "h" ||
97
+ duration[i] === "d") {
98
+ unit = duration[i];
99
+ i++;
100
+ }
101
+ }
102
+ if (unit === "")
103
+ break;
104
+ const value = parseInt(numStr, 10);
105
+ total += value * units[unit];
106
+ }
107
+ return total;
25
108
  };