@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.
- package/dist/src/Buffer.js +7 -6
- package/dist/src/Callbacks.d.ts +53 -0
- package/dist/src/Callbacks.d.ts.map +1 -0
- package/dist/src/Callbacks.js +25 -0
- package/dist/src/Crypto.js +3 -3
- package/dist/src/Evolu/Db.d.ts +1 -1
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +12 -12
- package/dist/src/Evolu/Owner.d.ts +1 -1
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +2 -2
- package/dist/src/Evolu/Protocol.d.ts +12 -12
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +30 -27
- package/dist/src/Evolu/Relay.d.ts +4 -3
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +6 -8
- package/dist/src/Evolu/Storage.d.ts +7 -8
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +5 -5
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +4 -3
- package/dist/src/Evolu/Timestamp.d.ts +1 -0
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +1 -0
- package/dist/src/Instances.d.ts +1 -1
- package/dist/src/Instances.d.ts.map +1 -1
- package/dist/src/Instances.js +1 -1
- package/dist/src/Number.d.ts +2 -2
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/{RefCountedResourceManager.d.ts → Resources.d.ts} +14 -15
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/{RefCountedResourceManager.js → Resources.js} +24 -24
- package/dist/src/Result.d.ts +1 -1
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Task.js +3 -3
- package/dist/src/Time.js +2 -2
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/package.json +1 -1
- package/src/Buffer.ts +6 -6
- package/src/{CallbackRegistry.ts → Callbacks.ts} +28 -29
- package/src/Crypto.ts +3 -3
- package/src/Evolu/Db.ts +1 -1
- package/src/Evolu/Evolu.ts +12 -16
- package/src/Evolu/Owner.ts +1 -1
- package/src/Evolu/Protocol.ts +32 -26
- package/src/Evolu/Relay.ts +13 -14
- package/src/Evolu/Storage.ts +10 -11
- package/src/Evolu/Sync.ts +4 -3
- package/src/Evolu/Timestamp.ts +1 -0
- package/src/Instances.ts +1 -1
- package/src/Number.ts +4 -4
- package/src/{RefCountedResourceManager.ts → Resources.ts} +29 -30
- package/src/Result.ts +1 -1
- package/src/Skiplist.ts +1 -1
- package/src/Task.ts +2 -2
- package/src/Time.ts +1 -1
- package/src/index.ts +2 -1
- package/dist/src/CallbackRegistry.d.ts +0 -53
- package/dist/src/CallbackRegistry.d.ts.map +0 -1
- package/dist/src/CallbackRegistry.js +0 -25
- package/dist/src/RefCountedResourceManager.d.ts.map +0 -1
package/package.json
CHANGED
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)
|
|
119
|
+
let length = NonNegativeInt.orThrow(arrayLike ? arrayLike.length : 0);
|
|
120
120
|
|
|
121
121
|
const buffer: Buffer = {
|
|
122
|
-
getCapacity: () => value.length
|
|
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)
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
*
|
|
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
|
|
10
|
+
* argument. Executed callbacks are automatically removed.
|
|
11
11
|
*
|
|
12
|
-
* This is useful for correlating asynchronous operations
|
|
13
|
-
* where callback functions cannot be passed directly (e.g.,
|
|
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.
|
|
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
|
|
25
|
-
* const id =
|
|
26
|
-
*
|
|
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
|
|
30
|
-
* const id =
|
|
28
|
+
* const stringCallbacks = createCallbacks<string>(deps);
|
|
29
|
+
* const id = stringCallbacks.register((value) => {
|
|
31
30
|
* console.log(value);
|
|
32
31
|
* });
|
|
33
|
-
*
|
|
32
|
+
* stringCallbacks.execute(id, "hello");
|
|
34
33
|
*
|
|
35
34
|
* // Promise.withResolvers pattern
|
|
36
|
-
* const
|
|
35
|
+
* const promiseCallbacks = createCallbacks<string>(deps);
|
|
37
36
|
* const { promise, resolve } = Promise.withResolvers<string>();
|
|
38
|
-
* const id =
|
|
39
|
-
*
|
|
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
|
|
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
|
|
59
|
-
export const
|
|
58
|
+
/** Creates a new {@link Callbacks}. */
|
|
59
|
+
export const createCallbacks = <T = undefined>(
|
|
60
60
|
deps: RandomBytesDep,
|
|
61
|
-
):
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
192
|
+
return NonNegativeInt.orThrow(padmePaddedLength(length) - length);
|
|
193
193
|
};
|
|
194
194
|
|
|
195
195
|
/**
|
package/src/Evolu/Db.ts
CHANGED
package/src/Evolu/Evolu.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
553
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
630
|
+
onCompleteCallbacks.execute(message.onCompleteId);
|
|
632
631
|
}
|
|
633
632
|
break;
|
|
634
633
|
}
|
|
635
634
|
|
|
636
635
|
case "onExport": {
|
|
637
|
-
|
|
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
|
|
750
|
+
const onCompletes = [];
|
|
752
751
|
|
|
753
752
|
for (const [change, onComplete] of mutateMicrotaskQueue) {
|
|
754
753
|
if (change !== null) changes.push(change);
|
|
755
|
-
if (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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
879
|
+
const onCompleteId = exportCallbacks.register(resolve);
|
|
884
880
|
dbWorker.postMessage({ type: "export", onCompleteId });
|
|
885
881
|
return promise;
|
|
886
882
|
},
|
package/src/Evolu/Owner.ts
CHANGED
|
@@ -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
|
|
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;
|
package/src/Evolu/Protocol.ts
CHANGED
|
@@ -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
|
|
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
|
|
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())
|
|
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(
|
|
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
|
|
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
|
|
854
|
+
let previousLength = NonNegativeInt.orThrow(0);
|
|
852
855
|
let previousValue = null as T | null;
|
|
853
|
-
let runLength = 0
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
: [
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
1883
|
-
Number: 21
|
|
1884
|
-
Null: 22
|
|
1885
|
-
Bytes: 23
|
|
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
|
|
1895
|
+
NonNegativeInt: NonNegativeInt.orThrow(30),
|
|
1890
1896
|
|
|
1891
1897
|
// String optimizations
|
|
1892
|
-
EmptyString: 31
|
|
1893
|
-
Base64Url: 32
|
|
1894
|
-
Id: 33
|
|
1895
|
-
Json: 34
|
|
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
|
|
1901
|
-
DateIsoWithNegativeTime: 36
|
|
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.
|
package/src/Evolu/Relay.ts
CHANGED
|
@@ -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
|
-
*
|
|
42
|
-
*
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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(() => {
|
package/src/Evolu/Storage.ts
CHANGED
|
@@ -29,13 +29,12 @@ import { orderTimestampBytes, Timestamp, TimestampBytes } from "./Timestamp.js";
|
|
|
29
29
|
|
|
30
30
|
export interface StorageConfig {
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
|
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)
|
|
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
|
|
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)
|
|
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 {
|
|
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 =
|
|
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
|
package/src/Evolu/Timestamp.ts
CHANGED
|
@@ -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}
|
|
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
|
|
62
|
+
numberOfBuckets = PositiveInt.orThrow(16),
|
|
63
63
|
|
|
64
64
|
/** Default: 2 */
|
|
65
|
-
minNumberOfItemsPerBucket = 2
|
|
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
|
|
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
|
|
81
|
+
indexes.push(PositiveInt.orThrow(bucketBoundary));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
assertNonEmptyReadonlyArray(indexes);
|