@evolu/common 8.0.0-next.3 → 8.0.0-next.5

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 (81) hide show
  1. package/dist/src/Assert.d.ts +10 -5
  2. package/dist/src/Assert.d.ts.map +1 -1
  3. package/dist/src/Assert.js +10 -5
  4. package/dist/src/Error.d.ts +5 -3
  5. package/dist/src/Error.d.ts.map +1 -1
  6. package/dist/src/Error.js +16 -0
  7. package/dist/src/Identicon.js +4 -4
  8. package/dist/src/LockManager.d.ts +74 -0
  9. package/dist/src/LockManager.d.ts.map +1 -0
  10. package/dist/src/LockManager.js +103 -0
  11. package/dist/src/Microtask.d.ts.map +1 -1
  12. package/dist/src/Microtask.js +90 -26
  13. package/dist/src/Object.d.ts.map +1 -1
  14. package/dist/src/RefCount.d.ts.map +1 -1
  15. package/dist/src/RefCount.js +153 -68
  16. package/dist/src/Resource.d.ts +6 -6
  17. package/dist/src/Resource.d.ts.map +1 -1
  18. package/dist/src/Resource.js +211 -162
  19. package/dist/src/Schedule.d.ts +5 -5
  20. package/dist/src/Schedule.d.ts.map +1 -1
  21. package/dist/src/Schedule.js +53 -44
  22. package/dist/src/Sqlite.d.ts +2 -0
  23. package/dist/src/Sqlite.d.ts.map +1 -1
  24. package/dist/src/Sqlite.js +66 -41
  25. package/dist/src/Task.d.ts +99 -123
  26. package/dist/src/Task.d.ts.map +1 -1
  27. package/dist/src/Task.js +273 -256
  28. package/dist/src/Test.d.ts +14 -15
  29. package/dist/src/Test.d.ts.map +1 -1
  30. package/dist/src/Time.d.ts +5 -0
  31. package/dist/src/Time.d.ts.map +1 -1
  32. package/dist/src/Time.js +5 -0
  33. package/dist/src/Type.d.ts +7 -0
  34. package/dist/src/Type.d.ts.map +1 -1
  35. package/dist/src/Type.js +7 -0
  36. package/dist/src/Types.d.ts +0 -8
  37. package/dist/src/Types.d.ts.map +1 -1
  38. package/dist/src/WebSocket.d.ts.map +1 -1
  39. package/dist/src/WebSocket.js +7 -6
  40. package/dist/src/Worker.d.ts +42 -8
  41. package/dist/src/Worker.d.ts.map +1 -1
  42. package/dist/src/Worker.js +292 -112
  43. package/dist/src/index.d.ts +1 -0
  44. package/dist/src/index.d.ts.map +1 -1
  45. package/dist/src/index.js +1 -0
  46. package/dist/src/local-first/Db.d.ts +5 -4
  47. package/dist/src/local-first/Db.d.ts.map +1 -1
  48. package/dist/src/local-first/Db.js +43 -37
  49. package/dist/src/local-first/Evolu.d.ts +3 -2
  50. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  51. package/dist/src/local-first/Evolu.js +90 -80
  52. package/dist/src/local-first/Owner.d.ts.map +1 -1
  53. package/dist/src/local-first/Shared.d.ts +65 -54
  54. package/dist/src/local-first/Shared.d.ts.map +1 -1
  55. package/dist/src/local-first/Shared.js +390 -376
  56. package/package.json +1 -1
  57. package/src/Array.ts +8 -8
  58. package/src/Assert.ts +11 -6
  59. package/src/Error.ts +5 -4
  60. package/src/Identicon.ts +4 -4
  61. package/src/LockManager.ts +181 -0
  62. package/src/Microtask.ts +17 -11
  63. package/src/Object.ts +3 -3
  64. package/src/RefCount.ts +25 -21
  65. package/src/Resource.ts +63 -56
  66. package/src/Schedule.ts +70 -48
  67. package/src/Set.ts +4 -4
  68. package/src/Sqlite.ts +43 -31
  69. package/src/Task.ts +392 -430
  70. package/src/Test.ts +14 -15
  71. package/src/Time.ts +7 -0
  72. package/src/Type.ts +9 -0
  73. package/src/Types.ts +0 -9
  74. package/src/WebSocket.ts +7 -6
  75. package/src/Worker.ts +195 -53
  76. package/src/index.ts +1 -0
  77. package/src/local-first/Db.ts +86 -61
  78. package/src/local-first/Evolu.ts +74 -68
  79. package/src/local-first/Owner.ts +1 -1
  80. package/src/local-first/Query.ts +1 -1
  81. package/src/local-first/Shared.ts +485 -476
package/src/Resource.ts CHANGED
@@ -113,21 +113,21 @@ export const createResourceRef = <T extends Resource, D>(
113
113
  unabortable<ResourceRef<T, D>, never, D>(async (run) => {
114
114
  const resourceRefRun = run.create();
115
115
 
116
- await using stack = new AsyncDisposableStack();
117
- stack.use(resourceRefRun);
116
+ await using disposer = new AsyncDisposableStack();
117
+ disposer.use(resourceRefRun);
118
118
 
119
119
  const initial = await resourceRefRun(create);
120
120
  if (!initial.ok) return initial;
121
121
 
122
122
  let current = createOwnedResource(initial.value);
123
123
 
124
- const mutex = stack.use(createMutex());
125
- stack.defer(() => current.stack.disposeAsync());
124
+ const mutex = disposer.use(createMutex());
125
+ disposer.defer(() => current.stack.disposeAsync());
126
126
  // Register as the last so disposal aborts further calls first.
127
127
  // Repeated registration is safe because disposal is idempotent.
128
- stack.use(resourceRefRun);
128
+ disposer.use(resourceRefRun);
129
129
 
130
- const moved = stack.move();
130
+ const disposables = disposer.move();
131
131
 
132
132
  return ok({
133
133
  get: () => resourceRefRun(mutex.withLock(() => ok(current.resource))),
@@ -145,7 +145,7 @@ export const createResourceRef = <T extends Resource, D>(
145
145
  ),
146
146
  ),
147
147
 
148
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
148
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
149
149
  });
150
150
  });
151
151
 
@@ -165,12 +165,6 @@ export interface SharedResource<
165
165
  T extends Resource,
166
166
  D = unknown,
167
167
  > extends AsyncDisposable {
168
- /** Returns the current shared-resource state for monitoring/debugging. */
169
- readonly snapshot: () => SharedResourceSnapshot;
170
-
171
- /** Returns the current resource, or `undefined` if absent. */
172
- readonly get: () => BorrowedResource<T> | undefined;
173
-
174
168
  /**
175
169
  * Acquires a shared reference.
176
170
  *
@@ -203,6 +197,12 @@ export interface SharedResource<
203
197
 
204
198
  /** Returns the current acquire count. */
205
199
  readonly getCount: Task<NonNegativeInt, never, D>;
200
+
201
+ /** Returns the current resource, or `undefined` if absent. */
202
+ readonly get: () => BorrowedResource<T> | undefined;
203
+
204
+ /** Returns the current shared-resource state for monitoring/debugging. */
205
+ readonly snapshot: () => SharedResourceSnapshot;
206
206
  }
207
207
 
208
208
  /** Snapshot returned by {@link SharedResource.snapshot}. */
@@ -237,38 +237,31 @@ export const createSharedResource = <T extends Resource, D>(
237
237
  create: Task<T, never, D>,
238
238
  { idleDisposeAfter, onDisposed }: SharedResourceOptions = {},
239
239
  ): Task<SharedResource<T, D>, never, D> =>
240
- unabortable<SharedResource<T, D>, never, D>((run) => {
240
+ unabortable<SharedResource<T, D>, never, D>(async (run) => {
241
241
  const sharedResourceRun = run.create();
242
242
  let current: OwnedResource<T> | undefined;
243
243
  let idleDisposeFiber: Fiber<void, AbortError, D> | undefined;
244
244
 
245
- const stack = new AsyncDisposableStack();
246
- const refCount = stack.use(createRefCount());
245
+ await using disposer = new AsyncDisposableStack();
246
+ const refCount = disposer.use(createRefCount());
247
247
 
248
- const mutex = stack.use(createMutex());
248
+ const mutex = disposer.use(createMutex());
249
249
 
250
250
  const disposeCurrent = async () => {
251
251
  if (!current) return;
252
- await using stack = new AsyncDisposableStack();
253
- if (onDisposed) stack.defer(onDisposed);
254
- stack.use(current.stack);
252
+ await using disposer = new AsyncDisposableStack();
253
+ if (onDisposed) disposer.defer(onDisposed);
254
+ disposer.use(current.stack);
255
255
  current = undefined;
256
256
  };
257
- stack.defer(disposeCurrent);
257
+ disposer.defer(disposeCurrent);
258
258
 
259
259
  // Register as the last so disposal aborts further calls first.
260
- stack.use(sharedResourceRun);
260
+ disposer.use(sharedResourceRun);
261
261
 
262
- const moved = stack.move();
262
+ const disposables = disposer.move();
263
263
 
264
264
  return ok({
265
- snapshot: () => ({
266
- isIdle: refCount.getCount() === 0 && !current && !idleDisposeFiber,
267
- mutex: mutex.snapshot(),
268
- }),
269
-
270
- get: () => current?.resource,
271
-
272
265
  acquire: unabortable<BorrowedResource<T>, never, D>(() =>
273
266
  sharedResourceRun(
274
267
  mutex.withLock(async (run) => {
@@ -320,7 +313,14 @@ export const createSharedResource = <T extends Resource, D>(
320
313
  getCount: () =>
321
314
  sharedResourceRun(mutex.withLock(() => ok(refCount.getCount()))),
322
315
 
323
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
316
+ get: () => current?.resource,
317
+
318
+ snapshot: () => ({
319
+ isIdle: refCount.getCount() === 0 && !current && !idleDisposeFiber,
320
+ mutex: mutex.snapshot(),
321
+ }),
322
+
323
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
324
324
  });
325
325
  });
326
326
 
@@ -356,9 +356,6 @@ export interface SharedResourceByKey<
356
356
  T extends Resource,
357
357
  D = unknown,
358
358
  > extends AsyncDisposable {
359
- /** Returns the current resource for `key`, or `undefined` if absent. */
360
- readonly get: (key: K) => BorrowedResource<T> | undefined;
361
-
362
359
  /**
363
360
  * Acquires the shared resource for `key`, creating it on first use.
364
361
  *
@@ -386,6 +383,9 @@ export interface SharedResourceByKey<
386
383
  /** Returns the current acquire count for `key`. Missing keys return `0`. */
387
384
  readonly getCount: (key: K) => Task<NonNegativeInt, never, D>;
388
385
 
386
+ /** Returns the current resource for `key`, or `undefined` if absent. */
387
+ readonly get: (key: K) => BorrowedResource<T> | undefined;
388
+
389
389
  /** Returns current keyed resources and their per-key mutex state. */
390
390
  readonly snapshot: () => SharedResourceByKeySnapshot<K, T>;
391
391
  }
@@ -436,29 +436,28 @@ export function createSharedResourceByKey<
436
436
  onDisposed,
437
437
  }: SharedResourceByKeyOptions<K, L> = {},
438
438
  ): Task<SharedResourceByKey<K, T, D>, never, D> {
439
- return unabortable<SharedResourceByKey<K, T, D>, never, D>((run) => {
439
+ return unabortable<SharedResourceByKey<K, T, D>, never, D>(async (run) => {
440
440
  const sharedResourceByKeyRun = run.create();
441
441
  const sharedResourcesByKey = createLookupMap<K, SharedResource<T, D>, L>({
442
442
  lookup,
443
443
  });
444
444
 
445
- const stack = new AsyncDisposableStack();
445
+ await using disposer = new AsyncDisposableStack();
446
446
 
447
- const mutexByKey = stack.use(createMutexByKey<K, L>({ lookup }));
448
- stack.defer(async () => {
449
- const stack = new AsyncDisposableStack();
450
- for (const resource of sharedResourcesByKey.values()) stack.use(resource);
451
- await stack.disposeAsync();
447
+ const mutexByKey = disposer.use(createMutexByKey<K, L>({ lookup }));
448
+ disposer.defer(async () => {
449
+ await using disposer = new AsyncDisposableStack();
450
+ for (const resource of sharedResourcesByKey.values())
451
+ disposer.use(resource);
452
+ await disposer.disposeAsync();
452
453
  sharedResourcesByKey.clear();
453
454
  });
454
455
  // Register as the last so disposal aborts further calls first.
455
- stack.use(sharedResourceByKeyRun);
456
+ disposer.use(sharedResourceByKeyRun);
456
457
 
457
- const moved = stack.move();
458
+ const disposables = disposer.move();
458
459
 
459
460
  return ok({
460
- get: (key) => sharedResourcesByKey.get(key)?.get(),
461
-
462
461
  acquire: (key) =>
463
462
  unabortable<BorrowedResource<T>, never, D>(() =>
464
463
  sharedResourceByKeyRun(
@@ -521,6 +520,8 @@ export function createSharedResourceByKey<
521
520
  }),
522
521
  ),
523
522
 
523
+ get: (key) => sharedResourcesByKey.get(key)?.get(),
524
+
524
525
  snapshot: () => {
525
526
  const resourcesByKey = new Map<K, BorrowedResource<T>>();
526
527
  const mutexSnapshotsByKey = new Map<K, SemaphoreSnapshot | null>();
@@ -539,7 +540,7 @@ export function createSharedResourceByKey<
539
540
  };
540
541
  },
541
542
 
542
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
543
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
543
544
  });
544
545
  });
545
546
  }
@@ -661,27 +662,31 @@ export function createSharedResourceByKeyWithClaims<
661
662
  return unabortable<SharedResourceByKeyWithClaims<K, C, T, D>, never, D>(
662
663
  async (run) => {
663
664
  const sharedResourceClaimsRun = run.create();
664
- await using stack = new AsyncDisposableStack();
665
+ await using disposer = new AsyncDisposableStack();
665
666
 
666
667
  const keyByClaim = createRelation<C, K, LC, LK>({
667
668
  lookupA: claimLookup,
668
669
  lookupB: resourceLookup,
669
670
  });
670
- const pairRefCountsByClaim = stack.adopt(
671
+ const pairRefCountsByClaim = disposer.adopt(
671
672
  createLookupMap<C, RefCountByKey<K>, LC>({ lookup: claimLookup }),
672
673
  (pairRefCountsByClaim) => {
674
+ using disposer = new DisposableStack();
675
+ disposer.defer(() => {
676
+ pairRefCountsByClaim.clear();
677
+ });
678
+
673
679
  for (const pairRefCountByKey of pairRefCountsByClaim.values()) {
674
- pairRefCountByKey[Symbol.dispose]();
680
+ disposer.use(pairRefCountByKey);
675
681
  }
676
- pairRefCountsByClaim.clear();
677
682
  },
678
683
  );
679
684
 
680
- const mutexByKey = stack.use(
685
+ const mutexByKey = disposer.use(
681
686
  createMutexByKey<K, LK>({ lookup: resourceLookup }),
682
687
  );
683
688
 
684
- stack.defer(() => {
689
+ disposer.defer(() => {
685
690
  keyByClaim.clear();
686
691
  });
687
692
 
@@ -692,10 +697,12 @@ export function createSharedResourceByKeyWithClaims<
692
697
  }),
693
698
  );
694
699
  assertNotAborted(sharedResourcesByKeyResult);
695
- const sharedResourcesByKey = stack.use(sharedResourcesByKeyResult.value);
700
+ const sharedResourcesByKey = disposer.use(
701
+ sharedResourcesByKeyResult.value,
702
+ );
696
703
 
697
704
  // Register as the last so disposal aborts further calls first.
698
- stack.use(sharedResourceClaimsRun);
705
+ disposer.use(sharedResourceClaimsRun);
699
706
 
700
707
  /** Asserts that one call does not contain the same logical key twice. */
701
708
  const assertNoDuplicateResourceKeys = (
@@ -710,7 +717,7 @@ export function createSharedResourceByKeyWithClaims<
710
717
  );
711
718
  };
712
719
 
713
- const moved = stack.move();
720
+ const disposables = disposer.move();
714
721
 
715
722
  return ok({
716
723
  addClaim: (claim, resourceKeys) =>
@@ -840,7 +847,7 @@ export function createSharedResourceByKeyWithClaims<
840
847
  return resources;
841
848
  },
842
849
 
843
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
850
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
844
851
  });
845
852
  },
846
853
  );
package/src/Schedule.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * @module
5
5
  */
6
6
 
7
+ import { assertType } from "./Assert.js";
7
8
  import { fibonacciAt, FibonacciIndex, increment } from "./Number.js";
8
9
  import type { RandomDep } from "./Random.js";
9
10
  import { done, err, type NextResult, ok } from "./Result.js";
@@ -13,9 +14,15 @@ import {
13
14
  durationToMillis,
14
15
  Millis,
15
16
  minMillis,
17
+ saturateMillis,
16
18
  type TimeDep,
17
19
  } from "./Time.js";
18
- import { onePositiveInt, PositiveInt } from "./Type.js";
20
+ import {
21
+ NonNegativeFiniteNumber,
22
+ NonNegativeInt,
23
+ onePositiveInt,
24
+ PositiveInt,
25
+ } from "./Type.js";
19
26
  import type { Predicate } from "./Types.js";
20
27
 
21
28
  /**
@@ -113,11 +120,11 @@ const createScheduleStepMetrics = (
113
120
  return () => {
114
121
  const now = deps.time.now();
115
122
  const currentAttempt = attempt;
116
- attempt = increment(attempt) as PositiveInt;
123
+ attempt = PositiveInt.orThrow(increment(attempt));
117
124
  start ??= now;
118
- const elapsed = (now - start) as Millis;
125
+ const elapsed = saturateMillis(now - start);
119
126
  const elapsedSincePrevious =
120
- previous === null ? (0 as Millis) : ((now - previous) as Millis);
127
+ previous === null ? minMillis : saturateMillis(now - previous);
121
128
  previous = now;
122
129
  return { attempt: currentAttempt, elapsed, elapsedSincePrevious };
123
130
  };
@@ -235,18 +242,24 @@ export const spaced =
235
242
  *
236
243
  * @group Constructors
237
244
  */
238
- export const exponential =
239
- (base: Duration, factor = 2): Schedule<Millis> =>
240
- () => {
245
+ export const exponential = (base: Duration, factor = 2): Schedule<Millis> => {
246
+ assertType(
247
+ NonNegativeFiniteNumber,
248
+ factor,
249
+ "Expected factor to be a non-negative finite number.",
250
+ );
251
+
252
+ return () => {
241
253
  const baseMs = durationToMillis(base);
242
254
  let attempt = 0;
243
255
  return () => {
244
256
  attempt++;
245
257
  const rawDelay = baseMs * Math.pow(factor, attempt - 1);
246
- const delay = Millis.orThrow(Math.max(0, Math.round(rawDelay)));
258
+ const delay = saturateMillis(rawDelay);
247
259
  return ok([delay, delay]);
248
260
  };
249
261
  };
262
+ };
250
263
 
251
264
  /**
252
265
  * Linear backoff schedule.
@@ -276,7 +289,7 @@ export const linear =
276
289
  let attempt = 0;
277
290
  return () => {
278
291
  attempt++;
279
- const delay = Millis.orThrow(ms * attempt);
292
+ const delay = saturateMillis(ms * attempt);
280
293
  return ok([delay, delay]);
281
294
  };
282
295
  };
@@ -308,12 +321,10 @@ export const fibonacci =
308
321
  (initial: Duration): Schedule<Millis> =>
309
322
  () => {
310
323
  const ms = durationToMillis(initial);
311
- let index = 1;
324
+ let index = FibonacciIndex.orThrow(1);
312
325
  return () => {
313
- const delay = Millis.orThrow(
314
- ms * fibonacciAt(FibonacciIndex.orThrow(index)),
315
- );
316
- index++;
326
+ const delay = saturateMillis(ms * fibonacciAt(index));
327
+ index = FibonacciIndex.orNull(increment(index)) ?? index;
317
328
  return ok([delay, delay]);
318
329
  };
319
330
  };
@@ -356,7 +367,7 @@ export const fixed =
356
367
  const remainder = intervalMs === 0 ? 0 : elapsed % intervalMs;
357
368
  const boundary = intervalMs - remainder;
358
369
  const delay = runningBehind ? 0 : boundary;
359
- return ok([count++, delay as Millis]);
370
+ return ok([count++, saturateMillis(delay)]);
360
371
  };
361
372
  };
362
373
 
@@ -388,7 +399,7 @@ export const windowed =
388
399
  const { elapsed } = metrics();
389
400
  const remainder = intervalMs === 0 ? 0 : elapsed % intervalMs;
390
401
  const delay = intervalMs === 0 ? 0 : intervalMs - remainder;
391
- return ok([count++, delay as Millis]);
402
+ return ok([count++, saturateMillis(delay)]);
392
403
  };
393
404
  };
394
405
 
@@ -563,18 +574,22 @@ export const unfoldSchedule =
563
574
  *
564
575
  * @group Limiting
565
576
  */
566
- export const take =
567
- (n: number) =>
568
- <Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
569
- (deps) => {
570
- const step = schedule(deps);
571
- let attempt = 0;
572
- return (input) => {
573
- attempt++;
574
- if (attempt > n) return err(done());
575
- return step(input);
577
+ export const take = (n: number) => {
578
+ assertType(NonNegativeInt, n, "Expected n to be a non-negative integer.");
579
+
580
+ return <Output, Input>(
581
+ schedule: Schedule<Output, Input>,
582
+ ): Schedule<Output, Input> =>
583
+ (deps) => {
584
+ const step = schedule(deps);
585
+ let attempt = 0;
586
+ return (input) => {
587
+ attempt++;
588
+ if (attempt > n) return err(done());
589
+ return step(input);
590
+ };
576
591
  };
577
- };
592
+ };
578
593
 
579
594
  /**
580
595
  * Limits schedule execution to a maximum elapsed time.
@@ -659,19 +674,27 @@ export const maxDelay = (max: Duration) => {
659
674
  *
660
675
  * @group Delay
661
676
  */
662
- export const jitter =
663
- (factor = 0.5) =>
664
- <Output, Input>(schedule: Schedule<Output, Input>): Schedule<Output, Input> =>
665
- (deps) => {
666
- const step = schedule(deps);
667
- return (input) => {
668
- const result = step(input);
669
- if (!result.ok) return result;
670
- const [output, delay] = result.value;
671
- const jittered = delay * (1 - factor + deps.random.next() * 2 * factor);
672
- return ok([output, Millis.orThrow(Math.max(0, Math.round(jittered)))]);
677
+ export const jitter = (factor = 0.5) => {
678
+ assertType(
679
+ NonNegativeFiniteNumber,
680
+ factor,
681
+ "Expected factor to be a non-negative finite number.",
682
+ );
683
+
684
+ return <Output, Input>(
685
+ schedule: Schedule<Output, Input>,
686
+ ): Schedule<Output, Input> =>
687
+ (deps) => {
688
+ const step = schedule(deps);
689
+ return (input) => {
690
+ const result = step(input);
691
+ if (!result.ok) return result;
692
+ const [output, delay] = result.value;
693
+ const jittered = delay * (1 - factor + deps.random.next() * 2 * factor);
694
+ return ok([output, saturateMillis(jittered)]);
695
+ };
673
696
  };
674
- };
697
+ };
675
698
 
676
699
  /**
677
700
  * Adds an initial delay before the first attempt.
@@ -755,7 +778,7 @@ export const modifyDelay =
755
778
  const result = step(input);
756
779
  if (!result.ok) return result;
757
780
  const [output, delay] = result.value;
758
- return ok([output, Millis.orThrow(Math.max(0, Math.round(f(delay))))]);
781
+ return ok([output, saturateMillis(f(delay))]);
759
782
  };
760
783
  };
761
784
 
@@ -787,8 +810,7 @@ export const compensate =
787
810
  const result = step(input);
788
811
  if (!result.ok) return result;
789
812
  const [output, delay] = result.value;
790
- const adjusted = Math.max(0, delay - elapsedSincePrevious);
791
- return ok([output, adjusted as Millis]);
813
+ return ok([output, saturateMillis(delay - elapsedSincePrevious)]);
792
814
  };
793
815
  };
794
816
 
@@ -1042,15 +1064,15 @@ export function passthrough<Output, Input>(
1042
1064
  * // Outputs: 100, 300, 700, 1500, ... (cumulative)
1043
1065
  *
1044
1066
  * // Collect all outputs
1045
- * const collected = foldSchedule([] as Millis[], (acc, delay: Millis) => [
1046
- * ...acc,
1047
- * delay,
1048
- * ])(take(3)(spaced("1s")));
1067
+ * const collected = foldSchedule<ReadonlyArray<Millis>, Millis>(
1068
+ * [],
1069
+ * (acc, delay) => [...acc, delay],
1070
+ * )(take(3)(spaced("1s")));
1049
1071
  * // Outputs: [1000], [1000, 1000], [1000, 1000, 1000]
1050
1072
  *
1051
1073
  * // Count attempts with custom output
1052
1074
  * const counted = foldSchedule(
1053
- * { attempts: 0, lastDelay: 0 as Millis },
1075
+ * { attempts: 0, lastDelay: minMillis },
1054
1076
  * (acc, delay: Millis) => ({
1055
1077
  * attempts: acc.attempts + 1,
1056
1078
  * lastDelay: delay,
@@ -1338,7 +1360,7 @@ export const unionSchedules =
1338
1360
  const [outputA, delayA] = resultA.value;
1339
1361
  const [outputB, delayB] = resultB.value;
1340
1362
  // Use minimum delay, output from the one with smaller delay
1341
- const minDelay = Math.min(delayA, delayB) as Millis;
1363
+ const minDelay = Millis.orThrow(Math.min(delayA, delayB));
1342
1364
  return delayA <= delayB
1343
1365
  ? ok([outputA, minDelay])
1344
1366
  : ok([outputB, minDelay]);
package/src/Set.ts CHANGED
@@ -65,7 +65,7 @@ export function createSet<T>(
65
65
  /** Possibly empty set. */
66
66
  export function createSet<T>(items: ReadonlyArray<T>): ReadonlySet<T>;
67
67
  export function createSet<T>(items: ReadonlyArray<T>): ReadonlySet<T> {
68
- return new Set(items) as ReadonlySet<T>;
68
+ return new Set(items);
69
69
  }
70
70
 
71
71
  /**
@@ -154,7 +154,7 @@ export const deleteFromSet = <T>(
154
154
  ): ReadonlySet<T> => {
155
155
  const next = new Set(set);
156
156
  next.delete(item);
157
- return next as ReadonlySet<T>;
157
+ return next;
158
158
  };
159
159
 
160
160
  /**
@@ -191,7 +191,7 @@ export function mapSet<T, U>(
191
191
  for (const item of set) {
192
192
  next.add(mapper(item));
193
193
  }
194
- return next as ReadonlySet<U>;
194
+ return next;
195
195
  }
196
196
 
197
197
  /**
@@ -229,7 +229,7 @@ export function filterSet<T>(
229
229
  next.add(item);
230
230
  }
231
231
  }
232
- return next as ReadonlySet<T>;
232
+ return next;
233
233
  }
234
234
 
235
235
  /**
package/src/Sqlite.ts CHANGED
@@ -191,6 +191,9 @@ export type SqliteRow = Record<string, SqliteValue>;
191
191
  export interface SqliteDriver extends Disposable {
192
192
  readonly exec: (query: SqliteQuery) => SqliteExecResult;
193
193
 
194
+ /** Disposes this driver, then deletes its persistent database file. */
195
+ readonly deleteDatabase: () => void;
196
+
194
197
  /**
195
198
  * Returns bytes backed by {@link ArrayBuffer}.
196
199
  *
@@ -234,18 +237,18 @@ export const createSqlite =
234
237
  async (run) => {
235
238
  const { createSqliteDriver } = run.deps;
236
239
  const console = run.deps.console.child("sql");
237
- await using stack = new AsyncDisposableStack();
240
+ await using disposer = new AsyncDisposableStack();
238
241
 
239
242
  const driverResult = await run(createSqliteDriver(name, options));
240
243
  if (!driverResult.ok) return driverResult;
241
- const driver = stack.use(driverResult.value);
244
+ const driver = disposer.use(driverResult.value);
242
245
  console.debug("SQLite driver created");
243
246
 
244
- const moved = stack.move();
247
+ const disposables = disposer.move();
245
248
 
246
249
  const sqlite: Sqlite = {
247
250
  exec: <R extends SqliteRow = SqliteRow>(query: SqliteQuery) => {
248
- assertNotDisposed(moved);
251
+ assertNotDisposed(disposables);
249
252
  console.debug({ query });
250
253
 
251
254
  const label =
@@ -274,18 +277,17 @@ export const createSqlite =
274
277
  },
275
278
 
276
279
  transaction: ((callback: () => Result<unknown, unknown> | void) => {
277
- assertNotDisposed(moved);
280
+ assertNotDisposed(disposables);
278
281
  console.debug("begin");
279
282
  driver.exec(sql`begin;`);
280
283
 
284
+ using rollback = new DisposableStack();
281
285
  let shouldRollback = true;
282
- using _rollback = {
283
- [Symbol.dispose]: () => {
284
- if (!shouldRollback) return;
285
- console.debug("rollback");
286
- driver.exec(sql`rollback;`);
287
- },
288
- };
286
+ rollback.defer(() => {
287
+ if (!shouldRollback) return;
288
+ console.debug("rollback");
289
+ driver.exec(sql`rollback;`);
290
+ });
289
291
 
290
292
  const result = callback();
291
293
  if (result != null && !result.ok) return result;
@@ -298,11 +300,11 @@ export const createSqlite =
298
300
  }) as SqliteTransaction,
299
301
 
300
302
  export: () => {
301
- assertNotDisposed(moved);
303
+ assertNotDisposed(disposables);
302
304
  return driver.export();
303
305
  },
304
306
 
305
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
307
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
306
308
  };
307
309
 
308
310
  return ok(sqlite);
@@ -318,17 +320,18 @@ export const testSetupSqlite = async (
318
320
  readonly sqlite: Sqlite;
319
321
  }
320
322
  > => {
321
- await using stack = new AsyncDisposableStack();
322
- const run = stack.use(testCreateRun(deps));
323
- const sqlite = stack.use(
323
+ await using disposer = new AsyncDisposableStack();
324
+ const run = disposer.use(testCreateRun(deps));
325
+ const sqlite = disposer.use(
324
326
  await run.orThrow(createSqlite(testName, { mode: "memory" })),
325
327
  );
326
- const moved = stack.move();
328
+ const runWithSqlite = disposer.use(run.create({ ...run.deps, sqlite }));
329
+ const disposables = disposer.move();
327
330
 
328
331
  return {
329
- run: run.addDeps({ sqlite }),
332
+ run: runWithSqlite,
330
333
  sqlite,
331
- [Symbol.asyncDispose]: () => moved.disposeAsync(),
334
+ [Symbol.asyncDispose]: () => disposables.disposeAsync(),
332
335
  };
333
336
  };
334
337
 
@@ -377,27 +380,36 @@ export const createPreparedStatementsCache = <P>(
377
380
  factory: (sql: SafeSql) => P,
378
381
  disposeFn: (statement: P) => void,
379
382
  ): PreparedStatements<P> => {
380
- let isDisposed = false;
381
- const cache = new Map<SafeSql, P>();
383
+ using disposer = new DisposableStack();
384
+ const statementsBySql = new Map<SafeSql, P>();
385
+
386
+ disposer.defer(() => {
387
+ using disposer = new DisposableStack();
388
+ disposer.defer(() => {
389
+ statementsBySql.clear();
390
+ });
391
+
392
+ for (const statement of statementsBySql.values()) {
393
+ disposer.adopt(statement, disposeFn);
394
+ }
395
+ });
396
+
397
+ const disposables = disposer.move();
382
398
 
383
399
  return {
384
400
  get: (query, alwaysPrepare) => {
401
+ assertNotDisposed(disposables);
385
402
  if (alwaysPrepare !== true && !query.options?.prepare)
386
403
  return null as never;
387
- let statement = cache.get(query.sql);
404
+ let statement = statementsBySql.get(query.sql);
388
405
  if (!statement) {
389
406
  statement = factory(query.sql);
390
- cache.set(query.sql, statement);
407
+ statementsBySql.set(query.sql, statement);
391
408
  }
392
- return statement as never;
409
+ return statement;
393
410
  },
394
411
 
395
- [Symbol.dispose]: () => {
396
- if (isDisposed) return;
397
- isDisposed = true;
398
- cache.forEach(disposeFn);
399
- cache.clear();
400
- },
412
+ [Symbol.dispose]: () => disposables.dispose(),
401
413
  };
402
414
  };
403
415