@evolu/common 6.0.1-preview.29 → 6.0.1-preview.30

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 (66) hide show
  1. package/dist/src/Buffer.js +7 -6
  2. package/dist/src/Callbacks.d.ts +53 -0
  3. package/dist/src/Callbacks.d.ts.map +1 -0
  4. package/dist/src/Callbacks.js +25 -0
  5. package/dist/src/Crypto.js +3 -3
  6. package/dist/src/Evolu/Db.d.ts +1 -1
  7. package/dist/src/Evolu/Db.d.ts.map +1 -1
  8. package/dist/src/Evolu/Evolu.d.ts.map +1 -1
  9. package/dist/src/Evolu/Evolu.js +12 -12
  10. package/dist/src/Evolu/Owner.d.ts +1 -1
  11. package/dist/src/Evolu/Owner.d.ts.map +1 -1
  12. package/dist/src/Evolu/Owner.js +2 -2
  13. package/dist/src/Evolu/Protocol.d.ts +12 -12
  14. package/dist/src/Evolu/Protocol.d.ts.map +1 -1
  15. package/dist/src/Evolu/Protocol.js +30 -27
  16. package/dist/src/Evolu/Relay.d.ts +4 -3
  17. package/dist/src/Evolu/Relay.d.ts.map +1 -1
  18. package/dist/src/Evolu/Relay.js +6 -8
  19. package/dist/src/Evolu/Storage.d.ts +7 -8
  20. package/dist/src/Evolu/Storage.d.ts.map +1 -1
  21. package/dist/src/Evolu/Storage.js +5 -5
  22. package/dist/src/Evolu/Sync.d.ts.map +1 -1
  23. package/dist/src/Evolu/Sync.js +4 -3
  24. package/dist/src/Evolu/Timestamp.d.ts +1 -0
  25. package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
  26. package/dist/src/Evolu/Timestamp.js +1 -0
  27. package/dist/src/Instances.d.ts +1 -1
  28. package/dist/src/Instances.d.ts.map +1 -1
  29. package/dist/src/Instances.js +1 -1
  30. package/dist/src/Number.d.ts +2 -2
  31. package/dist/src/Number.d.ts.map +1 -1
  32. package/dist/src/Number.js +5 -4
  33. package/dist/src/{RefCountedResourceManager.d.ts → Resources.d.ts} +14 -15
  34. package/dist/src/Resources.d.ts.map +1 -0
  35. package/dist/src/{RefCountedResourceManager.js → Resources.js} +24 -24
  36. package/dist/src/Result.d.ts +1 -1
  37. package/dist/src/Skiplist.js +2 -1
  38. package/dist/src/Task.js +3 -3
  39. package/dist/src/Time.js +2 -2
  40. package/dist/src/index.d.ts +2 -1
  41. package/dist/src/index.d.ts.map +1 -1
  42. package/dist/src/index.js +2 -1
  43. package/package.json +1 -1
  44. package/src/Buffer.ts +6 -6
  45. package/src/{CallbackRegistry.ts → Callbacks.ts} +28 -29
  46. package/src/Crypto.ts +3 -3
  47. package/src/Evolu/Db.ts +1 -1
  48. package/src/Evolu/Evolu.ts +12 -16
  49. package/src/Evolu/Owner.ts +1 -1
  50. package/src/Evolu/Protocol.ts +32 -26
  51. package/src/Evolu/Relay.ts +13 -14
  52. package/src/Evolu/Storage.ts +10 -11
  53. package/src/Evolu/Sync.ts +4 -3
  54. package/src/Evolu/Timestamp.ts +1 -0
  55. package/src/Instances.ts +1 -1
  56. package/src/Number.ts +4 -4
  57. package/src/{RefCountedResourceManager.ts → Resources.ts} +29 -30
  58. package/src/Result.ts +1 -1
  59. package/src/Skiplist.ts +1 -1
  60. package/src/Task.ts +2 -2
  61. package/src/Time.ts +1 -1
  62. package/src/index.ts +2 -1
  63. package/dist/src/CallbackRegistry.d.ts +0 -53
  64. package/dist/src/CallbackRegistry.d.ts.map +0 -1
  65. package/dist/src/CallbackRegistry.js +0 -25
  66. package/dist/src/RefCountedResourceManager.d.ts.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evolu/common",
3
- "version": "6.0.1-preview.29",
3
+ "version": "6.0.1-preview.30",
4
4
  "description": "TypeScript library and local-first framework",
5
5
  "keywords": [
6
6
  "evolu",
package/src/Buffer.ts CHANGED
@@ -116,10 +116,10 @@ export const createBuffer = (
116
116
  let value = arrayLike
117
117
  ? new globalThis.Uint8Array(arrayLike)
118
118
  : new globalThis.Uint8Array(512);
119
- let length = (arrayLike ? arrayLike.length : 0) as NonNegativeInt;
119
+ let length = NonNegativeInt.orThrow(arrayLike ? arrayLike.length : 0);
120
120
 
121
121
  const buffer: Buffer = {
122
- getCapacity: () => value.length as NonNegativeInt,
122
+ getCapacity: () => NonNegativeInt.orThrow(value.length),
123
123
 
124
124
  getLength: () => length,
125
125
 
@@ -132,7 +132,7 @@ export const createBuffer = (
132
132
  value.set(oldValue);
133
133
  }
134
134
  value.set(arg, length);
135
- length = (length + arg.length) as NonNegativeInt;
135
+ length = NonNegativeInt.orThrow(length + arg.length);
136
136
  },
137
137
 
138
138
  shift: () => {
@@ -142,7 +142,7 @@ export const createBuffer = (
142
142
  const first = value[0];
143
143
  value = value.subarray(1);
144
144
  length--;
145
- return first as NonNegativeInt;
145
+ return NonNegativeInt.orThrow(first);
146
146
  },
147
147
 
148
148
  shiftN: (n) => {
@@ -151,7 +151,7 @@ export const createBuffer = (
151
151
  }
152
152
  const subarray = value.subarray(0, n);
153
153
  value = value.subarray(n);
154
- length = (length - n) as NonNegativeInt;
154
+ length = NonNegativeInt.orThrow(length - n);
155
155
  return subarray;
156
156
  },
157
157
 
@@ -165,7 +165,7 @@ export const createBuffer = (
165
165
  },
166
166
 
167
167
  reset: () => {
168
- length = 0 as NonNegativeInt;
168
+ length = NonNegativeInt.orThrow(0);
169
169
  },
170
170
 
171
171
  unwrap: () => value.subarray(0, length),
@@ -4,46 +4,45 @@ import { Result } from "./Result.js";
4
4
  import { createId, Id } from "./Type.js";
5
5
 
6
6
  /**
7
- * A registry for one-time callback functions.
7
+ * Request-response correlation for callbacks across boundaries.
8
8
  *
9
9
  * Stores callbacks with unique IDs and executes them once with an optional
10
- * argument. Executed callbacks are automatically removed from the registry.
10
+ * argument. Executed callbacks are automatically removed.
11
11
  *
12
- * This is useful for correlating asynchronous operations across boundaries
13
- * where callback functions cannot be passed directly (e.g., web workers).
12
+ * This is useful for correlating asynchronous request-response operations
13
+ * across boundaries where callback functions cannot be passed directly (e.g.,
14
+ * web workers, message queues).
14
15
  *
15
16
  * The `execute` method intentionally does not use try-catch or {@link Result}
16
- * because it's the callback's responsibility to handle its own errors. The
17
- * registry is just a correlation mechanism and should not interfere with error
18
- * handling or debugging by masking the original error location.
17
+ * because it's the callback's responsibility to handle its own errors.
19
18
  *
20
19
  * ### Example
21
20
  *
22
21
  * ```ts
23
22
  * // No-argument callbacks
24
- * const registry = createCallbackRegistry(deps);
25
- * const id = registry.register(() => console.log("called"));
26
- * registry.execute(id);
23
+ * const callbacks = createCallbacks(deps);
24
+ * const id = callbacks.register(() => console.log("called"));
25
+ * callbacks.execute(id);
27
26
  *
28
27
  * // With argument callbacks
29
- * const stringRegistry = createCallbackRegistry<string>(deps);
30
- * const id = stringRegistry.register((value) => {
28
+ * const stringCallbacks = createCallbacks<string>(deps);
29
+ * const id = stringCallbacks.register((value) => {
31
30
  * console.log(value);
32
31
  * });
33
- * stringRegistry.execute(id, "hello");
32
+ * stringCallbacks.execute(id, "hello");
34
33
  *
35
34
  * // Promise.withResolvers pattern
36
- * const promiseRegistry = createCallbackRegistry<string>(deps);
35
+ * const promiseCallbacks = createCallbacks<string>(deps);
37
36
  * const { promise, resolve } = Promise.withResolvers<string>();
38
- * const id = promiseRegistry.register(resolve);
39
- * promiseRegistry.execute(id, "resolved value");
37
+ * const id = promiseCallbacks.register(resolve);
38
+ * promiseCallbacks.execute(id, "resolved value");
40
39
  * await promise; // "resolved value"
41
40
  * ```
42
41
  *
43
42
  * @template T - The type of argument passed to callbacks (defaults to undefined
44
43
  * for no-argument callbacks)
45
44
  */
46
- export interface CallbackRegistry<T = undefined> {
45
+ export interface Callbacks<T = undefined> {
47
46
  /** Registers a callback function and returns a unique ID. */
48
47
  readonly register: (callback: (arg: T) => void) => CallbackId;
49
48
 
@@ -53,12 +52,13 @@ export interface CallbackRegistry<T = undefined> {
53
52
  : (id: CallbackId, arg: T) => undefined;
54
53
  }
55
54
 
55
+ /** Unique identifier for a callback in {@link Callbacks}. */
56
56
  export type CallbackId = Id & Brand<"Callback">;
57
57
 
58
- /** Creates a new {@link CallbackRegistry} for one-time callback functions. */
59
- export const createCallbackRegistry = <T = undefined>(
58
+ /** Creates a new {@link Callbacks}. */
59
+ export const createCallbacks = <T = undefined>(
60
60
  deps: RandomBytesDep,
61
- ): CallbackRegistry<T> => {
61
+ ): Callbacks<T> => {
62
62
  const callbackMap = new Map<CallbackId, (arg: T) => void>();
63
63
 
64
64
  return {
@@ -70,15 +70,14 @@ export const createCallbackRegistry = <T = undefined>(
70
70
 
71
71
  execute: (id: CallbackId, ...args: T extends undefined ? [] : [T]) => {
72
72
  const callback = callbackMap.get(id);
73
- if (callback) {
74
- callbackMap.delete(id);
75
- if (args.length === 0) {
76
- // Called without argument (undefined case)
77
- (callback as () => void)();
78
- } else {
79
- callback(args[0]);
80
- }
73
+ if (!callback) return;
74
+ callbackMap.delete(id);
75
+ if (args.length === 0) {
76
+ // Called without argument (undefined case)
77
+ (callback as () => void)();
78
+ } else {
79
+ callback(args[0]);
81
80
  }
82
81
  },
83
- } as CallbackRegistry<T>;
82
+ } as Callbacks<T>;
84
83
  };
package/src/Crypto.ts CHANGED
@@ -176,12 +176,12 @@ export const createSymmetricCrypto = (
176
176
  * https://bford.info/pub/sec/purb.pdf
177
177
  */
178
178
  export const padmePaddedLength = (length: NonNegativeInt): NonNegativeInt => {
179
- if (length <= 0) return 0 as NonNegativeInt;
179
+ if (length <= 0) return NonNegativeInt.orThrow(0);
180
180
  const e = 31 - Math.clz32(length >>> 0);
181
181
  const s = 32 - Math.clz32(e >>> 0);
182
182
  const z = Math.max(0, e - s);
183
183
  const mask = (1 << z) - 1;
184
- return ((length + mask) & ~mask) as NonNegativeInt;
184
+ return NonNegativeInt.orThrow((length + mask) & ~mask);
185
185
  };
186
186
 
187
187
  /**
@@ -189,7 +189,7 @@ export const padmePaddedLength = (length: NonNegativeInt): NonNegativeInt => {
189
189
  * {@link padmePaddedLength}.
190
190
  */
191
191
  export const padmePaddingLength = (length: NonNegativeInt): NonNegativeInt => {
192
- return (padmePaddedLength(length) - length) as NonNegativeInt;
192
+ return NonNegativeInt.orThrow(padmePaddedLength(length) - length);
193
193
  };
194
194
 
195
195
  /**
package/src/Evolu/Db.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isNonEmptyArray, NonEmptyReadonlyArray } from "../Array.js";
2
- import { CallbackId } from "../CallbackRegistry.js";
2
+ import { CallbackId } from "../Callbacks.js";
3
3
  import { ConsoleConfig, ConsoleDep } from "../Console.js";
4
4
  import {
5
5
  createSymmetricCrypto,
@@ -1,7 +1,7 @@
1
1
  import { pack } from "msgpackr";
2
2
  import { isNonEmptyArray, isNonEmptyReadonlyArray } from "../Array.js";
3
3
  import { assert, assertNonEmptyArray } from "../Assert.js";
4
- import { createCallbackRegistry } from "../CallbackRegistry.js";
4
+ import { createCallbacks } from "../Callbacks.js";
5
5
  import { ConsoleDep } from "../Console.js";
6
6
  import { RandomBytesDep, SymmetricCryptoDecryptError } from "../Crypto.js";
7
7
  import { eqArrayNumber } from "../Eq.js";
@@ -549,9 +549,8 @@ const createEvoluInstance =
549
549
 
550
550
  const subscribedQueries = createSubscribedQueries(rowsStore);
551
551
  const loadingPromises = createLoadingPromises(subscribedQueries);
552
- const onCompleteRegistry = createCallbackRegistry(deps);
553
- const exportRegistry =
554
- createCallbackRegistry<Uint8Array<ArrayBuffer>>(deps);
552
+ const onCompleteCallbacks = createCallbacks(deps);
553
+ const exportCallbacks = createCallbacks<Uint8Array<ArrayBuffer>>(deps);
555
554
 
556
555
  const dbWorker = deps.createDbWorker(dbConfig.name);
557
556
 
@@ -601,7 +600,7 @@ const createEvoluInstance =
601
600
  }
602
601
 
603
602
  for (const id of message.onCompleteIds) {
604
- onCompleteRegistry.execute(id);
603
+ onCompleteCallbacks.execute(id);
605
604
  }
606
605
  break;
607
606
  }
@@ -628,13 +627,13 @@ const createEvoluInstance =
628
627
  if (message.reload) {
629
628
  deps.reloadApp(reloadUrl);
630
629
  } else {
631
- onCompleteRegistry.execute(message.onCompleteId);
630
+ onCompleteCallbacks.execute(message.onCompleteId);
632
631
  }
633
632
  break;
634
633
  }
635
634
 
636
635
  case "onExport": {
637
- exportRegistry.execute(
636
+ exportCallbacks.execute(
638
637
  message.onCompleteId,
639
638
  message.file as Uint8Array<ArrayBuffer>,
640
639
  );
@@ -748,11 +747,11 @@ const createEvoluInstance =
748
747
 
749
748
  const processMutationQueue = () => {
750
749
  const changes: Array<MutationChange> = [];
751
- const onCompleteCallbacks = [];
750
+ const onCompletes = [];
752
751
 
753
752
  for (const [change, onComplete] of mutateMicrotaskQueue) {
754
753
  if (change !== null) changes.push(change);
755
- if (onComplete) onCompleteCallbacks.push(onComplete);
754
+ if (onComplete) onCompletes.push(onComplete);
756
755
  }
757
756
 
758
757
  const queueLength = mutateMicrotaskQueue.length;
@@ -764,10 +763,7 @@ const createEvoluInstance =
764
763
  return;
765
764
  }
766
765
 
767
- const onCompleteIds = onCompleteCallbacks.map(
768
- onCompleteRegistry.register,
769
- );
770
-
766
+ const onCompleteIds = onCompletes.map(onCompleteCallbacks.register);
771
767
  loadingPromises.releaseUnsubscribedOnMutation();
772
768
 
773
769
  if (!isNonEmptyArray(changes)) return;
@@ -846,7 +842,7 @@ const createEvoluInstance =
846
842
 
847
843
  resetAppOwner: (options) => {
848
844
  const { promise, resolve } = Promise.withResolvers<undefined>();
849
- const onCompleteId = onCompleteRegistry.register(resolve);
845
+ const onCompleteId = onCompleteCallbacks.register(resolve);
850
846
  dbWorker.postMessage({
851
847
  type: "reset",
852
848
  onCompleteId,
@@ -857,7 +853,7 @@ const createEvoluInstance =
857
853
 
858
854
  restoreAppOwner: (mnemonic, options) => {
859
855
  const { promise, resolve } = Promise.withResolvers<undefined>();
860
- const onCompleteId = onCompleteRegistry.register(resolve);
856
+ const onCompleteId = onCompleteCallbacks.register(resolve);
861
857
  dbWorker.postMessage({
862
858
  type: "reset",
863
859
  onCompleteId,
@@ -880,7 +876,7 @@ const createEvoluInstance =
880
876
  exportDatabase: () => {
881
877
  const { promise, resolve } =
882
878
  Promise.withResolvers<Uint8Array<ArrayBuffer>>();
883
- const onCompleteId = exportRegistry.register(resolve);
879
+ const onCompleteId = exportCallbacks.register(resolve);
884
880
  dbWorker.postMessage({ type: "export", onCompleteId });
885
881
  return promise;
886
882
  },
@@ -73,7 +73,7 @@ export const ownerIdToOwnerIdBytes = (ownerId: OwnerId): OwnerIdBytes =>
73
73
  export const ownerIdBytesToOwnerId = (ownerIdBytes: OwnerIdBytes): OwnerId =>
74
74
  idBytesToId(ownerIdBytes as IdBytes) as OwnerId;
75
75
 
76
- export const ownerWriteKeyLength = 16 as NonNegativeInt;
76
+ export const ownerWriteKeyLength = NonNegativeInt.orThrow(16);
77
77
 
78
78
  export const OwnerEncryptionKey = brand("OwnerEncryptionKey", EncryptionKey);
79
79
  export type OwnerEncryptionKey = typeof OwnerEncryptionKey.Type;
@@ -325,7 +325,7 @@ export const defaultProtocolMessageRangesMaxSize =
325
325
  export type ProtocolMessage = Uint8Array & Brand<"ProtocolMessage">;
326
326
 
327
327
  /** Evolu Protocol version. */
328
- export const protocolVersion = 0 as NonNegativeInt;
328
+ export const protocolVersion = NonNegativeInt.orThrow(0);
329
329
 
330
330
  export const MessageType = {
331
331
  /** Request message from initiator (client) to non-initiator (relay). */
@@ -525,7 +525,7 @@ export const createProtocolMessageForSync =
525
525
 
526
526
  splitRange(deps)(
527
527
  ownerIdBytes,
528
- 0 as NonNegativeInt,
528
+ NonNegativeInt.orThrow(0),
529
529
  size,
530
530
  InfiniteUpperBound,
531
531
  buffer,
@@ -628,7 +628,7 @@ export const createProtocolMessageBuffer = (
628
628
  const isWithinSizeLimits = () => getSize() <= totalMaxSize;
629
629
 
630
630
  const getSize = () =>
631
- (getHeaderAndMessagesSize() + getRangesSize()) as PositiveInt;
631
+ PositiveInt.orThrow(getHeaderAndMessagesSize() + getRangesSize());
632
632
 
633
633
  const getHeaderAndMessagesSize = () =>
634
634
  buffers.header.getLength() +
@@ -726,7 +726,10 @@ export const createProtocolMessageBuffer = (
726
726
  buffers.ranges.timestamps.addInfinite();
727
727
  }
728
728
 
729
- encodeNonNegativeInt(buffers.ranges.types, range.type as NonNegativeInt);
729
+ encodeNonNegativeInt(
730
+ buffers.ranges.types,
731
+ NonNegativeInt.orThrow(range.type),
732
+ );
730
733
 
731
734
  switch (range.type) {
732
735
  case RangeType.Skip:
@@ -781,7 +784,7 @@ export interface TimestampsBuffer {
781
784
  }
782
785
 
783
786
  export const createTimestampsBuffer = (): TimestampsBuffer => {
784
- let count = 0 as NonNegativeInt;
787
+ let count = NonNegativeInt.orThrow(0);
785
788
  const countBuffer = createBuffer();
786
789
 
787
790
  const syncCount = () => {
@@ -848,9 +851,9 @@ const createRunLengthEncoder = <T>(
848
851
  encodeValue: (buffer: Buffer, value: T) => void,
849
852
  ): RunLengthEncoder<T> => {
850
853
  const buffer = createBuffer();
851
- let previousLength = 0 as NonNegativeInt;
854
+ let previousLength = NonNegativeInt.orThrow(0);
852
855
  let previousValue = null as T | null;
853
- let runLength = 0 as NonNegativeInt;
856
+ let runLength = NonNegativeInt.orThrow(0);
854
857
 
855
858
  return {
856
859
  add: (value) => {
@@ -859,7 +862,7 @@ const createRunLengthEncoder = <T>(
859
862
  buffer.truncate(previousLength);
860
863
  } else {
861
864
  previousValue = value;
862
- runLength = 1 as NonNegativeInt;
865
+ runLength = NonNegativeInt.orThrow(1);
863
866
  }
864
867
  previousLength = buffer.getLength();
865
868
  encodeValue(buffer, value);
@@ -1255,7 +1258,7 @@ const sync =
1255
1258
  if (storageSize == null) return err(ProtocolErrorCode.SyncError);
1256
1259
 
1257
1260
  let prevUpperBound: RangeUpperBound | null = null;
1258
- let prevIndex = 0 as NonNegativeInt;
1261
+ let prevIndex = NonNegativeInt.orThrow(0);
1259
1262
 
1260
1263
  let skip = false;
1261
1264
  let nonSkipRangeAdded = false;
@@ -1458,7 +1461,7 @@ const splitRange =
1458
1461
  upperBound: RangeUpperBound,
1459
1462
  buffer: ProtocolMessageBuffer,
1460
1463
  ): void => {
1461
- const itemCount = (upper - lower) as NonNegativeInt;
1464
+ const itemCount = NonNegativeInt.orThrow(upper - lower);
1462
1465
  const buckets = computeBalancedBuckets(itemCount);
1463
1466
 
1464
1467
  if (!buckets.ok) {
@@ -1470,7 +1473,7 @@ const splitRange =
1470
1473
 
1471
1474
  deps.storage.iterate(
1472
1475
  ownerId,
1473
- 0 as NonNegativeInt,
1476
+ NonNegativeInt.orThrow(0),
1474
1477
  itemCount,
1475
1478
  (timestamp) => {
1476
1479
  range.timestamps.add(timestampBytesToTimestamp(timestamp));
@@ -1486,7 +1489,10 @@ const splitRange =
1486
1489
  const fingerprintRangesBuckets =
1487
1490
  lower === 0
1488
1491
  ? buckets.value
1489
- : [lower, ...buckets.value.map((b) => (b + lower) as NonNegativeInt)];
1492
+ : [
1493
+ lower,
1494
+ ...buckets.value.map((b) => NonNegativeInt.orThrow(b + lower)),
1495
+ ];
1490
1496
 
1491
1497
  const fingerprintRanges = deps.storage.fingerprintRanges(
1492
1498
  ownerId,
@@ -1510,7 +1516,7 @@ const decodeRanges = (buffer: Buffer): ReadonlyArray<Range> => {
1510
1516
  const rangesCount = decodeNonNegativeInt(buffer);
1511
1517
  if (rangesCount === 0) return [];
1512
1518
 
1513
- const timestampsCount = (rangesCount - 1) as NonNegativeInt;
1519
+ const timestampsCount = NonNegativeInt.orThrow(rangesCount - 1);
1514
1520
  const timestamps = decodeTimestamps(buffer, timestampsCount);
1515
1521
  const rangeTypes: Array<RangeType> = [];
1516
1522
 
@@ -1845,7 +1851,7 @@ export const decodeNonNegativeInt = (buffer: Buffer): NonNegativeInt => {
1845
1851
  };
1846
1852
 
1847
1853
  export const encodeLength = (buffer: Buffer, value: ArrayLike<any>): void => {
1848
- encodeNonNegativeInt(buffer, value.length as NonNegativeInt);
1854
+ encodeNonNegativeInt(buffer, NonNegativeInt.orThrow(value.length));
1849
1855
  };
1850
1856
 
1851
1857
  export const decodeLength = decodeNonNegativeInt;
@@ -1867,7 +1873,7 @@ export const encodeNodeId = (buffer: Buffer, nodeId: NodeId): void => {
1867
1873
  };
1868
1874
 
1869
1875
  export const decodeNodeId = (buffer: Buffer): NodeId => {
1870
- const bytes = buffer.shiftN(8 as NonNegativeInt);
1876
+ const bytes = buffer.shiftN(NonNegativeInt.orThrow(8));
1871
1877
  return bytesToHex(bytes) as NodeId;
1872
1878
  };
1873
1879
 
@@ -1879,26 +1885,26 @@ export const ProtocolValueType = {
1879
1885
  // 0-19 small ints
1880
1886
 
1881
1887
  // SQLite types
1882
- String: 20 as NonNegativeInt,
1883
- Number: 21 as NonNegativeInt,
1884
- Null: 22 as NonNegativeInt,
1885
- Bytes: 23 as NonNegativeInt,
1888
+ String: NonNegativeInt.orThrow(20),
1889
+ Number: NonNegativeInt.orThrow(21),
1890
+ Null: NonNegativeInt.orThrow(22),
1891
+ Bytes: NonNegativeInt.orThrow(23),
1886
1892
  // We can add more types for other DBs or anything else later.
1887
1893
 
1888
1894
  // Optimized types
1889
- NonNegativeInt: 30 as NonNegativeInt,
1895
+ NonNegativeInt: NonNegativeInt.orThrow(30),
1890
1896
 
1891
1897
  // String optimizations
1892
- EmptyString: 31 as NonNegativeInt, // 1 byte vs 2 bytes (50% reduction)
1893
- Base64Url: 32 as NonNegativeInt,
1894
- Id: 33 as NonNegativeInt,
1895
- Json: 34 as NonNegativeInt,
1898
+ EmptyString: NonNegativeInt.orThrow(31), // 1 byte vs 2 bytes (50% reduction)
1899
+ Base64Url: NonNegativeInt.orThrow(32),
1900
+ Id: NonNegativeInt.orThrow(33),
1901
+ Json: NonNegativeInt.orThrow(34),
1896
1902
 
1897
1903
  // new Date().toISOString() - 24 bytes
1898
1904
  // encoded with fixed length - 8 bytes
1899
1905
  // encode as NonNegativeInt - 6 bytes (additional 25% reduction)
1900
- DateIsoWithNonNegativeTime: 35 as NonNegativeInt,
1901
- DateIsoWithNegativeTime: 36 as NonNegativeInt, // 9 bytes
1906
+ DateIsoWithNonNegativeTime: NonNegativeInt.orThrow(35),
1907
+ DateIsoWithNegativeTime: NonNegativeInt.orThrow(36), // 9 bytes
1902
1908
 
1903
1909
  // TODO: Operations (from 40)
1904
1910
  // Increment, Decrement, Patch, whatever.
@@ -38,8 +38,8 @@ export interface RelayConfig extends ConsoleConfig, StorageConfig {
38
38
  * Optional callback to check if an {@link OwnerId} is allowed to access the
39
39
  * relay. If this callback is not provided, all owners are allowed.
40
40
  *
41
- * If provided, the callback receives the OwnerId and should return a
42
- * {@link MaybeAsync} boolean: `true` to allow access, or `false` to deny.
41
+ * The callback receives the {@link OwnerId} and returns a {@link MaybeAsync}
42
+ * boolean: `true` to allow access, or `false` to deny.
43
43
  *
44
44
  * The callback can be synchronous (for SQLite or in-memory checks) or
45
45
  * asynchronous (for calling remote APIs).
@@ -55,7 +55,8 @@ export interface RelayConfig extends ConsoleConfig, StorageConfig {
55
55
  * Owners specify which relays to connect to via {@link OwnerTransport}. In
56
56
  * WebSocket-based implementations, this check occurs before accepting the
57
57
  * connection, with the OwnerId typically extracted from the URL Path (e.g.,
58
- * `ws://localhost:4000/<ownerId>`).
58
+ * `ws://localhost:4000/<ownerId>`). The relay requires the URL to be in the
59
+ * correct format for OwnerId extraction.
59
60
  *
60
61
  * ### Example
61
62
  *
@@ -211,17 +212,15 @@ export const createRelaySqliteStorage =
211
212
  storedBytes + incomingBytes,
212
213
  );
213
214
 
214
- if (config.isOwnerWithinQuota) {
215
- const withinQuotaResult = config.isOwnerWithinQuota(
216
- ownerId,
217
- newStoredBytes,
218
- );
219
- const isWithinQuota = isAsync(withinQuotaResult)
220
- ? await withinQuotaResult
221
- : withinQuotaResult;
222
- if (!isWithinQuota) {
223
- return err({ type: "StorageQuotaError", ownerId });
224
- }
215
+ const withinQuotaResult = config.isOwnerWithinQuota(
216
+ ownerId,
217
+ newStoredBytes,
218
+ );
219
+ const isWithinQuota = isAsync(withinQuotaResult)
220
+ ? await withinQuotaResult
221
+ : withinQuotaResult;
222
+ if (!isWithinQuota) {
223
+ return err({ type: "StorageQuotaError", ownerId });
225
224
  }
226
225
 
227
226
  return deps.sqlite.transaction(() => {
@@ -29,13 +29,12 @@ import { orderTimestampBytes, Timestamp, TimestampBytes } from "./Timestamp.js";
29
29
 
30
30
  export interface StorageConfig {
31
31
  /**
32
- * Optional callback to check if an {@link OwnerId} is within their quota for
33
- * the requested write. If this callback is not provided, all writes are
34
- * allowed regardless of size.
32
+ * Callback called before an attempt to write, to check if an {@link OwnerId}
33
+ * has sufficient quota for the write.
35
34
  *
36
- * If provided, the callback receives the OwnerId and the number of bytes
37
- * required for the write, and should return a {@link MaybeAsync} boolean:
38
- * `true` to allow the write, or `false` to deny it due to quota limits.
35
+ * The callback receives the {@link OwnerId} and the number of bytes required
36
+ * for the write, and returns a {@link MaybeAsync} boolean: `true` to allow the
37
+ * write, or `false` to deny it due to quota limits.
39
38
  *
40
39
  * The callback can be synchronous (for SQLite or in-memory checks) or
41
40
  * asynchronous (for calling remote APIs).
@@ -58,7 +57,7 @@ export interface StorageConfig {
58
57
  * };
59
58
  * ```
60
59
  */
61
- readonly isOwnerWithinQuota?: (
60
+ readonly isOwnerWithinQuota: (
62
61
  ownerId: OwnerId,
63
62
  requiredBytes: PositiveInt,
64
63
  ) => MaybeAsync<boolean>;
@@ -183,7 +182,7 @@ export interface StorageQuotaError extends BaseOwnerError {
183
182
  */
184
183
  export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
185
184
 
186
- export const fingerprintSize = 12 as NonNegativeInt;
185
+ export const fingerprintSize = NonNegativeInt.orThrow(12);
187
186
 
188
187
  /** A fingerprint of an empty range. */
189
188
  export const zeroFingerprint = new Uint8Array(fingerprintSize) as Fingerprint;
@@ -425,7 +424,7 @@ export const createBaseSqliteStorage =
425
424
  }
426
425
 
427
426
  for (let i = 0; i < result.value.rows.length; i++) {
428
- const index = (begin + 1 + i) as NonNegativeInt;
427
+ const index = NonNegativeInt.orThrow(begin + 1 + i);
429
428
  if (!callback(result.value.rows[i].t, index)) return;
430
429
  }
431
430
  },
@@ -1153,7 +1152,7 @@ const randomSkiplistLevel = (deps: RandomDep): PositiveInt => {
1153
1152
  ) {
1154
1153
  level += 1;
1155
1154
  }
1156
- return level as PositiveInt;
1155
+ return PositiveInt.orThrow(level);
1157
1156
  };
1158
1157
 
1159
1158
  /**
@@ -1294,7 +1293,7 @@ const findLowerBound =
1294
1293
  if (!count.ok) return count;
1295
1294
 
1296
1295
  // `decrement` converts a count to an index.
1297
- return ok(decrement(count.value) as NonNegativeInt);
1296
+ return ok(NonNegativeInt.orThrow(decrement(count.value)));
1298
1297
  };
1299
1298
 
1300
1299
  const getTimestampCount =
package/src/Evolu/Sync.ts CHANGED
@@ -9,10 +9,10 @@ import {
9
9
  } from "../Crypto.js";
10
10
  import { eqArrayNumber } from "../Eq.js";
11
11
  import { createTransferableError, TransferableError } from "../Error.js";
12
- import { constFalse } from "../Function.js";
12
+ import { constFalse, constTrue } from "../Function.js";
13
13
  import { objectToEntries } from "../Object.js";
14
14
  import { RandomDep } from "../Random.js";
15
- import { createRefCountedResourceManager } from "../RefCountedResourceManager.js";
15
+ import { createResources } from "../Resources.js";
16
16
  import { err, ok, Result } from "../Result.js";
17
17
  import { sql, SqliteDep, SqliteError, SqliteValue } from "../Sqlite.js";
18
18
  import { AbortError, createMutex } from "../Task.js";
@@ -253,7 +253,7 @@ export const createSync =
253
253
  });
254
254
  };
255
255
 
256
- const transports = createRefCountedResourceManager<
256
+ const transports = createResources<
257
257
  WebSocket,
258
258
  TransportKey,
259
259
  OwnerTransport,
@@ -451,6 +451,7 @@ const createClientStorage =
451
451
  }): Result<ClientStorage, SqliteError> => {
452
452
  const sqliteStorageBase = createBaseSqliteStorage(deps)({
453
453
  onStorageError: config.onError,
454
+ isOwnerWithinQuota: constTrue, // Clients don't have quota limits
454
455
  });
455
456
 
456
457
  // TODO: Mutex per OwnerId
@@ -147,6 +147,7 @@ export const maxNodeId = "ffffffffffffffff" as NodeId;
147
147
  * - https://muratbuffalo.blogspot.com/2014/07/hybrid-logical-clocks.html
148
148
  * - https://sergeiturukin.com/2017/06/26/hybrid-logical-clocks.html
149
149
  * - https://jaredforsyth.com/posts/hybrid-logical-clocks/
150
+ * - https://willowprotocol.org/more/timestamps_really/index.html
150
151
  *
151
152
  * ### Privacy Considerations
152
153
  *
package/src/Instances.ts CHANGED
@@ -38,7 +38,7 @@ export interface Instances<K extends string, T extends Disposable>
38
38
  readonly delete: (key: K) => boolean;
39
39
  }
40
40
 
41
- /** Creates an {@link Instances} manager. */
41
+ /** Creates an {@link Instances}. */
42
42
  export const createInstances = <
43
43
  K extends string,
44
44
  T extends Disposable,
package/src/Number.ts CHANGED
@@ -59,15 +59,15 @@ export const computeBalancedBuckets = (
59
59
  numberOfItems: NonNegativeInt,
60
60
 
61
61
  /** Default: 16 */
62
- numberOfBuckets = 16 as PositiveInt,
62
+ numberOfBuckets = PositiveInt.orThrow(16),
63
63
 
64
64
  /** Default: 2 */
65
- minNumberOfItemsPerBucket = 2 as PositiveInt,
65
+ minNumberOfItemsPerBucket = PositiveInt.orThrow(2),
66
66
  ): Result<NonEmptyReadonlyArray<PositiveInt>, PositiveInt> => {
67
67
  const minRequiredItems = numberOfBuckets * minNumberOfItemsPerBucket;
68
68
 
69
69
  if (numberOfItems < minRequiredItems)
70
- return err(minRequiredItems as PositiveInt);
70
+ return err(PositiveInt.orThrow(minRequiredItems));
71
71
 
72
72
  const indexes: Array<PositiveInt> = [];
73
73
  const itemsPerBucket = Math.floor(numberOfItems / numberOfBuckets);
@@ -78,7 +78,7 @@ export const computeBalancedBuckets = (
78
78
  const hasExtraItem = i < extraItems;
79
79
  const itemsInThisBucket = itemsPerBucket + (hasExtraItem ? 1 : 0);
80
80
  bucketBoundary += itemsInThisBucket;
81
- indexes.push(bucketBoundary as PositiveInt);
81
+ indexes.push(PositiveInt.orThrow(bucketBoundary));
82
82
  }
83
83
 
84
84
  assertNonEmptyReadonlyArray(indexes);