@evolu/common 7.4.1 → 8.0.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -61
- package/dist/src/Array.d.ts +343 -102
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +181 -85
- package/dist/src/Assert.d.ts +57 -11
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +47 -11
- package/dist/src/BigInt.d.ts +6 -1
- package/dist/src/BigInt.d.ts.map +1 -1
- package/dist/src/BigInt.js +5 -0
- package/dist/src/Brand.d.ts +40 -12
- package/dist/src/Brand.d.ts.map +1 -1
- package/dist/src/Brand.js +5 -0
- package/dist/src/Buffer.d.ts +6 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +7 -2
- package/dist/src/Cache.d.ts +10 -2
- package/dist/src/Cache.d.ts.map +1 -1
- package/dist/src/Cache.js +6 -0
- package/dist/src/Callbacks.d.ts +10 -7
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +23 -13
- package/dist/src/Console.d.ts +361 -69
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +217 -117
- package/dist/src/Crypto.d.ts +70 -21
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +72 -27
- package/dist/src/Eq.d.ts +22 -2
- package/dist/src/Eq.d.ts.map +1 -1
- package/dist/src/Eq.js +21 -1
- package/dist/src/Error.d.ts +23 -12
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +27 -11
- package/dist/src/Function.d.ts +128 -56
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Function.js +118 -14
- package/dist/src/Identicon.d.ts +5 -0
- package/dist/src/Identicon.d.ts.map +1 -1
- package/dist/src/Identicon.js +6 -1
- package/dist/src/Lookup.d.ts +160 -0
- package/dist/src/Lookup.d.ts.map +1 -0
- package/dist/src/Lookup.js +192 -0
- package/dist/src/Microtask.d.ts +21 -0
- package/dist/src/Microtask.d.ts.map +1 -0
- package/dist/src/Microtask.js +37 -0
- package/dist/src/Number.d.ts +18 -4
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +23 -1
- package/dist/src/Object.d.ts +110 -5
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +108 -5
- package/dist/src/Option.d.ts +58 -0
- package/dist/src/Option.d.ts.map +1 -0
- package/dist/src/Option.js +43 -0
- package/dist/src/Order.d.ts +5 -0
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +8 -3
- package/dist/src/Platform.d.ts +36 -2
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +10 -6
- package/dist/src/Polyfills.d.ts +27 -0
- package/dist/src/Polyfills.d.ts.map +1 -0
- package/dist/src/Polyfills.js +299 -0
- package/dist/src/Random.d.ts +18 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +9 -7
- package/dist/src/Redacted.d.ts +7 -4
- package/dist/src/Redacted.d.ts.map +1 -1
- package/dist/src/Redacted.js +5 -0
- package/dist/src/Ref.d.ts +31 -16
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Ref.js +35 -7
- package/dist/src/RefCount.d.ts +62 -0
- package/dist/src/RefCount.d.ts.map +1 -0
- package/dist/src/RefCount.js +83 -0
- package/dist/src/Relation.d.ts +84 -29
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Relation.js +83 -105
- package/dist/src/Resource.d.ts +263 -0
- package/dist/src/Resource.d.ts.map +1 -0
- package/dist/src/Resource.js +389 -0
- package/dist/src/Result.d.ts +390 -374
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +142 -70
- package/dist/src/Schedule.d.ts +953 -0
- package/dist/src/Schedule.d.ts.map +1 -0
- package/dist/src/Schedule.js +1199 -0
- package/dist/src/Set.d.ts +181 -0
- package/dist/src/Set.d.ts.map +1 -0
- package/dist/src/Set.js +137 -0
- package/dist/src/Skiplist.d.ts +6 -1
- package/dist/src/Skiplist.d.ts.map +1 -1
- package/dist/src/Skiplist.js +5 -0
- package/dist/src/Sqlite.d.ts +160 -60
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +290 -190
- package/dist/src/Store.d.ts +36 -28
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +55 -15
- package/dist/src/String.d.ts +5 -0
- package/dist/src/String.d.ts.map +1 -1
- package/dist/src/String.js +5 -0
- package/dist/src/Task.d.ts +2263 -423
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1488 -348
- package/dist/src/Test.d.ts +122 -0
- package/dist/src/Test.d.ts.map +1 -0
- package/dist/src/Test.js +66 -0
- package/dist/src/Time.d.ts +201 -48
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +190 -86
- package/dist/src/Tracer.d.ts +48 -0
- package/dist/src/Tracer.d.ts.map +1 -0
- package/dist/src/Tracer.js +6 -0
- package/dist/src/Type.d.ts +582 -209
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +362 -240
- package/dist/src/Types.d.ts +160 -6
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/Types.js +22 -2
- package/dist/src/WebSocket.d.ts +113 -58
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +210 -118
- package/dist/src/Worker.d.ts +245 -60
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +220 -64
- package/dist/src/index.d.ts +25 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +19 -3
- package/dist/src/local-first/Db.d.ts +24 -213
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +622 -293
- package/dist/src/local-first/Error.d.ts +12 -0
- package/dist/src/local-first/Error.d.ts.map +1 -0
- package/dist/src/local-first/Error.js +6 -0
- package/dist/src/local-first/Evolu.d.ts +330 -285
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +472 -402
- package/dist/src/local-first/LocalAuth.d.ts +10 -10
- package/dist/src/local-first/LocalAuth.d.ts.map +1 -1
- package/dist/src/local-first/LocalAuth.js +6 -1
- package/dist/src/local-first/Owner.d.ts +70 -27
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +27 -14
- package/dist/src/local-first/Protocol.d.ts +58 -50
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +253 -155
- package/dist/src/local-first/Query.d.ts +133 -43
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Query.js +139 -90
- package/dist/src/local-first/Relay.d.ts +18 -38
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +59 -171
- package/dist/src/local-first/Schema.d.ts +139 -144
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +78 -163
- package/dist/src/local-first/Shared.d.ts +125 -0
- package/dist/src/local-first/Shared.d.ts.map +1 -0
- package/dist/src/local-first/Shared.js +569 -0
- package/dist/src/local-first/Storage.d.ts +50 -53
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +121 -173
- package/dist/src/local-first/Timestamp.d.ts +28 -35
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +24 -25
- package/dist/src/local-first/index.d.ts +2 -15
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +2 -15
- package/package.json +36 -26
- package/src/Array.ts +467 -106
- package/src/Assert.ts +87 -12
- package/src/BigInt.ts +7 -1
- package/src/Brand.ts +41 -12
- package/src/Buffer.ts +10 -4
- package/src/Cache.ts +11 -2
- package/src/Callbacks.ts +33 -23
- package/src/Console.ts +567 -178
- package/src/Crypto.ts +111 -62
- package/src/Eq.ts +30 -3
- package/src/Error.ts +50 -34
- package/src/Function.ts +140 -68
- package/src/Identicon.ts +7 -1
- package/src/Lookup.ts +415 -0
- package/src/Microtask.ts +58 -0
- package/src/Number.ts +40 -7
- package/src/Object.ts +155 -16
- package/src/Option.ts +74 -0
- package/src/Order.ts +11 -3
- package/src/Platform.ts +43 -7
- package/src/Polyfills.ts +465 -0
- package/src/Random.ts +25 -11
- package/src/Redacted.ts +8 -4
- package/src/Ref.ts +72 -21
- package/src/RefCount.ts +170 -0
- package/src/Relation.ts +188 -136
- package/src/Resource.ts +864 -0
- package/src/Result.ts +548 -381
- package/src/Schedule.ts +1471 -0
- package/src/Set.ts +247 -0
- package/src/Skiplist.ts +7 -1
- package/src/Sqlite.ts +409 -304
- package/src/Store.ts +94 -48
- package/src/String.ts +6 -0
- package/src/Task.ts +3685 -740
- package/src/Test.ts +162 -0
- package/src/Time.ts +348 -145
- package/src/Tracer.ts +54 -0
- package/src/Type.ts +851 -430
- package/src/Types.ts +202 -7
- package/src/WebSocket.ts +311 -178
- package/src/Worker.ts +527 -147
- package/src/index.ts +74 -3
- package/src/local-first/Db.ts +871 -597
- package/src/local-first/Error.ts +17 -0
- package/src/local-first/Evolu.ts +884 -824
- package/src/local-first/LocalAuth.ts +10 -10
- package/src/local-first/Owner.ts +94 -42
- package/src/local-first/Protocol.ts +440 -303
- package/src/local-first/Query.ts +243 -194
- package/src/local-first/Relay.ts +91 -252
- package/src/local-first/Schema.ts +284 -394
- package/src/local-first/Shared.ts +932 -0
- package/src/local-first/Storage.ts +206 -277
- package/src/local-first/Timestamp.ts +38 -45
- package/src/local-first/index.ts +2 -16
- package/LICENSE +0 -21
- package/dist/src/Instances.d.ts +0 -34
- package/dist/src/Instances.d.ts.map +0 -1
- package/dist/src/Instances.js +0 -44
- package/dist/src/Resources.d.ts +0 -118
- package/dist/src/Resources.d.ts.map +0 -1
- package/dist/src/Resources.js +0 -197
- package/dist/src/local-first/Platform.d.ts +0 -25
- package/dist/src/local-first/Platform.d.ts.map +0 -1
- package/dist/src/local-first/Platform.js +0 -1
- package/dist/src/local-first/Public.d.ts +0 -22
- package/dist/src/local-first/Public.d.ts.map +0 -1
- package/dist/src/local-first/Public.js +0 -15
- package/dist/src/local-first/PublicKysely.d.ts +0 -148
- package/dist/src/local-first/PublicKysely.d.ts.map +0 -1
- package/dist/src/local-first/PublicKysely.js +0 -184
- package/dist/src/local-first/Sync.d.ts +0 -112
- package/dist/src/local-first/Sync.d.ts.map +0 -1
- package/dist/src/local-first/Sync.js +0 -526
- package/src/Instances.ts +0 -92
- package/src/Resources.ts +0 -367
- package/src/local-first/Platform.ts +0 -27
- package/src/local-first/Public.ts +0 -42
- package/src/local-first/PublicKysely.ts +0 -239
- package/src/local-first/Sync.ts +0 -949
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* transfer, ownership, real-time broadcasting, request-response semantics, and
|
|
21
21
|
* error handling.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
23
|
+
* ## Message structure
|
|
24
24
|
*
|
|
25
25
|
* | Field | Notes |
|
|
26
26
|
* | :----------------------------- | :------------------------ |
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* | - {@link NonNegativeInt} | Number of ranges. |
|
|
44
44
|
* | - {@link Range} | |
|
|
45
45
|
*
|
|
46
|
-
*
|
|
46
|
+
* ## WriteKey validation
|
|
47
47
|
*
|
|
48
48
|
* The initiator sends a hasWriteKey flag and optionally a WriteKey. The
|
|
49
49
|
* WriteKey is required when sending messages as a secure token proving the
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
* validates the WriteKey immediately after parsing the initiator header, before
|
|
53
53
|
* processing any messages or ranges.
|
|
54
54
|
*
|
|
55
|
-
*
|
|
55
|
+
* ## Synchronization
|
|
56
56
|
*
|
|
57
57
|
* - **Messages**: Sends {@link EncryptedCrdtMessage}s in either direction.
|
|
58
58
|
* - **Ranges**: Determines messages to sync. Usage varies by transport—e.g., sent
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
* initiator. In relay-to-relay or P2P sync, both sides may require the
|
|
80
80
|
* {@link OwnerWriteKey} depending on who is the initiator.
|
|
81
81
|
*
|
|
82
|
-
*
|
|
82
|
+
* ## Protocol errors
|
|
83
83
|
*
|
|
84
84
|
* The protocol uses error codes in the header to signal issues:
|
|
85
85
|
*
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
* All protocol errors except `ProtocolInvalidDataError` include the `OwnerId`
|
|
95
95
|
* to allow clients to associate errors with the correct owner.
|
|
96
96
|
*
|
|
97
|
-
*
|
|
97
|
+
* ## Message size limit
|
|
98
98
|
*
|
|
99
99
|
* The protocol enforces a strict maximum size for all messages, defined by
|
|
100
100
|
* {@link ProtocolMessageMaxSize}. This ensures every {@link ProtocolMessage} is
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
* message limit to ensure efficient sync with
|
|
108
108
|
* {@link defaultProtocolMessageRangesMaxSize}.
|
|
109
109
|
*
|
|
110
|
-
*
|
|
110
|
+
* ## Why Binary?
|
|
111
111
|
*
|
|
112
112
|
* The protocol avoids JSON because:
|
|
113
113
|
*
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
* To avoid reinventing serialization where it’s unnecessary—like for JSON and
|
|
128
128
|
* certain numbers—the Evolu Protocol relies on MessagePack.
|
|
129
129
|
*
|
|
130
|
-
*
|
|
130
|
+
* ## Versioning
|
|
131
131
|
*
|
|
132
132
|
* Evolu Protocol uses explicit versioning to ensure compatibility between
|
|
133
133
|
* clients and relays (or peers). Each protocol message begins with a version
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
* Version negotiation is per-owner, allowing Evolu Protocol to evolve safely
|
|
149
149
|
* over time and provide clear feedback about version mismatches.
|
|
150
150
|
*
|
|
151
|
-
*
|
|
151
|
+
* ## Credible exit
|
|
152
152
|
*
|
|
153
153
|
* The protocol specification is intentionally non-configurable to ensure
|
|
154
154
|
* universal compatibility. This design allows applications (users) to switch
|
|
@@ -176,18 +176,20 @@
|
|
|
176
176
|
* indicators) to be forwarded by relays without storage overhead.
|
|
177
177
|
*/
|
|
178
178
|
import { Packr } from "msgpackr";
|
|
179
|
-
import {
|
|
179
|
+
import { isNonEmptyArray } from "../Array.js";
|
|
180
180
|
import { assert } from "../Assert.js";
|
|
181
181
|
import { bytesToHex, bytesToUtf8, createBuffer, hexToBytes, utf8ToBytes, } from "../Buffer.js";
|
|
182
|
-
import { createPadmePadding, } from "../Crypto.js";
|
|
182
|
+
import { createPadmePadding, decryptWithXChaCha20Poly1305, EncryptionKey, encryptWithXChaCha20Poly1305, Entropy24, XChaCha20Poly1305Ciphertext, xChaCha20Poly1305NonceLength, } from "../Crypto.js";
|
|
183
183
|
import { eqArrayNumber } from "../Eq.js";
|
|
184
184
|
import { computeBalancedBuckets } from "../Number.js";
|
|
185
185
|
import { createRecord, objectToEntries } from "../Object.js";
|
|
186
186
|
import { err, ok } from "../Result.js";
|
|
187
|
-
import {
|
|
188
|
-
import {
|
|
187
|
+
import { SqliteValue } from "../Sqlite.js";
|
|
188
|
+
import { Millis } from "../Time.js";
|
|
189
|
+
import { Base64Url, base64UrlToUint8Array, between, DateIso, Id, IdBytes, idBytesToId, idBytesTypeValueLength, idToIdBytes, Int, Json, jsonToJsonValue, NonNegativeInt, Number, onePositiveInt, PositiveInt, uint8ArrayToBase64Url, zeroNonNegativeInt, } from "../Type.js";
|
|
190
|
+
import { OwnerId, OwnerIdBytes, ownerIdToOwnerIdBytes, OwnerWriteKey, ownerWriteKeyLength, } from "./Owner.js";
|
|
189
191
|
import { DbChange, fingerprintSize, InfiniteUpperBound, RangeType, } from "./Storage.js";
|
|
190
|
-
import { Counter, eqTimestamp,
|
|
192
|
+
import { Counter, eqTimestamp, NodeId, Timestamp, TimestampBytes, timestampBytesLength, timestampBytesToTimestamp, timestampToTimestampBytes, } from "./Timestamp.js";
|
|
191
193
|
/**
|
|
192
194
|
* Evolu uses MessagePack for numbers and JSONs.
|
|
193
195
|
*
|
|
@@ -213,7 +215,7 @@ const maxProtocolMessageMaxSize = 100_000_000;
|
|
|
213
215
|
* Increasing this value on the client side would break compatibility with
|
|
214
216
|
* relays that enforce smaller limits.
|
|
215
217
|
*/
|
|
216
|
-
export const ProtocolMessageMaxSize = between(minProtocolMessageMaxSize, maxProtocolMessageMaxSize)(Int);
|
|
218
|
+
export const ProtocolMessageMaxSize = /*#__PURE__*/ between(minProtocolMessageMaxSize, maxProtocolMessageMaxSize)(Int);
|
|
217
219
|
/**
|
|
218
220
|
* Default {@link ProtocolMessageMaxSize} (1MB).
|
|
219
221
|
*
|
|
@@ -232,7 +234,7 @@ export const defaultProtocolMessageMaxSize = minProtocolMessageMaxSize;
|
|
|
232
234
|
* {@link defaultProtocolMessageMaxSize}, maintaining compatibility between all
|
|
233
235
|
* clients and relays.
|
|
234
236
|
*/
|
|
235
|
-
export const ProtocolMessageRangesMaxSize = between(3_000, 100_000)(Int);
|
|
237
|
+
export const ProtocolMessageRangesMaxSize = /*#__PURE__*/ between(3_000, 100_000)(Int);
|
|
236
238
|
/**
|
|
237
239
|
* Default {@link ProtocolMessageRangesMaxSize} (30KB).
|
|
238
240
|
*
|
|
@@ -241,7 +243,7 @@ export const ProtocolMessageRangesMaxSize = between(3_000, 100_000)(Int);
|
|
|
241
243
|
*/
|
|
242
244
|
export const defaultProtocolMessageRangesMaxSize = 30_000;
|
|
243
245
|
/** Evolu Protocol version. */
|
|
244
|
-
export const protocolVersion =
|
|
246
|
+
export const protocolVersion = onePositiveInt;
|
|
245
247
|
export const MessageType = {
|
|
246
248
|
/** Request message from initiator (client) to non-initiator (relay). */
|
|
247
249
|
Request: 0,
|
|
@@ -250,6 +252,24 @@ export const MessageType = {
|
|
|
250
252
|
/** Broadcast message from non-initiator (relay) to subscribed clients. */
|
|
251
253
|
Broadcast: 2,
|
|
252
254
|
};
|
|
255
|
+
/**
|
|
256
|
+
* Parses the protocol header needed for routing.
|
|
257
|
+
*
|
|
258
|
+
* Every protocol message begins with `protocolVersion`, `ownerId`, and
|
|
259
|
+
* `messageType`.
|
|
260
|
+
*/
|
|
261
|
+
export const parseProtocolHeader = (inputMessage) => {
|
|
262
|
+
try {
|
|
263
|
+
return ok(parseProtocolHeaderFromBuffer(createBuffer(inputMessage)));
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
return err({
|
|
267
|
+
type: "ProtocolInvalidDataError",
|
|
268
|
+
data: inputMessage,
|
|
269
|
+
error,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
};
|
|
253
273
|
export const SubscriptionFlags = {
|
|
254
274
|
/** No subscription changes for this owner. */
|
|
255
275
|
None: 0,
|
|
@@ -328,10 +348,7 @@ export const createProtocolMessageForSync = (deps) => (ownerId, subscriptionFlag
|
|
|
328
348
|
});
|
|
329
349
|
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
330
350
|
const size = deps.storage.getSize(ownerIdBytes);
|
|
331
|
-
|
|
332
|
-
if (size == null)
|
|
333
|
-
return null;
|
|
334
|
-
splitRange(deps)(ownerIdBytes, NonNegativeInt.orThrow(0), size, InfiniteUpperBound, buffer);
|
|
351
|
+
splitRange(deps)(ownerIdBytes, zeroNonNegativeInt, size, InfiniteUpperBound, buffer);
|
|
335
352
|
return buffer.unwrap();
|
|
336
353
|
};
|
|
337
354
|
export const createProtocolMessageForUnsubscribe = (ownerId) => createProtocolMessageBuffer(ownerId, {
|
|
@@ -413,9 +430,7 @@ export const createProtocolMessageBuffer = (ownerId, options) => {
|
|
|
413
430
|
buffers.messages.dbChanges.extend(message.change);
|
|
414
431
|
assert(isWithinSizeLimits(), "the message is too big");
|
|
415
432
|
},
|
|
416
|
-
canSplitRange: () =>
|
|
417
|
-
return getRangesSize() + safeMargins.splitRange <= rangesMaxSize;
|
|
418
|
-
},
|
|
433
|
+
canSplitRange: () => getRangesSize() + safeMargins.splitRange <= rangesMaxSize,
|
|
419
434
|
canAddTimestampsRangeAndMessage: (timestamps, message) => {
|
|
420
435
|
const rangesNewSize = getRangesSize() + timestamps.getLength() + safeMargins.timestampsRange;
|
|
421
436
|
return (rangesNewSize <= rangesMaxSize &&
|
|
@@ -472,7 +487,7 @@ export const createProtocolMessageBuffer = (ownerId, options) => {
|
|
|
472
487
|
};
|
|
473
488
|
};
|
|
474
489
|
export const createTimestampsBuffer = () => {
|
|
475
|
-
let count =
|
|
490
|
+
let count = zeroNonNegativeInt;
|
|
476
491
|
const countBuffer = createBuffer();
|
|
477
492
|
const syncCount = () => {
|
|
478
493
|
countBuffer.reset();
|
|
@@ -517,9 +532,9 @@ export const createTimestampsBuffer = () => {
|
|
|
517
532
|
};
|
|
518
533
|
const createRunLengthEncoder = (encodeValue) => {
|
|
519
534
|
const buffer = createBuffer();
|
|
520
|
-
let previousLength =
|
|
535
|
+
let previousLength = zeroNonNegativeInt;
|
|
521
536
|
let previousValue = null;
|
|
522
|
-
let runLength =
|
|
537
|
+
let runLength = zeroNonNegativeInt;
|
|
523
538
|
return {
|
|
524
539
|
add: (value) => {
|
|
525
540
|
if (value === previousValue) {
|
|
@@ -538,7 +553,8 @@ const createRunLengthEncoder = (encodeValue) => {
|
|
|
538
553
|
unwrap: () => buffer.unwrap(),
|
|
539
554
|
};
|
|
540
555
|
};
|
|
541
|
-
export const applyProtocolMessageAsClient = (
|
|
556
|
+
export const applyProtocolMessageAsClient = (inputMessage, options = {}) => async (run) => {
|
|
557
|
+
const { storage } = run.deps;
|
|
542
558
|
try {
|
|
543
559
|
const input = createBuffer(inputMessage);
|
|
544
560
|
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
@@ -585,39 +601,45 @@ export const applyProtocolMessageAsClient = (deps) => async (inputMessage, optio
|
|
|
585
601
|
}
|
|
586
602
|
const messages = decodeMessages(input);
|
|
587
603
|
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
604
|
+
if (isNonEmptyArray(messages)) {
|
|
605
|
+
try {
|
|
606
|
+
const result = await run(storage.writeMessages(ownerIdBytes, messages));
|
|
607
|
+
// Quota errors are handled by Storage; protocol just stops syncing.
|
|
608
|
+
if (!result.ok)
|
|
609
|
+
return ok({ type: "NoResponse" });
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
run.deps.console.error(error);
|
|
613
|
+
return ok({ type: "NoResponse" });
|
|
614
|
+
}
|
|
593
615
|
}
|
|
594
616
|
// Now: No writeKey, no sync.
|
|
595
617
|
// TODO: Allow to sync SharedReadonlyOwner
|
|
596
618
|
// Without local changes, writeKey will not be required.
|
|
597
619
|
// With local changes, writeKey will be required and if not provided,
|
|
598
620
|
// the sync will stop.
|
|
599
|
-
const writeKey = options.
|
|
621
|
+
const writeKey = options.writeKey;
|
|
600
622
|
if (writeKey == null) {
|
|
601
|
-
return ok({ type: "
|
|
623
|
+
return ok({ type: "NoResponse" });
|
|
602
624
|
}
|
|
603
625
|
if (messageType === MessageType.Broadcast) {
|
|
604
|
-
return ok({ type: "
|
|
626
|
+
return ok({ type: "Broadcast" });
|
|
605
627
|
}
|
|
606
628
|
const ranges = decodeRanges(input);
|
|
607
|
-
if (!
|
|
608
|
-
return ok({ type: "
|
|
629
|
+
if (!isNonEmptyArray(ranges)) {
|
|
630
|
+
return ok({ type: "NoResponse" });
|
|
609
631
|
}
|
|
610
632
|
const output = createProtocolMessageBuffer(ownerId, {
|
|
611
633
|
messageType: MessageType.Request,
|
|
612
634
|
writeKey,
|
|
613
635
|
rangesMaxSize: options.rangesMaxSize,
|
|
614
636
|
});
|
|
615
|
-
const result = sync(deps)(ranges, output, ownerIdBytes);
|
|
637
|
+
const result = sync(run.deps)(ranges, output, ownerIdBytes);
|
|
616
638
|
// Client sync error (handled via Storage) or no changes.
|
|
617
639
|
if (!result.ok || !result.value) {
|
|
618
|
-
return ok({ type: "
|
|
640
|
+
return ok({ type: "NoResponse" });
|
|
619
641
|
}
|
|
620
|
-
return ok({ type: "
|
|
642
|
+
return ok({ type: "Response", message: output.unwrap() });
|
|
621
643
|
}
|
|
622
644
|
catch (error) {
|
|
623
645
|
return err({
|
|
@@ -627,9 +649,10 @@ export const applyProtocolMessageAsClient = (deps) => async (inputMessage, optio
|
|
|
627
649
|
});
|
|
628
650
|
}
|
|
629
651
|
};
|
|
630
|
-
export const applyProtocolMessageAsRelay = (
|
|
652
|
+
export const applyProtocolMessageAsRelay = (inputMessage, options = {},
|
|
631
653
|
/** For tests only. */
|
|
632
|
-
version = protocolVersion) => {
|
|
654
|
+
version = protocolVersion) => async (run) => {
|
|
655
|
+
const { storage } = run.deps;
|
|
633
656
|
try {
|
|
634
657
|
const input = createBuffer(inputMessage);
|
|
635
658
|
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
@@ -640,7 +663,7 @@ version = protocolVersion) => {
|
|
|
640
663
|
encodeNonNegativeInt(output, version);
|
|
641
664
|
output.extend(ownerIdBytes);
|
|
642
665
|
return ok({
|
|
643
|
-
type: "
|
|
666
|
+
type: "Response",
|
|
644
667
|
message: output.unwrap(),
|
|
645
668
|
});
|
|
646
669
|
}
|
|
@@ -663,10 +686,10 @@ version = protocolVersion) => {
|
|
|
663
686
|
break;
|
|
664
687
|
}
|
|
665
688
|
if (writeKey) {
|
|
666
|
-
const isValid =
|
|
689
|
+
const isValid = storage.validateWriteKey(ownerIdBytes, writeKey);
|
|
667
690
|
if (!isValid) {
|
|
668
691
|
return ok({
|
|
669
|
-
type: "
|
|
692
|
+
type: "Response",
|
|
670
693
|
message: createProtocolMessageBuffer(ownerId, {
|
|
671
694
|
messageType: MessageType.Response,
|
|
672
695
|
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
@@ -675,26 +698,33 @@ version = protocolVersion) => {
|
|
|
675
698
|
}
|
|
676
699
|
}
|
|
677
700
|
const messages = decodeMessages(input);
|
|
678
|
-
if (
|
|
701
|
+
if (isNonEmptyArray(messages)) {
|
|
679
702
|
if (!writeKey) {
|
|
680
703
|
return ok({
|
|
681
|
-
type: "
|
|
704
|
+
type: "Response",
|
|
682
705
|
message: createProtocolMessageBuffer(ownerId, {
|
|
683
706
|
messageType: MessageType.Response,
|
|
684
707
|
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
685
708
|
}).unwrap(),
|
|
686
709
|
});
|
|
687
710
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
711
|
+
try {
|
|
712
|
+
const result = await run(storage.writeMessages(ownerIdBytes, messages));
|
|
713
|
+
if (!result.ok) {
|
|
714
|
+
const message = createProtocolMessageBuffer(ownerId, {
|
|
715
|
+
messageType: MessageType.Response,
|
|
716
|
+
errorCode: ProtocolErrorCode.QuotaError,
|
|
717
|
+
}).unwrap();
|
|
718
|
+
return ok({ type: "Response", message });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
run.deps.console.error(error);
|
|
693
723
|
const message = createProtocolMessageBuffer(ownerId, {
|
|
694
724
|
messageType: MessageType.Response,
|
|
695
|
-
errorCode,
|
|
725
|
+
errorCode: ProtocolErrorCode.WriteError,
|
|
696
726
|
}).unwrap();
|
|
697
|
-
return ok({ type: "
|
|
727
|
+
return ok({ type: "Response", message });
|
|
698
728
|
}
|
|
699
729
|
/**
|
|
700
730
|
* Broadcast messages to all subscribed owners for real-time
|
|
@@ -735,10 +765,10 @@ version = protocolVersion) => {
|
|
|
735
765
|
});
|
|
736
766
|
// Non-initiators always respond to provide sync completion feedback,
|
|
737
767
|
// even when there's nothing to sync.
|
|
738
|
-
if (!
|
|
739
|
-
return ok({ type: "
|
|
768
|
+
if (!isNonEmptyArray(ranges)) {
|
|
769
|
+
return ok({ type: "Response", message: output.unwrap() });
|
|
740
770
|
}
|
|
741
|
-
const result = sync(deps)(ranges, output, ownerIdBytes);
|
|
771
|
+
const result = sync(run.deps)(ranges, output, ownerIdBytes);
|
|
742
772
|
const message = result.ok
|
|
743
773
|
? output.unwrap()
|
|
744
774
|
: createProtocolMessageBuffer(ownerId, {
|
|
@@ -746,7 +776,7 @@ version = protocolVersion) => {
|
|
|
746
776
|
errorCode: result.error,
|
|
747
777
|
}).unwrap();
|
|
748
778
|
// Non-initiators always respond to provide sync completion feedback,
|
|
749
|
-
return ok({ type: "
|
|
779
|
+
return ok({ type: "Response", message });
|
|
750
780
|
}
|
|
751
781
|
catch (error) {
|
|
752
782
|
return err({
|
|
@@ -765,6 +795,33 @@ const decodeVersionAndOwner = (input) => {
|
|
|
765
795
|
const ownerId = decodeId(input);
|
|
766
796
|
return [version, ownerId];
|
|
767
797
|
};
|
|
798
|
+
const parseProtocolHeaderFromBuffer = (input) => {
|
|
799
|
+
const [version, ownerId] = decodeVersionAndOwner(input);
|
|
800
|
+
if (version !== protocolVersion) {
|
|
801
|
+
throw new ProtocolDecodeError(`Unsupported protocol version: ${version}`);
|
|
802
|
+
}
|
|
803
|
+
const messageTypeValue = input.shift();
|
|
804
|
+
let messageType;
|
|
805
|
+
switch (messageTypeValue) {
|
|
806
|
+
case MessageType.Request:
|
|
807
|
+
messageType = MessageType.Request;
|
|
808
|
+
break;
|
|
809
|
+
case MessageType.Response:
|
|
810
|
+
messageType = MessageType.Response;
|
|
811
|
+
break;
|
|
812
|
+
case MessageType.Broadcast:
|
|
813
|
+
messageType = MessageType.Broadcast;
|
|
814
|
+
break;
|
|
815
|
+
default:
|
|
816
|
+
throw new ProtocolDecodeError("Invalid MessageType");
|
|
817
|
+
}
|
|
818
|
+
return {
|
|
819
|
+
type: "ProtocolHeader",
|
|
820
|
+
version: 1,
|
|
821
|
+
ownerId,
|
|
822
|
+
messageType,
|
|
823
|
+
};
|
|
824
|
+
};
|
|
768
825
|
/**
|
|
769
826
|
* Error thrown for internal protocol validation failures, such as invalid data
|
|
770
827
|
* or type errors.
|
|
@@ -778,21 +835,27 @@ class ProtocolDecodeError extends Error {
|
|
|
778
835
|
}
|
|
779
836
|
const decodeMessages = (buffer) => {
|
|
780
837
|
const timestamps = decodeTimestamps(buffer);
|
|
781
|
-
const messages =
|
|
782
|
-
for (
|
|
838
|
+
const messages = new Array(timestamps.length);
|
|
839
|
+
for (let i = 0; i < timestamps.length; i++) {
|
|
840
|
+
const timestamp = timestamps[i];
|
|
783
841
|
const changeLength = decodeLength(buffer);
|
|
784
842
|
const change = buffer.shiftN(changeLength);
|
|
785
|
-
messages
|
|
843
|
+
messages[i] = { timestamp, change };
|
|
786
844
|
}
|
|
787
845
|
return messages;
|
|
788
846
|
};
|
|
789
847
|
const sync = (deps) => (ranges, output, ownerIdBytes) => {
|
|
790
848
|
const outputInitialSize = output.getSize();
|
|
791
|
-
|
|
792
|
-
|
|
849
|
+
let storageSize;
|
|
850
|
+
try {
|
|
851
|
+
storageSize = deps.storage.getSize(ownerIdBytes);
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
deps.console.error(error);
|
|
793
855
|
return err(ProtocolErrorCode.SyncError);
|
|
856
|
+
}
|
|
794
857
|
let prevUpperBound = null;
|
|
795
|
-
let prevIndex =
|
|
858
|
+
let prevIndex = zeroNonNegativeInt;
|
|
796
859
|
let skip = false;
|
|
797
860
|
let nonSkipRangeAdded = false;
|
|
798
861
|
const skipRange = (range) => {
|
|
@@ -822,9 +885,14 @@ const sync = (deps) => (ranges, output, ownerIdBytes) => {
|
|
|
822
885
|
};
|
|
823
886
|
// When we don't have a space...
|
|
824
887
|
const addFingerprintForRemainingRange = (begin) => {
|
|
825
|
-
|
|
826
|
-
|
|
888
|
+
let fingerprint;
|
|
889
|
+
try {
|
|
890
|
+
fingerprint = deps.storage.fingerprint(ownerIdBytes, begin, storageSize);
|
|
891
|
+
}
|
|
892
|
+
catch (error) {
|
|
893
|
+
deps.console.error(error);
|
|
827
894
|
return false;
|
|
895
|
+
}
|
|
828
896
|
// There is always a space for a ramaining range.
|
|
829
897
|
output.addRange({
|
|
830
898
|
type: RangeType.Fingerprint,
|
|
@@ -836,18 +904,28 @@ const sync = (deps) => (ranges, output, ownerIdBytes) => {
|
|
|
836
904
|
for (const range of ranges) {
|
|
837
905
|
const currentUpperBound = range.upperBound;
|
|
838
906
|
const lower = prevIndex;
|
|
839
|
-
let upper
|
|
840
|
-
|
|
907
|
+
let upper;
|
|
908
|
+
try {
|
|
909
|
+
upper = deps.storage.findLowerBound(ownerIdBytes, prevIndex, storageSize, currentUpperBound);
|
|
910
|
+
}
|
|
911
|
+
catch (error) {
|
|
912
|
+
deps.console.error(error);
|
|
841
913
|
return err(ProtocolErrorCode.SyncError);
|
|
914
|
+
}
|
|
842
915
|
switch (range.type) {
|
|
843
916
|
case RangeType.Skip: {
|
|
844
917
|
skipRange(range);
|
|
845
918
|
break;
|
|
846
919
|
}
|
|
847
920
|
case RangeType.Fingerprint: {
|
|
848
|
-
|
|
849
|
-
|
|
921
|
+
let ourFingerprint;
|
|
922
|
+
try {
|
|
923
|
+
ourFingerprint = deps.storage.fingerprint(ownerIdBytes, lower, upper);
|
|
924
|
+
}
|
|
925
|
+
catch (error) {
|
|
926
|
+
deps.console.error(error);
|
|
850
927
|
return err(ProtocolErrorCode.SyncError);
|
|
928
|
+
}
|
|
851
929
|
if (eqArrayNumber(range.fingerprint, ourFingerprint)) {
|
|
852
930
|
skipRange(range);
|
|
853
931
|
}
|
|
@@ -868,38 +946,46 @@ const sync = (deps) => (ranges, output, ownerIdBytes) => {
|
|
|
868
946
|
let endBound = currentUpperBound;
|
|
869
947
|
const timestampsWeNeed = new Map(range.timestamps.map((t) => [t.join(), true]));
|
|
870
948
|
const ourTimestamps = createTimestampsBuffer();
|
|
871
|
-
let cantReadDbChange = false;
|
|
872
949
|
let exceeded = false;
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
950
|
+
let iterateFailed = false;
|
|
951
|
+
try {
|
|
952
|
+
deps.storage.iterate(ownerIdBytes, lower, upper, (timestamp, index) => {
|
|
953
|
+
const timestampString = timestamp.join();
|
|
954
|
+
const timestampBinary = timestampBytesToTimestamp(timestamp);
|
|
955
|
+
let message = null;
|
|
956
|
+
if (timestampsWeNeed.has(timestampString)) {
|
|
957
|
+
timestampsWeNeed.delete(timestampString);
|
|
958
|
+
}
|
|
959
|
+
else {
|
|
960
|
+
try {
|
|
961
|
+
message = {
|
|
962
|
+
timestamp: timestampBinary,
|
|
963
|
+
change: deps.storage.readDbChange(ownerIdBytes, timestamp),
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
deps.console.error(error);
|
|
968
|
+
iterateFailed = true;
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (!output.canAddTimestampsRangeAndMessage(ourTimestamps, message)) {
|
|
973
|
+
exceeded = true;
|
|
974
|
+
endBound = timestamp;
|
|
975
|
+
upper = index;
|
|
884
976
|
return false;
|
|
885
977
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
ourTimestamps.add(timestampBinary);
|
|
898
|
-
if (message)
|
|
899
|
-
output.addMessage(message);
|
|
900
|
-
return true;
|
|
901
|
-
});
|
|
902
|
-
if (cantReadDbChange) {
|
|
978
|
+
ourTimestamps.add(timestampBinary);
|
|
979
|
+
if (message)
|
|
980
|
+
output.addMessage(message);
|
|
981
|
+
return true;
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
catch (error) {
|
|
985
|
+
deps.console.error(error);
|
|
986
|
+
return err(ProtocolErrorCode.SyncError);
|
|
987
|
+
}
|
|
988
|
+
if (iterateFailed) {
|
|
903
989
|
return err(ProtocolErrorCode.SyncError);
|
|
904
990
|
}
|
|
905
991
|
const addRange = () => {
|
|
@@ -943,7 +1029,7 @@ const splitRange = (deps) => (ownerId, lower, upper, upperBound, buffer) => {
|
|
|
943
1029
|
upperBound,
|
|
944
1030
|
timestamps: createTimestampsBuffer(),
|
|
945
1031
|
};
|
|
946
|
-
deps.storage.iterate(ownerId,
|
|
1032
|
+
deps.storage.iterate(ownerId, zeroNonNegativeInt, itemCount, (timestamp) => {
|
|
947
1033
|
range.timestamps.add(timestampBytesToTimestamp(timestamp));
|
|
948
1034
|
return true;
|
|
949
1035
|
});
|
|
@@ -957,10 +1043,14 @@ const splitRange = (deps) => (ownerId, lower, upper, upperBound, buffer) => {
|
|
|
957
1043
|
lower,
|
|
958
1044
|
...buckets.value.map((b) => NonNegativeInt.orThrow(b + lower)),
|
|
959
1045
|
];
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1046
|
+
let fingerprintRanges;
|
|
1047
|
+
try {
|
|
1048
|
+
fingerprintRanges = deps.storage.fingerprintRanges(ownerId, fingerprintRangesBuckets, upperBound);
|
|
1049
|
+
}
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
deps.console.error(error);
|
|
963
1052
|
return;
|
|
1053
|
+
}
|
|
964
1054
|
const rangesToUse = lower > 0 ? fingerprintRanges.slice(1) : fingerprintRanges;
|
|
965
1055
|
for (const range of rangesToUse) {
|
|
966
1056
|
buffer.addRange(range);
|
|
@@ -974,20 +1064,20 @@ const decodeRanges = (buffer) => {
|
|
|
974
1064
|
return [];
|
|
975
1065
|
const timestampsCount = NonNegativeInt.orThrow(rangesCount - 1);
|
|
976
1066
|
const timestamps = decodeTimestamps(buffer, timestampsCount);
|
|
977
|
-
const rangeTypes =
|
|
1067
|
+
const rangeTypes = new Array(rangesCount);
|
|
978
1068
|
for (let i = 0; i < rangesCount; i++) {
|
|
979
1069
|
const rangeType = decodeNonNegativeInt(buffer);
|
|
980
1070
|
switch (rangeType) {
|
|
981
1071
|
case RangeType.Fingerprint:
|
|
982
1072
|
case RangeType.Skip:
|
|
983
1073
|
case RangeType.Timestamps:
|
|
984
|
-
rangeTypes
|
|
1074
|
+
rangeTypes[i] = rangeType;
|
|
985
1075
|
break;
|
|
986
1076
|
default:
|
|
987
1077
|
throw new ProtocolDecodeError(`Invalid RangeType: ${rangeType}`);
|
|
988
1078
|
}
|
|
989
1079
|
}
|
|
990
|
-
const ranges =
|
|
1080
|
+
const ranges = new Array(rangesCount);
|
|
991
1081
|
for (let i = 0; i < rangesCount; i++) {
|
|
992
1082
|
const upperBound = i < timestampsCount
|
|
993
1083
|
? timestampToTimestampBytes(timestamps[i])
|
|
@@ -995,75 +1085,81 @@ const decodeRanges = (buffer) => {
|
|
|
995
1085
|
const rangeType = rangeTypes[i];
|
|
996
1086
|
switch (rangeType) {
|
|
997
1087
|
case RangeType.Skip:
|
|
998
|
-
ranges
|
|
1088
|
+
ranges[i] = { type: RangeType.Skip, upperBound };
|
|
999
1089
|
break;
|
|
1000
1090
|
case RangeType.Fingerprint: {
|
|
1001
1091
|
const fingerprint = buffer.shiftN(fingerprintSize);
|
|
1002
|
-
ranges
|
|
1092
|
+
ranges[i] = {
|
|
1003
1093
|
type: RangeType.Fingerprint,
|
|
1004
1094
|
upperBound,
|
|
1005
1095
|
fingerprint,
|
|
1006
|
-
}
|
|
1096
|
+
};
|
|
1007
1097
|
break;
|
|
1008
1098
|
}
|
|
1009
1099
|
case RangeType.Timestamps: {
|
|
1010
1100
|
const timestamps = decodeTimestamps(buffer).map(timestampToTimestampBytes);
|
|
1011
|
-
ranges
|
|
1101
|
+
ranges[i] = {
|
|
1012
1102
|
type: RangeType.Timestamps,
|
|
1013
1103
|
upperBound,
|
|
1014
1104
|
timestamps,
|
|
1015
|
-
}
|
|
1105
|
+
};
|
|
1016
1106
|
break;
|
|
1017
1107
|
}
|
|
1018
1108
|
}
|
|
1019
1109
|
}
|
|
1020
1110
|
return ranges;
|
|
1021
1111
|
};
|
|
1022
|
-
/** Decodes an array of sorted timestamps with delta-encoded millis. */
|
|
1023
1112
|
const decodeTimestamps = (buffer, length) => {
|
|
1024
1113
|
length ??= decodeNonNegativeInt(buffer);
|
|
1025
1114
|
let previousMillis = 0;
|
|
1026
|
-
const millises =
|
|
1115
|
+
const millises = new Array(length);
|
|
1027
1116
|
for (let i = 0; i < length; i++) {
|
|
1028
1117
|
const deltaMillis = decodeNonNegativeInt(buffer);
|
|
1029
1118
|
const millis = Millis.from(previousMillis + deltaMillis);
|
|
1030
1119
|
if (!millis.ok)
|
|
1031
1120
|
throw new ProtocolDecodeError(millis.error.type);
|
|
1032
|
-
millises
|
|
1121
|
+
millises[i] = millis.value;
|
|
1033
1122
|
previousMillis = millis.value;
|
|
1034
1123
|
}
|
|
1035
|
-
const counters =
|
|
1036
|
-
let counterIndex = 0;
|
|
1037
|
-
while (counterIndex < length) {
|
|
1124
|
+
const counters = decodeRle(buffer, length, () => {
|
|
1038
1125
|
const counter = Counter.from(decodeNonNegativeInt(buffer));
|
|
1039
1126
|
if (!counter.ok)
|
|
1040
1127
|
throw new ProtocolDecodeError(counter.error.type);
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
const nodeIds = [];
|
|
1048
|
-
let nodeIdIndex = 0;
|
|
1049
|
-
while (nodeIdIndex < length) {
|
|
1050
|
-
const nodeId = decodeNodeId(buffer);
|
|
1051
|
-
const runLength = decodeNonNegativeInt(buffer);
|
|
1052
|
-
for (let i = 0; i < runLength; i++) {
|
|
1053
|
-
nodeIds.push(nodeId);
|
|
1054
|
-
nodeIdIndex++;
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
const timestamps = [];
|
|
1128
|
+
return counter.value;
|
|
1129
|
+
});
|
|
1130
|
+
const nodeIds = decodeRle(buffer, length, () => decodeNodeId(buffer));
|
|
1131
|
+
const timestamps = new Array(length);
|
|
1058
1132
|
for (let i = 0; i < length; i++) {
|
|
1059
|
-
timestamps
|
|
1133
|
+
timestamps[i] = {
|
|
1060
1134
|
millis: millises[i],
|
|
1061
1135
|
counter: counters[i],
|
|
1062
1136
|
nodeId: nodeIds[i],
|
|
1063
|
-
}
|
|
1137
|
+
};
|
|
1064
1138
|
}
|
|
1065
1139
|
return timestamps;
|
|
1066
1140
|
};
|
|
1141
|
+
export const decodeRle = (buffer, length, decodeValue) => {
|
|
1142
|
+
const values = new Array(length);
|
|
1143
|
+
let index = 0;
|
|
1144
|
+
while (index < length) {
|
|
1145
|
+
const value = decodeValue();
|
|
1146
|
+
const runLength = decodeNonNegativeInt(buffer);
|
|
1147
|
+
// Prevent infinite loop on malformed input.
|
|
1148
|
+
if (runLength === 0) {
|
|
1149
|
+
throw new ProtocolDecodeError("Invalid RLE encoding: runLength must be positive");
|
|
1150
|
+
}
|
|
1151
|
+
const remaining = length - index;
|
|
1152
|
+
// Prevent CPU/memory amplification via oversized runLength.
|
|
1153
|
+
if (runLength > remaining) {
|
|
1154
|
+
throw new ProtocolDecodeError(`Invalid RLE encoding: runLength ${runLength} exceeds remaining ${remaining}`);
|
|
1155
|
+
}
|
|
1156
|
+
for (let i = 0; i < runLength; i++) {
|
|
1157
|
+
values[index] = value;
|
|
1158
|
+
index++;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return values;
|
|
1162
|
+
};
|
|
1067
1163
|
const decodeId = (buffer) => {
|
|
1068
1164
|
const bytes = buffer.shiftN(idBytesTypeValueLength);
|
|
1069
1165
|
return idBytesToId(bytes);
|
|
@@ -1124,9 +1220,10 @@ export const encodeFlags = (buffer, flags) => {
|
|
|
1124
1220
|
*/
|
|
1125
1221
|
export const decodeFlags = (buffer, count) => {
|
|
1126
1222
|
const byte = buffer.shift();
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1223
|
+
const length = globalThis.Math.min(count, 8);
|
|
1224
|
+
const flags = new Array(length);
|
|
1225
|
+
for (let i = 0; i < length; i++) {
|
|
1226
|
+
flags[i] = (byte & (1 << i)) !== 0;
|
|
1130
1227
|
}
|
|
1131
1228
|
return flags;
|
|
1132
1229
|
};
|
|
@@ -1146,6 +1243,7 @@ export const encodeAndEncryptDbChange = (deps) => (message, key) => {
|
|
|
1146
1243
|
buffer.extend(timestampToTimestampBytes(message.timestamp));
|
|
1147
1244
|
encodeFlags(buffer, [
|
|
1148
1245
|
message.change.isInsert,
|
|
1246
|
+
// Encode nullable boolean as two flags: presence + value.
|
|
1149
1247
|
message.change.isDelete != null,
|
|
1150
1248
|
message.change.isDelete ?? false,
|
|
1151
1249
|
]);
|
|
@@ -1159,7 +1257,7 @@ export const encodeAndEncryptDbChange = (deps) => (message, key) => {
|
|
|
1159
1257
|
}
|
|
1160
1258
|
// Add PADMÉ padding (ignored during decoding)
|
|
1161
1259
|
buffer.extend(createPadmePadding(buffer.getLength()));
|
|
1162
|
-
const
|
|
1260
|
+
const [ciphertext, nonce] = encryptWithXChaCha20Poly1305(deps)(buffer.unwrap(), key);
|
|
1163
1261
|
buffer.reset();
|
|
1164
1262
|
buffer.extend(nonce);
|
|
1165
1263
|
encodeLength(buffer, ciphertext);
|
|
@@ -1171,19 +1269,19 @@ export const encodeAndEncryptDbChange = (deps) => (message, key) => {
|
|
|
1171
1269
|
* owner's encryption key. Verifies that the embedded timestamp matches the
|
|
1172
1270
|
* expected timestamp to ensure message integrity.
|
|
1173
1271
|
*/
|
|
1174
|
-
export const decryptAndDecodeDbChange = (
|
|
1272
|
+
export const decryptAndDecodeDbChange = (message, key) => {
|
|
1175
1273
|
try {
|
|
1176
1274
|
const buffer = createBuffer(message.change);
|
|
1177
|
-
const nonce = buffer.shiftN(
|
|
1275
|
+
const nonce = buffer.shiftN(xChaCha20Poly1305NonceLength);
|
|
1178
1276
|
const ciphertext = buffer.shiftN(decodeLength(buffer));
|
|
1179
|
-
const plaintextBytes =
|
|
1277
|
+
const plaintextBytes = decryptWithXChaCha20Poly1305(XChaCha20Poly1305Ciphertext.orThrow(ciphertext), Entropy24.orThrow(nonce), key);
|
|
1180
1278
|
if (!plaintextBytes.ok)
|
|
1181
1279
|
return plaintextBytes;
|
|
1182
1280
|
buffer.reset();
|
|
1183
1281
|
buffer.extend(plaintextBytes.value);
|
|
1184
1282
|
// Decode version (for future compatibility, not need yet)
|
|
1185
1283
|
decodeNonNegativeInt(buffer);
|
|
1186
|
-
const timestamp = timestampBytesToTimestamp(buffer.shiftN(timestampBytesLength));
|
|
1284
|
+
const timestamp = timestampBytesToTimestamp(TimestampBytes.orThrow(buffer.shiftN(timestampBytesLength)));
|
|
1187
1285
|
if (!eqTimestamp(timestamp, message.timestamp)) {
|
|
1188
1286
|
return err({
|
|
1189
1287
|
type: "ProtocolTimestampMismatchError",
|
|
@@ -1289,23 +1387,23 @@ const isSmallInt = (value) => value >= 0 && value < 20;
|
|
|
1289
1387
|
export const ProtocolValueType = {
|
|
1290
1388
|
// 0-19 small ints
|
|
1291
1389
|
// SQLite types
|
|
1292
|
-
String: NonNegativeInt.orThrow(20),
|
|
1293
|
-
Number: NonNegativeInt.orThrow(21),
|
|
1294
|
-
Null: NonNegativeInt.orThrow(22),
|
|
1295
|
-
Bytes: NonNegativeInt.orThrow(23),
|
|
1390
|
+
String: /*#__PURE__*/ NonNegativeInt.orThrow(20),
|
|
1391
|
+
Number: /*#__PURE__*/ NonNegativeInt.orThrow(21),
|
|
1392
|
+
Null: /*#__PURE__*/ NonNegativeInt.orThrow(22),
|
|
1393
|
+
Bytes: /*#__PURE__*/ NonNegativeInt.orThrow(23),
|
|
1296
1394
|
// We can add more types for other DBs or anything else later.
|
|
1297
1395
|
// Optimized types
|
|
1298
|
-
NonNegativeInt: NonNegativeInt.orThrow(30),
|
|
1396
|
+
NonNegativeInt: /*#__PURE__*/ NonNegativeInt.orThrow(30),
|
|
1299
1397
|
// String optimizations
|
|
1300
|
-
EmptyString: NonNegativeInt.orThrow(31), // 1 byte vs 2 bytes (50% reduction)
|
|
1301
|
-
Base64Url: NonNegativeInt.orThrow(32),
|
|
1302
|
-
Id: NonNegativeInt.orThrow(33),
|
|
1303
|
-
Json: NonNegativeInt.orThrow(34),
|
|
1398
|
+
EmptyString: /*#__PURE__*/ NonNegativeInt.orThrow(31), // 1 byte vs 2 bytes (50% reduction)
|
|
1399
|
+
Base64Url: /*#__PURE__*/ NonNegativeInt.orThrow(32),
|
|
1400
|
+
Id: /*#__PURE__*/ NonNegativeInt.orThrow(33),
|
|
1401
|
+
Json: /*#__PURE__*/ NonNegativeInt.orThrow(34),
|
|
1304
1402
|
// new Date().toISOString() - 24 bytes
|
|
1305
1403
|
// encoded with fixed length - 8 bytes
|
|
1306
1404
|
// encode as NonNegativeInt - 6 bytes (additional 25% reduction)
|
|
1307
|
-
DateIsoWithNonNegativeTime: NonNegativeInt.orThrow(35),
|
|
1308
|
-
DateIsoWithNegativeTime: NonNegativeInt.orThrow(36), // 9 bytes
|
|
1405
|
+
DateIsoWithNonNegativeTime: /*#__PURE__*/ NonNegativeInt.orThrow(35),
|
|
1406
|
+
DateIsoWithNegativeTime: /*#__PURE__*/ NonNegativeInt.orThrow(36), // 9 bytes
|
|
1309
1407
|
// TODO: Operations (from 40)
|
|
1310
1408
|
// Increment, Decrement, Patch, whatever.
|
|
1311
1409
|
};
|