@evolu/common 6.0.1-preview.2 → 6.0.1-preview.20
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/Assert.d.ts +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +7 -4
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- package/dist/src/Console.d.ts +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +29 -42
- package/dist/src/Evolu/Db.d.ts +137 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +300 -705
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +200 -115
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +287 -157
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- package/dist/src/Evolu/LocalAuth.d.ts +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +152 -127
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +88 -107
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +170 -186
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +487 -343
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- 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 +50 -12
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +159 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +42 -32
- package/dist/src/Evolu/Sync.d.ts +77 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +453 -20
- package/dist/src/Evolu/Timestamp.d.ts +35 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +27 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +2 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +147 -51
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +7 -241
- package/dist/src/Sqlite.d.ts +21 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +462 -327
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +490 -436
- package/dist/src/Types.d.ts +0 -74
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -13
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/package.json +14 -13
- package/src/Assert.ts +8 -7
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -79
- package/src/Evolu/Db.ts +505 -1027
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +682 -329
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +221 -235
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +681 -614
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +62 -16
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +275 -55
- package/src/Evolu/Sync.ts +758 -37
- package/src/Evolu/Timestamp.ts +38 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Number.ts +2 -6
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +152 -53
- package/src/Sqlite.ts +59 -23
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +696 -660
- package/src/Types.ts +0 -76
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +3 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Callbacks.ts +0 -43
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
|
@@ -1,35 +1,57 @@
|
|
|
1
|
+
/* eslint-disable jsdoc/no-undefined-types */
|
|
1
2
|
/**
|
|
2
3
|
* Evolu Protocol
|
|
3
4
|
*
|
|
4
5
|
* Evolu Protocol is a local-first, end-to-end encrypted binary synchronization
|
|
5
6
|
* protocol optimized for minimal size and maximum speed. It enables data sync
|
|
6
|
-
* between a client and a relay
|
|
7
|
-
*
|
|
7
|
+
* between a client and a relay. In the future, direct peer-to-peer (P2P) sync
|
|
8
|
+
* between clients will be possible without a relay.
|
|
9
|
+
*
|
|
10
|
+
* Relays don't need to sync with each other—clients using those relays will
|
|
11
|
+
* sync them eventually. If a relay is offline (e.g., for maintenance), it will
|
|
12
|
+
* sync automatically later via client sync logic. For relay backup using
|
|
13
|
+
* SQLite, see https://sqlite.org/rsync.html (uses a similar algorithm to Evolu
|
|
14
|
+
* RBSR).
|
|
8
15
|
*
|
|
9
16
|
* Evolu Protocol is designed for SQLite but can be extended to any database. It
|
|
10
17
|
* implements [Range-Based Set
|
|
11
18
|
* Reconciliation](https://arxiv.org/abs/2212.13567). To learn how RBSR works,
|
|
12
19
|
* check [Negentropy](https://logperiodic.com/rbsr.html). Evolu Protocol is
|
|
13
20
|
* similar to Negentropy but uses different encoding and also provides data
|
|
14
|
-
* transfer and
|
|
21
|
+
* transfer, ownership, real-time broadcasting, request-response semantics, and
|
|
22
|
+
* error handling.
|
|
15
23
|
*
|
|
16
24
|
* ### Message Structure
|
|
17
25
|
*
|
|
18
|
-
* | Field | Notes
|
|
19
|
-
* | :----------------------------- |
|
|
20
|
-
* | **Header** |
|
|
21
|
-
* | - {@link protocolVersion} |
|
|
22
|
-
* | - {@link OwnerId} |
|
|
23
|
-
* | - {@link
|
|
24
|
-
* | **
|
|
25
|
-
* | -
|
|
26
|
-
* | - {@link
|
|
27
|
-
* | - {@link
|
|
28
|
-
* | **
|
|
29
|
-
* | - {@link
|
|
30
|
-
* |
|
|
26
|
+
* | Field | Notes |
|
|
27
|
+
* | :----------------------------- | :------------------------ |
|
|
28
|
+
* | **Header** | |
|
|
29
|
+
* | - {@link protocolVersion} | |
|
|
30
|
+
* | - {@link OwnerId} | {@link Owner} |
|
|
31
|
+
* | - messageType | {@link MessageType} |
|
|
32
|
+
* | **Request (messageType=0)** | |
|
|
33
|
+
* | - hasWriteKey | 0 = no, 1 = yes |
|
|
34
|
+
* | - {@link OwnerWriteKey} | If hasWriteKey = 1 |
|
|
35
|
+
* | - subscriptionFlag | {@link SubscriptionFlags} |
|
|
36
|
+
* | **Response (messageType=1)** | |
|
|
37
|
+
* | - {@link ProtocolErrorCode} | |
|
|
38
|
+
* | **Broadcast (messageType=2)** | |
|
|
39
|
+
* | - (no additional fields) | |
|
|
40
|
+
* | **Messages** | |
|
|
41
|
+
* | - {@link NonNegativeInt} | A number of messages. |
|
|
42
|
+
* | - {@link EncryptedCrdtMessage} | |
|
|
43
|
+
* | **Ranges** | |
|
|
44
|
+
* | - {@link NonNegativeInt} | Number of ranges. |
|
|
45
|
+
* | - {@link Range} | |
|
|
46
|
+
*
|
|
47
|
+
* ### WriteKey Validation
|
|
31
48
|
*
|
|
32
|
-
*
|
|
49
|
+
* The initiator sends a hasWriteKey flag and optionally a WriteKey. The
|
|
50
|
+
* WriteKey is required when sending messages as a secure token proving the
|
|
51
|
+
* initiator can write changes. It's ok to not send a WriteKey if the initiator
|
|
52
|
+
* is only syncing (read-only) and not sending messages. The non-initiator
|
|
53
|
+
* validates the WriteKey immediately after parsing the initiator header, before
|
|
54
|
+
* processing any messages or ranges.
|
|
33
55
|
*
|
|
34
56
|
* ### Synchronization
|
|
35
57
|
*
|
|
@@ -43,16 +65,20 @@
|
|
|
43
65
|
* if further sync is needed or possible, continuing until both sides are
|
|
44
66
|
* synchronized.
|
|
45
67
|
*
|
|
68
|
+
* The **non-initiator always responds** to provide sync completion feedback,
|
|
69
|
+
* even with empty messages containing only the header and no error. This allows
|
|
70
|
+
* the initiator to detect when synchronization is complete.
|
|
71
|
+
*
|
|
46
72
|
* Both **Messages** and **Ranges** are optional, allowing each side to send,
|
|
47
73
|
* sync, or only subscribe data as needed.
|
|
48
74
|
*
|
|
49
|
-
* When the initiator sends data, the {@link
|
|
50
|
-
*
|
|
51
|
-
* responds without a {@link
|
|
52
|
-
* signals it wants data. If the non-initiator detects an issue, it
|
|
53
|
-
* error code via the `Error` field in the header back to the
|
|
54
|
-
* relay-to-relay or P2P sync, both sides may require the
|
|
55
|
-
* depending on who is the initiator.
|
|
75
|
+
* When the initiator sends data, the {@link OwnerWriteKey} is required as a
|
|
76
|
+
* secure token proving the initiator can write changes. The non-initiator
|
|
77
|
+
* responds without a {@link OwnerWriteKey}, since the initiator’s request
|
|
78
|
+
* already signals it wants data. If the non-initiator detects an issue, it
|
|
79
|
+
* sends an error code via the `Error` field in the header back to the
|
|
80
|
+
* initiator. In relay-to-relay or P2P sync, both sides may require the
|
|
81
|
+
* {@link OwnerWriteKey} depending on who is the initiator.
|
|
56
82
|
*
|
|
57
83
|
* ### Protocol Errors
|
|
58
84
|
*
|
|
@@ -66,15 +92,21 @@
|
|
|
66
92
|
* - {@link ProtocolUnsupportedVersionError}: Protocol version mismatch.
|
|
67
93
|
* - {@link ProtocolInvalidDataError}: The message is malformed or corrupted.
|
|
68
94
|
*
|
|
69
|
-
* All protocol errors except `ProtocolInvalidDataError` include the `
|
|
95
|
+
* All protocol errors except `ProtocolInvalidDataError` include the `OwnerId`
|
|
70
96
|
* to allow clients to associate errors with the correct owner.
|
|
71
97
|
*
|
|
72
98
|
* ### Message Size Limit
|
|
73
99
|
*
|
|
74
100
|
* The protocol enforces a strict maximum size for all messages, defined by
|
|
75
101
|
* {@link maxProtocolMessageSize}. This ensures every {@link ProtocolMessage} is
|
|
76
|
-
* less than or equal to this limit,
|
|
77
|
-
*
|
|
102
|
+
* less than or equal to this limit, enabling stateless transports, simplified
|
|
103
|
+
* relay implementation, and predictable memory usage. When all messages don't
|
|
104
|
+
* fit within the limit, the protocol automatically continues synchronization in
|
|
105
|
+
* subsequent rounds using range-based reconciliation.
|
|
106
|
+
*
|
|
107
|
+
* Individual database mutations are limited to `maxMutationSize` (640KB), which
|
|
108
|
+
* is smaller than the protocol message limit to ensure efficient sync with
|
|
109
|
+
* {@link maxProtocolMessageRangesSize}.
|
|
78
110
|
*
|
|
79
111
|
* ### Why Binary?
|
|
80
112
|
*
|
|
@@ -82,13 +114,12 @@
|
|
|
82
114
|
*
|
|
83
115
|
* - Encrypted data doesn’t compress well, unlike plain JSON.
|
|
84
116
|
* - Message size must be controlled during creation.
|
|
85
|
-
* - Sequential byte reading is faster than parsing and
|
|
117
|
+
* - Sequential byte reading is faster than parsing and avoids conversions.
|
|
86
118
|
*
|
|
87
119
|
* It uses structure-aware encoding, significantly outperforming generic binary
|
|
88
120
|
* serialization formats with the following optimizations:
|
|
89
121
|
*
|
|
90
122
|
* - **NonNegativeInt:** Up to 33% smaller than MessagePack.
|
|
91
|
-
* - **Base64Url Strings:** Up to 25% size reduction.
|
|
92
123
|
* - **DateIso:** Up to 75% smaller.
|
|
93
124
|
* - **Timestamp Encoding:** Delta encoding for milliseconds and run-length
|
|
94
125
|
* encoding (RLE) for counters and NodeIds.
|
|
@@ -120,25 +151,48 @@
|
|
|
120
151
|
*
|
|
121
152
|
* @module
|
|
122
153
|
*/
|
|
123
|
-
import {
|
|
124
|
-
import { pack, unpack, unpackMultiple } from "msgpackr";
|
|
154
|
+
import { Packr } from "msgpackr";
|
|
125
155
|
import { isNonEmptyReadonlyArray } from "../Array.js";
|
|
126
156
|
import { assert } from "../Assert.js";
|
|
127
|
-
import {
|
|
157
|
+
import { bytesToHex, bytesToUtf8, createBuffer, hexToBytes, utf8ToBytes, } from "../Buffer.js";
|
|
128
158
|
import { padmePaddingLength, } from "../Crypto.js";
|
|
129
159
|
import { eqArrayNumber } from "../Eq.js";
|
|
130
160
|
import { computeBalancedBuckets } from "../Number.js";
|
|
131
161
|
import { objectToEntries } from "../Object.js";
|
|
132
162
|
import { err, ok } from "../Result.js";
|
|
133
|
-
import { Base64Url,
|
|
134
|
-
import {
|
|
135
|
-
import {
|
|
163
|
+
import { Base64Url, base64UrlToUint8Array, DateIso, Id, idBytesToId, idBytesTypeValueLength, idToIdBytes, Json, jsonToJsonValue, NonNegativeInt, Number, uint8ArrayToBase64Url, } from "../Type.js";
|
|
164
|
+
import { ownerIdToOwnerIdBytes, ownerWriteKeyLength, } from "./Owner.js";
|
|
165
|
+
import { fingerprintSize, InfiniteUpperBound, RangeType, } from "./Storage.js";
|
|
166
|
+
import { Counter, eqTimestamp, Millis, timestampBytesLength, timestampBytesToTimestamp, timestampToTimestampBytes, } from "./Timestamp.js";
|
|
167
|
+
/**
|
|
168
|
+
* MessagePack serializer for standard compatibility and compact encoding.
|
|
169
|
+
*
|
|
170
|
+
* - `variableMapSize: true` - More compact maps, ~5-10% slower encoding
|
|
171
|
+
* - `useRecords: false` - Standard MessagePack without extensions
|
|
172
|
+
*/
|
|
173
|
+
const packr = new Packr({ variableMapSize: true, useRecords: false });
|
|
136
174
|
/** Maximum size of the entire protocol message in bytes. */
|
|
137
175
|
export const maxProtocolMessageSize = 1_000_000;
|
|
138
176
|
/** Maximum size of the ranges in bytes. */
|
|
139
177
|
export const maxProtocolMessageRangesSize = 30_000;
|
|
140
178
|
/** Evolu Protocol version. */
|
|
141
179
|
export const protocolVersion = 0;
|
|
180
|
+
export const MessageType = {
|
|
181
|
+
/** Request message from initiator (client) to non-initiator (relay). */
|
|
182
|
+
Request: 0,
|
|
183
|
+
/** Response message from non-initiator (relay) to initiator (client). */
|
|
184
|
+
Response: 1,
|
|
185
|
+
/** Broadcast message from non-initiator (relay) to subscribed clients. */
|
|
186
|
+
Broadcast: 2,
|
|
187
|
+
};
|
|
188
|
+
export const SubscriptionFlags = {
|
|
189
|
+
/** No subscription changes for this owner. */
|
|
190
|
+
None: 0,
|
|
191
|
+
/** Subscribe to updates for this owner. */
|
|
192
|
+
Subscribe: 1,
|
|
193
|
+
/** Unsubscribe from updates for this owner. */
|
|
194
|
+
Unsubscribe: 2,
|
|
195
|
+
};
|
|
142
196
|
export const ProtocolErrorCode = {
|
|
143
197
|
NoError: 0,
|
|
144
198
|
/** A code for {@link ProtocolWriteKeyError}. */
|
|
@@ -148,15 +202,6 @@ export const ProtocolErrorCode = {
|
|
|
148
202
|
/** A code for {@link ProtocolSyncError}. */
|
|
149
203
|
SyncError: 3,
|
|
150
204
|
};
|
|
151
|
-
export const RangeType = {
|
|
152
|
-
Fingerprint: 1,
|
|
153
|
-
Skip: 0,
|
|
154
|
-
Timestamps: 2,
|
|
155
|
-
};
|
|
156
|
-
export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
|
|
157
|
-
export const fingerprintSize = 12;
|
|
158
|
-
/** A fingerprint of an empty range. */
|
|
159
|
-
export const zeroFingerprint = new Uint8Array(fingerprintSize);
|
|
160
205
|
/**
|
|
161
206
|
* Creates a {@link ProtocolMessage} from CRDT messages.
|
|
162
207
|
*
|
|
@@ -166,12 +211,13 @@ export const zeroFingerprint = new Uint8Array(fingerprintSize);
|
|
|
166
211
|
*/
|
|
167
212
|
export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages, maxSize) => {
|
|
168
213
|
const buffer = createProtocolMessageBuffer(owner.id, {
|
|
214
|
+
messageType: MessageType.Request,
|
|
169
215
|
totalMaxSize: maxSize ?? maxProtocolMessageSize,
|
|
170
216
|
writeKey: owner.writeKey,
|
|
171
217
|
});
|
|
172
218
|
let notAllMessagesSent = false;
|
|
173
219
|
for (const message of messages) {
|
|
174
|
-
const change = encodeAndEncryptDbChange(deps)(message
|
|
220
|
+
const change = encodeAndEncryptDbChange(deps)(message, owner.encryptionKey);
|
|
175
221
|
const encryptedCrdtMessage = { timestamp: message.timestamp, change };
|
|
176
222
|
if (buffer.canAddMessage(encryptedCrdtMessage)) {
|
|
177
223
|
buffer.addMessage(encryptedCrdtMessage);
|
|
@@ -197,7 +243,7 @@ export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages,
|
|
|
197
243
|
* For now, using a random fingerprint avoids extra complexity and is good
|
|
198
244
|
* enough for this case.
|
|
199
245
|
*/
|
|
200
|
-
const randomFingerprint = deps.
|
|
246
|
+
const randomFingerprint = deps.randomBytes.create(fingerprintSize);
|
|
201
247
|
// There is always a space for Fingerprint with InfiniteUpperBound.
|
|
202
248
|
buffer.addRange({
|
|
203
249
|
type: RangeType.Fingerprint,
|
|
@@ -208,18 +254,25 @@ export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages,
|
|
|
208
254
|
return buffer.unwrap();
|
|
209
255
|
};
|
|
210
256
|
/** Creates a {@link ProtocolMessage} for sync. */
|
|
211
|
-
export const createProtocolMessageForSync = (deps) => (ownerId) => {
|
|
212
|
-
const buffer = createProtocolMessageBuffer(ownerId
|
|
213
|
-
|
|
214
|
-
|
|
257
|
+
export const createProtocolMessageForSync = (deps) => (ownerId, subscriptionFlag) => {
|
|
258
|
+
const buffer = createProtocolMessageBuffer(ownerId, {
|
|
259
|
+
messageType: MessageType.Request,
|
|
260
|
+
subscriptionFlag: subscriptionFlag ?? SubscriptionFlags.None,
|
|
261
|
+
});
|
|
262
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
263
|
+
const size = deps.storage.getSize(ownerIdBytes);
|
|
215
264
|
// Errors are handled by the storage.
|
|
216
265
|
if (size == null)
|
|
217
266
|
return null;
|
|
218
|
-
splitRange(deps)(
|
|
267
|
+
splitRange(deps)(ownerIdBytes, 0, size, InfiniteUpperBound, buffer);
|
|
219
268
|
return buffer.unwrap();
|
|
220
269
|
};
|
|
221
|
-
export const
|
|
222
|
-
|
|
270
|
+
export const createProtocolMessageForUnsubscribe = (ownerId) => createProtocolMessageBuffer(ownerId, {
|
|
271
|
+
messageType: MessageType.Request,
|
|
272
|
+
subscriptionFlag: SubscriptionFlags.Unsubscribe,
|
|
273
|
+
}).unwrap();
|
|
274
|
+
export const createProtocolMessageBuffer = (ownerId, options) => {
|
|
275
|
+
const { totalMaxSize = maxProtocolMessageSize, rangesMaxSize = maxProtocolMessageRangesSize, version = protocolVersion, } = options;
|
|
223
276
|
const buffers = {
|
|
224
277
|
header: createBuffer(),
|
|
225
278
|
messages: {
|
|
@@ -233,18 +286,28 @@ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
|
|
|
233
286
|
},
|
|
234
287
|
};
|
|
235
288
|
encodeNonNegativeInt(buffers.header, version);
|
|
236
|
-
buffers.header.extend(
|
|
237
|
-
|
|
238
|
-
|
|
289
|
+
buffers.header.extend(ownerIdToOwnerIdBytes(ownerId));
|
|
290
|
+
buffers.header.extend([options.messageType]);
|
|
291
|
+
if (options.messageType === MessageType.Request) {
|
|
292
|
+
if (!options.writeKey) {
|
|
293
|
+
buffers.header.extend([0]);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
buffers.header.extend([1]);
|
|
297
|
+
buffers.header.extend(options.writeKey);
|
|
298
|
+
}
|
|
299
|
+
const subscriptionFlag = options.subscriptionFlag ?? SubscriptionFlags.None;
|
|
300
|
+
buffers.header.extend([subscriptionFlag]);
|
|
301
|
+
}
|
|
302
|
+
else if (options.messageType === MessageType.Response) {
|
|
303
|
+
buffers.header.extend([options.errorCode]);
|
|
304
|
+
}
|
|
239
305
|
let isLastRangeInfinite = false;
|
|
240
306
|
const isWithinSizeLimits = () => getSize() <= totalMaxSize;
|
|
241
307
|
const getSize = () => (getHeaderAndMessagesSize() + getRangesSize());
|
|
242
308
|
const getHeaderAndMessagesSize = () => buffers.header.getLength() +
|
|
243
309
|
buffers.messages.timestamps.getLength() +
|
|
244
|
-
buffers.messages.dbChanges.getLength()
|
|
245
|
-
(buffers.messages.timestamps.getCount() > 0 && writeKey
|
|
246
|
-
? writeKeyLength
|
|
247
|
-
: 0);
|
|
310
|
+
buffers.messages.dbChanges.getLength();
|
|
248
311
|
const getRangesSize = () => buffers.ranges.timestamps.getCount() > 0
|
|
249
312
|
? buffers.ranges.timestamps.getLength() +
|
|
250
313
|
buffers.ranges.types.getLength() +
|
|
@@ -298,6 +361,7 @@ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
|
|
|
298
361
|
: true));
|
|
299
362
|
},
|
|
300
363
|
addRange: (range) => {
|
|
364
|
+
assert(options.messageType !== MessageType.Broadcast, "Cannot add a range into broadcast message");
|
|
301
365
|
assert(!isLastRangeInfinite, "Cannot add a range after an InfiniteUpperBound range");
|
|
302
366
|
isLastRangeInfinite = range.upperBound === InfiniteUpperBound;
|
|
303
367
|
/**
|
|
@@ -306,7 +370,7 @@ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
|
|
|
306
370
|
* use SkipRange.
|
|
307
371
|
*/
|
|
308
372
|
if (range.upperBound !== InfiniteUpperBound)
|
|
309
|
-
buffers.ranges.timestamps.add(
|
|
373
|
+
buffers.ranges.timestamps.add(timestampBytesToTimestamp(range.upperBound));
|
|
310
374
|
else {
|
|
311
375
|
buffers.ranges.timestamps.addInfinite();
|
|
312
376
|
}
|
|
@@ -330,8 +394,6 @@ export const createProtocolMessageBuffer = (ownerId, options = {}) => {
|
|
|
330
394
|
}
|
|
331
395
|
buffers.messages.timestamps.append(buffers.header);
|
|
332
396
|
buffers.header.extend(buffers.messages.dbChanges.unwrap());
|
|
333
|
-
if (buffers.messages.timestamps.getCount() > 0 && writeKey)
|
|
334
|
-
buffers.header.extend(writeKey);
|
|
335
397
|
if (buffers.ranges.timestamps.getCount() > 0) {
|
|
336
398
|
buffers.ranges.timestamps.append(buffers.header);
|
|
337
399
|
buffers.header.extend(buffers.ranges.types.unwrap());
|
|
@@ -409,118 +471,220 @@ const createRunLengthEncoder = (encodeValue) => {
|
|
|
409
471
|
unwrap: () => buffer.unwrap(),
|
|
410
472
|
};
|
|
411
473
|
};
|
|
412
|
-
export const applyProtocolMessageAsClient = (deps) => (inputMessage,
|
|
413
|
-
|
|
414
|
-
|
|
474
|
+
export const applyProtocolMessageAsClient = (deps) => async (inputMessage, options = {}) => {
|
|
475
|
+
// try-catch instead of Result for performance and stacktraces
|
|
476
|
+
try {
|
|
477
|
+
const input = createBuffer(inputMessage);
|
|
478
|
+
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
479
|
+
const version = options.version ?? protocolVersion;
|
|
480
|
+
if (requestedVersion !== version) {
|
|
481
|
+
return err({
|
|
482
|
+
type: "ProtocolUnsupportedVersionError",
|
|
483
|
+
unsupportedVersion: requestedVersion,
|
|
484
|
+
isInitiator: version < requestedVersion,
|
|
485
|
+
ownerId,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
const messageType = input.shift();
|
|
489
|
+
assert(messageType === MessageType.Response ||
|
|
490
|
+
messageType === MessageType.Broadcast, "Invalid MessageType");
|
|
491
|
+
if (messageType === MessageType.Response) {
|
|
492
|
+
const errorCode = input.shift();
|
|
493
|
+
if (errorCode !== ProtocolErrorCode.NoError) {
|
|
494
|
+
switch (errorCode) {
|
|
495
|
+
case ProtocolErrorCode.WriteKeyError:
|
|
496
|
+
return err({
|
|
497
|
+
type: "ProtocolWriteKeyError",
|
|
498
|
+
ownerId,
|
|
499
|
+
});
|
|
500
|
+
case ProtocolErrorCode.WriteError:
|
|
501
|
+
return err({
|
|
502
|
+
type: "ProtocolWriteError",
|
|
503
|
+
ownerId,
|
|
504
|
+
});
|
|
505
|
+
case ProtocolErrorCode.SyncError:
|
|
506
|
+
return err({
|
|
507
|
+
type: "ProtocolSyncError",
|
|
508
|
+
ownerId,
|
|
509
|
+
});
|
|
510
|
+
default:
|
|
511
|
+
throw new ProtocolDecodeError(`Invalid ProtocolErrorCode: ${errorCode}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const messages = decodeMessages(input);
|
|
516
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
517
|
+
if (isNonEmptyReadonlyArray(messages) &&
|
|
518
|
+
!(await deps.storage.writeMessages(ownerIdBytes, messages))) {
|
|
519
|
+
return ok({ type: "no-response" });
|
|
520
|
+
}
|
|
521
|
+
// Now: No writeKey, no sync.
|
|
522
|
+
// TODO: Allow to sync SharedReadonlyOwner
|
|
523
|
+
// Without local changes, writeKey will not be required.
|
|
524
|
+
// With local changes, writeKey will be required and if not provided,
|
|
525
|
+
// the sync will stop.
|
|
526
|
+
const writeKey = options.getWriteKey?.(ownerId);
|
|
527
|
+
if (writeKey == null) {
|
|
528
|
+
return ok({ type: "no-response" });
|
|
529
|
+
}
|
|
530
|
+
if (messageType === MessageType.Broadcast) {
|
|
531
|
+
return ok({ type: "broadcast" });
|
|
532
|
+
}
|
|
533
|
+
const ranges = decodeRanges(input);
|
|
534
|
+
if (!isNonEmptyReadonlyArray(ranges)) {
|
|
535
|
+
return ok({ type: "no-response" });
|
|
536
|
+
}
|
|
537
|
+
const output = createProtocolMessageBuffer(ownerId, {
|
|
538
|
+
messageType: MessageType.Request,
|
|
539
|
+
writeKey,
|
|
540
|
+
totalMaxSize: options.totalMaxSize,
|
|
541
|
+
rangesMaxSize: options.rangesMaxSize,
|
|
542
|
+
});
|
|
543
|
+
const syncResult = sync(deps)(ranges, output, ownerIdBytes);
|
|
544
|
+
// Client sync error (handled via Storage) or no changes.
|
|
545
|
+
if (!syncResult.ok || !syncResult.value) {
|
|
546
|
+
return ok({ type: "no-response" });
|
|
547
|
+
}
|
|
548
|
+
return ok({ type: "response", message: output.unwrap() });
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
415
551
|
return err({
|
|
416
|
-
type: "
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
ownerId,
|
|
552
|
+
type: "ProtocolInvalidDataError",
|
|
553
|
+
data: inputMessage,
|
|
554
|
+
error,
|
|
420
555
|
});
|
|
421
556
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
557
|
+
};
|
|
558
|
+
export const applyProtocolMessageAsRelay = (deps) => async (inputMessage, options = {},
|
|
559
|
+
/** For testing purposes only; should not be used in production. */
|
|
560
|
+
version = protocolVersion) => {
|
|
561
|
+
// try-catch instead of Result for performance and stacktraces
|
|
562
|
+
try {
|
|
563
|
+
const input = createBuffer(inputMessage);
|
|
564
|
+
const [requestedVersion, ownerId] = decodeVersionAndOwner(input);
|
|
565
|
+
const ownerIdBytes = ownerIdToOwnerIdBytes(ownerId);
|
|
566
|
+
if (requestedVersion !== version) {
|
|
567
|
+
// Non-initiator responds with its version and ownerId.
|
|
568
|
+
const output = createBuffer();
|
|
569
|
+
encodeNonNegativeInt(output, version);
|
|
570
|
+
output.extend(ownerIdBytes);
|
|
571
|
+
return ok({
|
|
572
|
+
type: "response",
|
|
573
|
+
message: output.unwrap(),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
const messageType = input.shift();
|
|
577
|
+
assert(messageType === MessageType.Request, "Invalid MessageType");
|
|
578
|
+
const hasWriteKey = input.shift();
|
|
579
|
+
let writeKey;
|
|
580
|
+
if (hasWriteKey === 1) {
|
|
581
|
+
writeKey = input.shiftN(ownerWriteKeyLength);
|
|
582
|
+
}
|
|
583
|
+
const subscriptionFlag = input.shift();
|
|
584
|
+
switch (subscriptionFlag) {
|
|
585
|
+
case SubscriptionFlags.Subscribe:
|
|
586
|
+
options.subscribe?.(ownerId);
|
|
587
|
+
break;
|
|
588
|
+
case SubscriptionFlags.Unsubscribe:
|
|
589
|
+
options.unsubscribe?.(ownerId);
|
|
590
|
+
break;
|
|
591
|
+
case SubscriptionFlags.None:
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
if (writeKey) {
|
|
595
|
+
const isValid = deps.storage.validateWriteKey(ownerIdBytes, writeKey);
|
|
596
|
+
if (!isValid) {
|
|
597
|
+
return ok({
|
|
598
|
+
type: "response",
|
|
599
|
+
message: createProtocolMessageBuffer(ownerId, {
|
|
600
|
+
messageType: MessageType.Response,
|
|
601
|
+
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
602
|
+
}).unwrap(),
|
|
430
603
|
});
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const messages = decodeMessages(input);
|
|
607
|
+
if (isNonEmptyReadonlyArray(messages)) {
|
|
608
|
+
if (!writeKey) {
|
|
609
|
+
return ok({
|
|
610
|
+
type: "response",
|
|
611
|
+
message: createProtocolMessageBuffer(ownerId, {
|
|
612
|
+
messageType: MessageType.Response,
|
|
613
|
+
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
614
|
+
}).unwrap(),
|
|
435
615
|
});
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Broadcast messages to all subscribed devices. This ensures real-time
|
|
619
|
+
* synchronization between clients.
|
|
620
|
+
*
|
|
621
|
+
* When a relay's database is deleted or clients migrate to a new relay
|
|
622
|
+
* (without data migration), clients will sync their data to the relay,
|
|
623
|
+
* and the relay will broadcast those messages to other connected
|
|
624
|
+
* clients. Those clients may receive messages they already have, but
|
|
625
|
+
* this is safe because `applyMessages` is idempotent. As the relay
|
|
626
|
+
* becomes more synchronized with clients over time, fewer duplicate
|
|
627
|
+
* messages will be broadcasted.
|
|
628
|
+
*/
|
|
629
|
+
if (options.broadcast) {
|
|
630
|
+
const broadcastBuffer = createProtocolMessageBuffer(ownerId, {
|
|
631
|
+
messageType: MessageType.Broadcast,
|
|
632
|
+
totalMaxSize: options.totalMaxSize,
|
|
633
|
+
rangesMaxSize: options.rangesMaxSize,
|
|
634
|
+
version,
|
|
440
635
|
});
|
|
441
|
-
|
|
442
|
-
|
|
636
|
+
for (const message of messages) {
|
|
637
|
+
broadcastBuffer.addMessage(message);
|
|
638
|
+
}
|
|
639
|
+
options.broadcast(ownerId, broadcastBuffer.unwrap());
|
|
640
|
+
}
|
|
641
|
+
if (!(await deps.storage.writeMessages(ownerIdBytes, messages))) {
|
|
642
|
+
return ok({
|
|
643
|
+
type: "response",
|
|
644
|
+
message: createProtocolMessageBuffer(ownerId, {
|
|
645
|
+
messageType: MessageType.Response,
|
|
646
|
+
errorCode: ProtocolErrorCode.WriteError,
|
|
647
|
+
}).unwrap(),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
443
650
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const output = createProtocolMessageBuffer(ownerId, {
|
|
456
|
-
writeKey,
|
|
457
|
-
totalMaxSize,
|
|
458
|
-
rangesMaxSize,
|
|
459
|
-
});
|
|
460
|
-
return sync(deps)("initiator", input, output, binaryOwnerId);
|
|
461
|
-
});
|
|
462
|
-
export const applyProtocolMessageAsRelay = (deps) => (inputMessage, { subscribe, broadcast, totalMaxSize, rangesMaxSize, } = {},
|
|
463
|
-
/** For testing purposes only; should not be used in production. */
|
|
464
|
-
version = protocolVersion) => tryDecodeProtocolData(inputMessage, (input) => {
|
|
465
|
-
const requestedVersion = decodeNonNegativeInt(input);
|
|
466
|
-
const ownerId = decodeOwnerId(input);
|
|
467
|
-
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
468
|
-
if (requestedVersion !== version) {
|
|
469
|
-
// Non-initiator responds with its version and ownerId.
|
|
470
|
-
const output = createBuffer();
|
|
471
|
-
encodeNonNegativeInt(output, version);
|
|
472
|
-
output.extend(binaryOwnerId);
|
|
473
|
-
return ok(output.unwrap());
|
|
474
|
-
}
|
|
475
|
-
subscribe?.(ownerId);
|
|
476
|
-
const messages = decodeMessages(input);
|
|
477
|
-
if (isNonEmptyReadonlyArray(messages)) {
|
|
478
|
-
const messagesEnd = inputMessage.length - input.getLength();
|
|
479
|
-
const writeKey = input.shiftN(writeKeyLength);
|
|
480
|
-
const writeKeyIsValid = deps.storage.validateWriteKey(binaryOwnerId, writeKey);
|
|
481
|
-
if (!writeKeyIsValid)
|
|
482
|
-
return ok(createProtocolMessageBuffer(ownerId, {
|
|
483
|
-
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
484
|
-
}).unwrap());
|
|
485
|
-
if (broadcast) {
|
|
486
|
-
// Instead of encoding a new protocol message, we reuse the inputMessage.
|
|
487
|
-
const broadcastMessage = concatBytes(inputMessage.slice(0, 17), new Uint8Array([ProtocolErrorCode.NoError]), inputMessage.slice(17, messagesEnd));
|
|
488
|
-
broadcast(ownerId, broadcastMessage);
|
|
651
|
+
const ranges = decodeRanges(input);
|
|
652
|
+
const output = createProtocolMessageBuffer(ownerId, {
|
|
653
|
+
messageType: MessageType.Response,
|
|
654
|
+
errorCode: ProtocolErrorCode.NoError,
|
|
655
|
+
totalMaxSize: options.totalMaxSize,
|
|
656
|
+
rangesMaxSize: options.rangesMaxSize,
|
|
657
|
+
});
|
|
658
|
+
// Non-initiators always respond to provide sync completion feedback,
|
|
659
|
+
// even when there's nothing to sync.
|
|
660
|
+
if (!isNonEmptyReadonlyArray(ranges)) {
|
|
661
|
+
return ok({ type: "response", message: output.unwrap() });
|
|
489
662
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
});
|
|
500
|
-
return sync(deps)("non-initiator", input, output, binaryOwnerId);
|
|
501
|
-
});
|
|
502
|
-
/**
|
|
503
|
-
* Wraps Evolu Protocol decoding functions, which use exceptions instead of
|
|
504
|
-
* {@link Result} to provide stack traces for debugging and reduce allocation
|
|
505
|
-
* overhead in success cases.
|
|
506
|
-
*/
|
|
507
|
-
const tryDecodeProtocolData = (data, callback) => {
|
|
508
|
-
try {
|
|
509
|
-
return callback(createBuffer(data));
|
|
663
|
+
const syncResult = sync(deps)(ranges, output, ownerIdBytes);
|
|
664
|
+
const message = syncResult.ok
|
|
665
|
+
? output.unwrap()
|
|
666
|
+
: createProtocolMessageBuffer(ownerId, {
|
|
667
|
+
messageType: MessageType.Response,
|
|
668
|
+
errorCode: syncResult.error,
|
|
669
|
+
}).unwrap();
|
|
670
|
+
// Non-initiators always respond to provide sync completion feedback,
|
|
671
|
+
return ok({ type: "response", message });
|
|
510
672
|
}
|
|
511
673
|
catch (error) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
});
|
|
518
|
-
throw error;
|
|
674
|
+
return err({
|
|
675
|
+
type: "ProtocolInvalidDataError",
|
|
676
|
+
data: inputMessage,
|
|
677
|
+
error,
|
|
678
|
+
});
|
|
519
679
|
}
|
|
520
680
|
};
|
|
521
681
|
const decodeVersionAndOwner = (input) => {
|
|
682
|
+
// This structure must never change across protocol versions. The version
|
|
683
|
+
// and owner ID must always be the first two fields in every protocol message
|
|
684
|
+
// to enable version negotiation and owner identification before any other
|
|
685
|
+
// processing occurs.
|
|
522
686
|
const version = decodeNonNegativeInt(input);
|
|
523
|
-
const ownerId =
|
|
687
|
+
const ownerId = decodeId(input);
|
|
524
688
|
return [version, ownerId];
|
|
525
689
|
};
|
|
526
690
|
/**
|
|
@@ -544,27 +708,11 @@ const decodeMessages = (buffer) => {
|
|
|
544
708
|
}
|
|
545
709
|
return messages;
|
|
546
710
|
};
|
|
547
|
-
const sync = (deps) => (
|
|
548
|
-
const ranges = decodeRanges(input);
|
|
549
|
-
if (!isNonEmptyReadonlyArray(ranges)) {
|
|
550
|
-
// Nothing to sync.
|
|
551
|
-
return ok(null);
|
|
552
|
-
}
|
|
553
|
-
const binaryOwnerId = binaryOwnerIdToOwnerId(ownerId);
|
|
711
|
+
const sync = (deps) => (ranges, output, ownerIdBytes) => {
|
|
554
712
|
const outputInitialSize = output.getSize();
|
|
555
|
-
const
|
|
556
|
-
// Only the relay (non-initiator) reports sync errors, not the client (initiator).
|
|
557
|
-
if (role === "initiator") {
|
|
558
|
-
return ok(null);
|
|
559
|
-
}
|
|
560
|
-
const message = createProtocolMessageBuffer(binaryOwnerId, {
|
|
561
|
-
errorCode: ProtocolErrorCode.SyncError,
|
|
562
|
-
});
|
|
563
|
-
return ok(message.unwrap());
|
|
564
|
-
};
|
|
565
|
-
const storageSize = deps.storage.getSize(ownerId);
|
|
713
|
+
const storageSize = deps.storage.getSize(ownerIdBytes);
|
|
566
714
|
if (storageSize == null)
|
|
567
|
-
return
|
|
715
|
+
return err(ProtocolErrorCode.SyncError);
|
|
568
716
|
let prevUpperBound = null;
|
|
569
717
|
let prevIndex = 0;
|
|
570
718
|
let skip = false;
|
|
@@ -596,7 +744,7 @@ const sync = (deps) => (role, input, output, ownerId) => {
|
|
|
596
744
|
};
|
|
597
745
|
// When we don't have a space...
|
|
598
746
|
const addFingerprintForRemainingRange = (begin) => {
|
|
599
|
-
const fingerprint = deps.storage.fingerprint(
|
|
747
|
+
const fingerprint = deps.storage.fingerprint(ownerIdBytes, begin, storageSize);
|
|
600
748
|
if (!fingerprint)
|
|
601
749
|
return false;
|
|
602
750
|
// There is always a space for a ramaining range.
|
|
@@ -610,30 +758,30 @@ const sync = (deps) => (role, input, output, ownerId) => {
|
|
|
610
758
|
for (const range of ranges) {
|
|
611
759
|
const currentUpperBound = range.upperBound;
|
|
612
760
|
const lower = prevIndex;
|
|
613
|
-
let upper = deps.storage.findLowerBound(
|
|
761
|
+
let upper = deps.storage.findLowerBound(ownerIdBytes, prevIndex, storageSize, currentUpperBound);
|
|
614
762
|
if (upper == null)
|
|
615
|
-
return
|
|
763
|
+
return err(ProtocolErrorCode.SyncError);
|
|
616
764
|
switch (range.type) {
|
|
617
765
|
case RangeType.Skip: {
|
|
618
766
|
skipRange(range);
|
|
619
767
|
break;
|
|
620
768
|
}
|
|
621
769
|
case RangeType.Fingerprint: {
|
|
622
|
-
const ourFingerprint = deps.storage.fingerprint(
|
|
770
|
+
const ourFingerprint = deps.storage.fingerprint(ownerIdBytes, lower, upper);
|
|
623
771
|
if (ourFingerprint == null)
|
|
624
|
-
return
|
|
772
|
+
return err(ProtocolErrorCode.SyncError);
|
|
625
773
|
if (eqArrayNumber(range.fingerprint, ourFingerprint)) {
|
|
626
774
|
skipRange(range);
|
|
627
775
|
}
|
|
628
776
|
else {
|
|
629
777
|
if (output.canSplitRange()) {
|
|
630
778
|
coalesceSkipsBeforeAdd();
|
|
631
|
-
splitRange(deps)(
|
|
779
|
+
splitRange(deps)(ownerIdBytes, lower, upper, currentUpperBound, output);
|
|
632
780
|
}
|
|
633
781
|
else {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
782
|
+
return addFingerprintForRemainingRange(upper)
|
|
783
|
+
? ok(true)
|
|
784
|
+
: err(ProtocolErrorCode.SyncError);
|
|
637
785
|
}
|
|
638
786
|
}
|
|
639
787
|
break;
|
|
@@ -642,19 +790,19 @@ const sync = (deps) => (role, input, output, ownerId) => {
|
|
|
642
790
|
let endBound = currentUpperBound;
|
|
643
791
|
const timestampsWeNeed = new Map(range.timestamps.map((t) => [t.join(), true]));
|
|
644
792
|
const ourTimestamps = createTimestampsBuffer();
|
|
645
|
-
let
|
|
793
|
+
let cantReadDbChange = false;
|
|
646
794
|
let exceeded = false;
|
|
647
|
-
deps.storage.iterate(
|
|
795
|
+
deps.storage.iterate(ownerIdBytes, lower, upper, (timestamp, index) => {
|
|
648
796
|
const timestampString = timestamp.join();
|
|
649
|
-
const timestampBinary =
|
|
797
|
+
const timestampBinary = timestampBytesToTimestamp(timestamp);
|
|
650
798
|
let message = null;
|
|
651
799
|
if (timestampsWeNeed.has(timestampString)) {
|
|
652
800
|
timestampsWeNeed.delete(timestampString);
|
|
653
801
|
}
|
|
654
802
|
else {
|
|
655
|
-
const dbChange = deps.storage.readDbChange(
|
|
803
|
+
const dbChange = deps.storage.readDbChange(ownerIdBytes, timestamp);
|
|
656
804
|
if (dbChange == null) {
|
|
657
|
-
|
|
805
|
+
cantReadDbChange = true;
|
|
658
806
|
return false;
|
|
659
807
|
}
|
|
660
808
|
message = {
|
|
@@ -673,8 +821,8 @@ const sync = (deps) => (role, input, output, ownerId) => {
|
|
|
673
821
|
output.addMessage(message);
|
|
674
822
|
return true;
|
|
675
823
|
});
|
|
676
|
-
if (
|
|
677
|
-
return
|
|
824
|
+
if (cantReadDbChange) {
|
|
825
|
+
return err(ProtocolErrorCode.SyncError);
|
|
678
826
|
}
|
|
679
827
|
const addRange = () => {
|
|
680
828
|
coalesceSkipsBeforeAdd();
|
|
@@ -687,9 +835,9 @@ const sync = (deps) => (role, input, output, ownerId) => {
|
|
|
687
835
|
if (exceeded) {
|
|
688
836
|
addRange();
|
|
689
837
|
if (!addFingerprintForRemainingRange(upper)) {
|
|
690
|
-
return
|
|
838
|
+
return err(ProtocolErrorCode.SyncError);
|
|
691
839
|
}
|
|
692
|
-
return ok(
|
|
840
|
+
return ok(true);
|
|
693
841
|
}
|
|
694
842
|
// If we need something, we have to respond with our timestamps.
|
|
695
843
|
if (timestampsWeNeed.size > 0) {
|
|
@@ -706,7 +854,7 @@ const sync = (deps) => (role, input, output, ownerId) => {
|
|
|
706
854
|
}
|
|
707
855
|
// If all ranges were skipped, there are no changes and sync is complete.
|
|
708
856
|
const hasChange = output.getSize() > outputInitialSize;
|
|
709
|
-
return ok(hasChange
|
|
857
|
+
return ok(hasChange);
|
|
710
858
|
};
|
|
711
859
|
const splitRange = (deps) => (ownerId, lower, upper, upperBound, buffer) => {
|
|
712
860
|
const itemCount = (upper - lower);
|
|
@@ -718,7 +866,7 @@ const splitRange = (deps) => (ownerId, lower, upper, upperBound, buffer) => {
|
|
|
718
866
|
timestamps: createTimestampsBuffer(),
|
|
719
867
|
};
|
|
720
868
|
deps.storage.iterate(ownerId, 0, itemCount, (timestamp) => {
|
|
721
|
-
range.timestamps.add(
|
|
869
|
+
range.timestamps.add(timestampBytesToTimestamp(timestamp));
|
|
722
870
|
return true;
|
|
723
871
|
});
|
|
724
872
|
buffer.addRange(range);
|
|
@@ -761,7 +909,7 @@ const decodeRanges = (buffer) => {
|
|
|
761
909
|
const ranges = [];
|
|
762
910
|
for (let i = 0; i < rangesCount; i++) {
|
|
763
911
|
const upperBound = i < timestampsCount
|
|
764
|
-
?
|
|
912
|
+
? timestampToTimestampBytes(timestamps[i])
|
|
765
913
|
: InfiniteUpperBound;
|
|
766
914
|
const rangeType = rangeTypes[i];
|
|
767
915
|
switch (rangeType) {
|
|
@@ -778,7 +926,7 @@ const decodeRanges = (buffer) => {
|
|
|
778
926
|
break;
|
|
779
927
|
}
|
|
780
928
|
case RangeType.Timestamps: {
|
|
781
|
-
const timestamps = decodeTimestamps(buffer).map(
|
|
929
|
+
const timestamps = decodeTimestamps(buffer).map(timestampToTimestampBytes);
|
|
782
930
|
ranges.push({
|
|
783
931
|
type: RangeType.Timestamps,
|
|
784
932
|
upperBound,
|
|
@@ -799,7 +947,7 @@ const decodeTimestamps = (buffer, length) => {
|
|
|
799
947
|
const deltaMillis = decodeNonNegativeInt(buffer);
|
|
800
948
|
const millis = Millis.from(previousMillis + deltaMillis);
|
|
801
949
|
if (!millis.ok)
|
|
802
|
-
throw new
|
|
950
|
+
throw new ProtocolDecodeError(millis.error.type);
|
|
803
951
|
millises.push(millis.value);
|
|
804
952
|
previousMillis = millis.value;
|
|
805
953
|
}
|
|
@@ -808,7 +956,7 @@ const decodeTimestamps = (buffer, length) => {
|
|
|
808
956
|
while (counterIndex < length) {
|
|
809
957
|
const counter = Counter.from(decodeNonNegativeInt(buffer));
|
|
810
958
|
if (!counter.ok)
|
|
811
|
-
throw new
|
|
959
|
+
throw new ProtocolDecodeError(counter.error.type);
|
|
812
960
|
const runLength = decodeNonNegativeInt(buffer);
|
|
813
961
|
for (let i = 0; i < runLength; i++) {
|
|
814
962
|
counters.push(counter.value);
|
|
@@ -835,84 +983,21 @@ const decodeTimestamps = (buffer, length) => {
|
|
|
835
983
|
}
|
|
836
984
|
return timestamps;
|
|
837
985
|
};
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
export const ownerIdToBinaryOwnerId = (ownerId) => base64Url256ToBytes(ownerId);
|
|
842
|
-
export const binaryOwnerIdToOwnerId = (binaryOwnerId) => decodeOwnerId(createBuffer(binaryOwnerId));
|
|
843
|
-
/**
|
|
844
|
-
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
845
|
-
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
846
|
-
*/
|
|
847
|
-
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
848
|
-
/**
|
|
849
|
-
* Alphabet used for Base64Url encoding. This is copied from the `nanoid`
|
|
850
|
-
* library to avoid dependency on a specific version of `nanoid`.
|
|
851
|
-
*/
|
|
852
|
-
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
853
|
-
/**
|
|
854
|
-
* Converts a Base64Url string to a Uint8Array for binary storage. This encoding
|
|
855
|
-
* is more space-efficient than UTF-8 for Base64Url strings.
|
|
856
|
-
*/
|
|
857
|
-
export const base64Url256ToBytes = (string) => {
|
|
858
|
-
const totalBits = string.length * 6; // 6 bits per character
|
|
859
|
-
const byteLength = Math.ceil(totalBits / 8);
|
|
860
|
-
const value = new globalThis.Uint8Array(byteLength);
|
|
861
|
-
let bitBuffer = 0;
|
|
862
|
-
let bitsInBuffer = 0;
|
|
863
|
-
let byteIndex = 0;
|
|
864
|
-
for (const char of string) {
|
|
865
|
-
const charValue = urlAlphabet.indexOf(char);
|
|
866
|
-
bitBuffer = (bitBuffer << 6) | charValue;
|
|
867
|
-
bitsInBuffer += 6;
|
|
868
|
-
while (bitsInBuffer >= 8) {
|
|
869
|
-
bitsInBuffer -= 8;
|
|
870
|
-
value[byteIndex++] = (bitBuffer >> bitsInBuffer) & 0xff;
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
if (bitsInBuffer > 0 && byteIndex < byteLength) {
|
|
874
|
-
value[byteIndex] = (bitBuffer << (8 - bitsInBuffer)) & 0xff;
|
|
875
|
-
}
|
|
876
|
-
return value;
|
|
877
|
-
};
|
|
878
|
-
export const decodeBase64Url256 = (buffer, stringLength) => {
|
|
879
|
-
const bytes = buffer.shiftN(Math.ceil((stringLength * 6) / 8));
|
|
880
|
-
let bitBuffer = 0;
|
|
881
|
-
let bitsInBuffer = 0;
|
|
882
|
-
let string = "";
|
|
883
|
-
for (const byte of bytes) {
|
|
884
|
-
bitBuffer = (bitBuffer << 8) | byte;
|
|
885
|
-
bitsInBuffer += 8;
|
|
886
|
-
while (bitsInBuffer >= 6) {
|
|
887
|
-
bitsInBuffer -= 6;
|
|
888
|
-
if (string.length < stringLength) {
|
|
889
|
-
const charValue = (bitBuffer >> bitsInBuffer) & 0x3f;
|
|
890
|
-
if (charValue < 0 || charValue >= urlAlphabet.length) {
|
|
891
|
-
throw new ProtocolDecodeError("invalid charValue");
|
|
892
|
-
}
|
|
893
|
-
string += urlAlphabet[charValue];
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
}
|
|
897
|
-
const result = Base64Url256.from(string);
|
|
898
|
-
if (!result.ok)
|
|
899
|
-
throw new ProtocolDecodeError(result.error.type);
|
|
900
|
-
return result.value;
|
|
986
|
+
const decodeId = (buffer) => {
|
|
987
|
+
const bytes = buffer.shiftN(idBytesTypeValueLength);
|
|
988
|
+
return idBytesToId(bytes);
|
|
901
989
|
};
|
|
902
|
-
const decodeId = (buffer) => decodeBase64Url256(buffer, idTypeValueLength);
|
|
903
|
-
/** Not all 16 bytes are valid {@link OwnerId}. */
|
|
904
|
-
const decodeOwnerId = (buffer) => decodeId(buffer);
|
|
905
990
|
/**
|
|
906
991
|
* Evolu uses MessagePack to handle all number variants except for
|
|
907
992
|
* NonNegativeInt. For NonNegativeInt, Evolu provides more efficient encoding.
|
|
908
993
|
*/
|
|
909
994
|
export const encodeNumber = (buffer, number) => {
|
|
910
|
-
buffer.extend(pack(number));
|
|
995
|
+
buffer.extend(packr.pack(number));
|
|
911
996
|
};
|
|
912
997
|
export const decodeNumber = (buffer) => {
|
|
913
998
|
let number;
|
|
914
999
|
let end;
|
|
915
|
-
unpackMultiple(buffer.unwrap(), (n, _, e) => {
|
|
1000
|
+
packr.unpackMultiple(buffer.unwrap(), (n, _, e) => {
|
|
916
1001
|
number = n;
|
|
917
1002
|
end = e;
|
|
918
1003
|
return false;
|
|
@@ -926,24 +1011,30 @@ export const decodeNumber = (buffer) => {
|
|
|
926
1011
|
buffer.shiftN(endResult.value);
|
|
927
1012
|
return numberResult.value;
|
|
928
1013
|
};
|
|
929
|
-
export const binaryTimestampToFingerprint = (timestamp) => {
|
|
930
|
-
const hash = sha256(timestamp).slice(0, fingerprintSize);
|
|
931
|
-
return hash;
|
|
932
|
-
};
|
|
933
1014
|
/**
|
|
934
1015
|
* Encodes and encrypts a {@link DbChange} using the provided owner's encryption
|
|
935
1016
|
* key. Returns an encrypted binary representation as {@link EncryptedDbChange}.
|
|
1017
|
+
*
|
|
1018
|
+
* The format includes the protocol version for backward compatibility and the
|
|
1019
|
+
* timestamp for tamper-proof verification that the timestamp matches the change
|
|
1020
|
+
* data.
|
|
936
1021
|
*/
|
|
937
|
-
export const encodeAndEncryptDbChange = (deps) => (
|
|
1022
|
+
export const encodeAndEncryptDbChange = (deps) => (message, key) => {
|
|
1023
|
+
const change = message.change;
|
|
938
1024
|
const buffer = createBuffer();
|
|
939
|
-
|
|
940
|
-
buffer
|
|
1025
|
+
// Encode protocol version first for backward compatibility
|
|
1026
|
+
encodeNonNegativeInt(buffer, protocolVersion);
|
|
1027
|
+
// Encode the timestamp (after version) for tamper verification
|
|
1028
|
+
const timestampBytes = timestampToTimestampBytes(message.timestamp);
|
|
1029
|
+
buffer.extend(timestampBytes);
|
|
1030
|
+
encodeString(buffer, change.table);
|
|
1031
|
+
buffer.extend(idToIdBytes(change.id));
|
|
941
1032
|
const entries = objectToEntries(change.values).map(([column, value]) => {
|
|
942
1033
|
return [column, value];
|
|
943
1034
|
});
|
|
944
1035
|
encodeLength(buffer, entries);
|
|
945
1036
|
for (const [column, value] of entries) {
|
|
946
|
-
|
|
1037
|
+
encodeString(buffer, column);
|
|
947
1038
|
encodeSqliteValue(buffer, value);
|
|
948
1039
|
}
|
|
949
1040
|
const paddingLength = padmePaddingLength(buffer.getLength());
|
|
@@ -957,30 +1048,55 @@ export const encodeAndEncryptDbChange = (deps) => (change, key) => {
|
|
|
957
1048
|
return buffer.unwrap();
|
|
958
1049
|
};
|
|
959
1050
|
/**
|
|
960
|
-
* Decrypts and decodes an {@link
|
|
961
|
-
* encryption key.
|
|
1051
|
+
* Decrypts and decodes an {@link EncryptedCrdtMessage} using the provided
|
|
1052
|
+
* owner's encryption key. Verifies that the embedded timestamp matches the
|
|
1053
|
+
* expected timestamp to ensure message integrity.
|
|
962
1054
|
*/
|
|
963
|
-
export const decryptAndDecodeDbChange = (deps) => (
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const
|
|
979
|
-
|
|
1055
|
+
export const decryptAndDecodeDbChange = (deps) => (message, key) => {
|
|
1056
|
+
// try-catch instead of Result for performance and stacktraces
|
|
1057
|
+
try {
|
|
1058
|
+
const buffer = createBuffer(message.change);
|
|
1059
|
+
const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
|
|
1060
|
+
const ciphertextLength = decodeLength(buffer);
|
|
1061
|
+
const ciphertext = buffer.shiftN(ciphertextLength);
|
|
1062
|
+
const plaintextBytes = deps.symmetricCrypto.decrypt(ciphertext, key, nonce);
|
|
1063
|
+
if (!plaintextBytes.ok)
|
|
1064
|
+
return plaintextBytes;
|
|
1065
|
+
buffer.reset();
|
|
1066
|
+
buffer.extend(plaintextBytes.value);
|
|
1067
|
+
// Decode version (for future compatibility, no validation needed for now)
|
|
1068
|
+
decodeNonNegativeInt(buffer);
|
|
1069
|
+
// Decode and verify the embedded timestamp
|
|
1070
|
+
const embeddedTimestampBytes = buffer.shiftN(timestampBytesLength);
|
|
1071
|
+
const embeddedTimestamp = timestampBytesToTimestamp(embeddedTimestampBytes);
|
|
1072
|
+
// Verify timestamp integrity
|
|
1073
|
+
if (!eqTimestamp(embeddedTimestamp, message.timestamp)) {
|
|
1074
|
+
return err({
|
|
1075
|
+
type: "ProtocolTimestampMismatchError",
|
|
1076
|
+
expected: message.timestamp,
|
|
1077
|
+
embedded: embeddedTimestamp,
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
const table = decodeString(buffer);
|
|
1081
|
+
const id = decodeId(buffer);
|
|
1082
|
+
const length = decodeLength(buffer);
|
|
1083
|
+
const values = Object.create(null);
|
|
1084
|
+
for (let i = 0; i < length; i++) {
|
|
1085
|
+
const column = decodeString(buffer);
|
|
1086
|
+
const value = decodeSqliteValue(buffer);
|
|
1087
|
+
values[column] = value;
|
|
1088
|
+
}
|
|
1089
|
+
const dbChange = { table, id, values };
|
|
1090
|
+
return ok(dbChange);
|
|
980
1091
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
return err({
|
|
1094
|
+
type: "ProtocolInvalidDataError",
|
|
1095
|
+
data: message.change,
|
|
1096
|
+
error,
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
984
1100
|
/**
|
|
985
1101
|
* Encodes a non-negative integer into a variable-length integer format. It's
|
|
986
1102
|
* more efficient than encoding via {@link encodeNumber}.
|
|
@@ -1047,14 +1163,6 @@ export const decodeNodeId = (buffer) => {
|
|
|
1047
1163
|
const bytes = buffer.shiftN(8);
|
|
1048
1164
|
return bytesToHex(bytes);
|
|
1049
1165
|
};
|
|
1050
|
-
export const encodeBase64Url256 = (buffer, string) => {
|
|
1051
|
-
encodeLength(buffer, string);
|
|
1052
|
-
buffer.extend(base64Url256ToBytes(string));
|
|
1053
|
-
};
|
|
1054
|
-
export const decodeBase64Url256WithLength = (buffer) => {
|
|
1055
|
-
const length = decodeLength(buffer);
|
|
1056
|
-
return decodeBase64Url256(buffer, length);
|
|
1057
|
-
};
|
|
1058
1166
|
// Small ints are encoded into ProtocolValueType, saving one byte per int.
|
|
1059
1167
|
const isSmallInt = (value) => value >= 0 && value < 20;
|
|
1060
1168
|
export const ProtocolValueType = {
|
|
@@ -1063,18 +1171,20 @@ export const ProtocolValueType = {
|
|
|
1063
1171
|
String: 20,
|
|
1064
1172
|
Number: 21,
|
|
1065
1173
|
Null: 22,
|
|
1066
|
-
|
|
1174
|
+
Bytes: 23,
|
|
1067
1175
|
// We can add more types for other DBs or anything else later.
|
|
1068
1176
|
// Optimized types
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1177
|
+
NonNegativeInt: 30,
|
|
1178
|
+
// String optimizations
|
|
1179
|
+
EmptyString: 31, // 1 byte vs 2 bytes (50% reduction)
|
|
1180
|
+
Base64Url: 32,
|
|
1181
|
+
Id: 33,
|
|
1182
|
+
Json: 34,
|
|
1073
1183
|
// new Date().toISOString() - 24 bytes
|
|
1074
1184
|
// encoded with fixed length - 8 bytes
|
|
1075
1185
|
// encode as NonNegativeInt - 6 bytes (additional 25% reduction)
|
|
1076
|
-
DateIsoWithNonNegativeTime:
|
|
1077
|
-
DateIsoWithNegativeTime:
|
|
1186
|
+
DateIsoWithNonNegativeTime: 35,
|
|
1187
|
+
DateIsoWithNegativeTime: 36, // 9 bytes
|
|
1078
1188
|
// TODO: Operations (from 40)
|
|
1079
1189
|
// Increment, Decrement, Patch, whatever.
|
|
1080
1190
|
};
|
|
@@ -1085,9 +1195,13 @@ export const encodeSqliteValue = (buffer, value) => {
|
|
|
1085
1195
|
}
|
|
1086
1196
|
switch (typeof value) {
|
|
1087
1197
|
case "string": {
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1198
|
+
if (value === "") {
|
|
1199
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.EmptyString);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
const dateIso = DateIso.fromParent(value);
|
|
1203
|
+
if (dateIso.ok) {
|
|
1204
|
+
const time = new Date(dateIso.value).getTime();
|
|
1091
1205
|
if (NonNegativeInt.is(time)) {
|
|
1092
1206
|
encodeNonNegativeInt(buffer, ProtocolValueType.DateIsoWithNonNegativeTime);
|
|
1093
1207
|
encodeNonNegativeInt(buffer, time);
|
|
@@ -1098,25 +1212,31 @@ export const encodeSqliteValue = (buffer, value) => {
|
|
|
1098
1212
|
}
|
|
1099
1213
|
return;
|
|
1100
1214
|
}
|
|
1101
|
-
const
|
|
1102
|
-
if (
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
buffer.extend(base64Url256ToBytes(base64Url256.value));
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url256);
|
|
1109
|
-
encodeBase64Url256(buffer, base64Url256.value);
|
|
1215
|
+
const id = Id.fromParent(value);
|
|
1216
|
+
if (id.ok) {
|
|
1217
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Id);
|
|
1218
|
+
buffer.extend(idToIdBytes(id.value));
|
|
1110
1219
|
return;
|
|
1111
1220
|
}
|
|
1112
|
-
const
|
|
1113
|
-
if
|
|
1114
|
-
|
|
1221
|
+
const json = Json.fromParent(value);
|
|
1222
|
+
// Only encode as Json if it survives JSON.parse/JSON.stringify round-trip.
|
|
1223
|
+
// Some valid JSON strings like "-0E0" get normalized to "0" during parsing,
|
|
1224
|
+
// which would cause data corruption if we don't verify round-trip safety.
|
|
1225
|
+
if (json.ok && JSON.stringify(jsonToJsonValue(json.value)) === value) {
|
|
1226
|
+
const jsonBytes = packr.pack(jsonToJsonValue(json.value));
|
|
1115
1227
|
encodeNonNegativeInt(buffer, ProtocolValueType.Json);
|
|
1116
1228
|
encodeLength(buffer, jsonBytes);
|
|
1117
1229
|
buffer.extend(jsonBytes);
|
|
1118
1230
|
return;
|
|
1119
1231
|
}
|
|
1232
|
+
const base64Url = Base64Url.fromParent(value);
|
|
1233
|
+
if (base64Url.ok) {
|
|
1234
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url);
|
|
1235
|
+
const bytes = base64UrlToUint8Array(base64Url.value);
|
|
1236
|
+
encodeLength(buffer, bytes);
|
|
1237
|
+
buffer.extend(bytes);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1120
1240
|
encodeNonNegativeInt(buffer, ProtocolValueType.String);
|
|
1121
1241
|
encodeString(buffer, value);
|
|
1122
1242
|
return;
|
|
@@ -1136,7 +1256,7 @@ export const encodeSqliteValue = (buffer, value) => {
|
|
|
1136
1256
|
return;
|
|
1137
1257
|
}
|
|
1138
1258
|
}
|
|
1139
|
-
encodeNonNegativeInt(buffer, ProtocolValueType.
|
|
1259
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Bytes);
|
|
1140
1260
|
encodeLength(buffer, value);
|
|
1141
1261
|
buffer.extend(value);
|
|
1142
1262
|
};
|
|
@@ -1152,33 +1272,57 @@ export const decodeSqliteValue = (buffer) => {
|
|
|
1152
1272
|
return decodeNumber(buffer);
|
|
1153
1273
|
case ProtocolValueType.Null:
|
|
1154
1274
|
return null;
|
|
1155
|
-
case ProtocolValueType.
|
|
1275
|
+
case ProtocolValueType.Bytes: {
|
|
1156
1276
|
const length = decodeLength(buffer);
|
|
1157
1277
|
return buffer.shiftN(length);
|
|
1158
1278
|
}
|
|
1159
|
-
case ProtocolValueType.Id:
|
|
1279
|
+
case ProtocolValueType.Id:
|
|
1160
1280
|
return decodeId(buffer);
|
|
1161
|
-
}
|
|
1162
|
-
case ProtocolValueType.Base64Url256:
|
|
1163
|
-
return decodeBase64Url256WithLength(buffer);
|
|
1164
1281
|
case ProtocolValueType.NonNegativeInt:
|
|
1165
1282
|
return decodeNonNegativeInt(buffer);
|
|
1166
1283
|
case ProtocolValueType.Json: {
|
|
1167
1284
|
const length = decodeLength(buffer);
|
|
1168
1285
|
const bytes = buffer.shiftN(length);
|
|
1169
|
-
return JSON.stringify(unpack(bytes));
|
|
1286
|
+
return JSON.stringify(packr.unpack(bytes));
|
|
1170
1287
|
}
|
|
1171
1288
|
case ProtocolValueType.DateIsoWithNonNegativeTime:
|
|
1172
1289
|
case ProtocolValueType.DateIsoWithNegativeTime: {
|
|
1173
1290
|
const time = type === ProtocolValueType.DateIsoWithNonNegativeTime
|
|
1174
1291
|
? decodeNonNegativeInt(buffer)
|
|
1175
1292
|
: decodeNumber(buffer);
|
|
1176
|
-
const
|
|
1177
|
-
if (!
|
|
1178
|
-
throw new ProtocolDecodeError(
|
|
1179
|
-
return
|
|
1293
|
+
const dateIso = DateIso.fromParent(new Date(time).toISOString());
|
|
1294
|
+
if (!dateIso.ok)
|
|
1295
|
+
throw new ProtocolDecodeError(dateIso.error.type);
|
|
1296
|
+
return dateIso.value;
|
|
1297
|
+
}
|
|
1298
|
+
case ProtocolValueType.EmptyString:
|
|
1299
|
+
return "";
|
|
1300
|
+
case ProtocolValueType.Base64Url: {
|
|
1301
|
+
const length = decodeLength(buffer);
|
|
1302
|
+
const bytes = buffer.shiftN(length);
|
|
1303
|
+
return uint8ArrayToBase64Url(bytes);
|
|
1180
1304
|
}
|
|
1181
1305
|
default:
|
|
1182
1306
|
throw new ProtocolDecodeError("invalid ProtocolValueType");
|
|
1183
1307
|
}
|
|
1184
1308
|
};
|
|
1309
|
+
/**
|
|
1310
|
+
* Decodes a ProtocolMessage into a readable JSON object for debugging.
|
|
1311
|
+
*
|
|
1312
|
+
* Note: This is a stub for future implementation. It should use:
|
|
1313
|
+
*
|
|
1314
|
+
* - DecodeVersionAndOwner
|
|
1315
|
+
* - DecodeError or decodeWriteKeys (depending on context)
|
|
1316
|
+
* - DecodeMessages
|
|
1317
|
+
* - DecodeRanges
|
|
1318
|
+
*
|
|
1319
|
+
* If you want to help, please contribute to this function.
|
|
1320
|
+
*/
|
|
1321
|
+
export const decodeProtocolMessageToJson = (_protocolMessage, _isInitiator) => {
|
|
1322
|
+
// TODO: Implement using
|
|
1323
|
+
// - decodeVersionAndOwner
|
|
1324
|
+
// -- decodeError or decodeWriteKeys (should be refactored out),
|
|
1325
|
+
// -- decodeMessages, and decodeRanges.
|
|
1326
|
+
// This is a stub for PRs and community contributions.
|
|
1327
|
+
throw new Error("decodeProtocolMessageToJson is not implemented yet.");
|
|
1328
|
+
};
|