@evolu/common 5.4.7 → 6.0.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 +30 -34
- package/dist/src/Array.d.ts +17 -0
- package/dist/src/Array.d.ts.map +1 -0
- package/dist/src/Array.js +12 -0
- package/dist/src/Assert.d.ts +68 -0
- package/dist/src/Assert.d.ts.map +1 -0
- package/dist/src/Assert.js +77 -0
- package/dist/src/BigInt.d.ts +20 -0
- package/dist/src/BigInt.d.ts.map +1 -0
- package/dist/src/BigInt.js +18 -0
- package/dist/src/Buffer.d.ts +92 -0
- package/dist/src/Buffer.d.ts.map +1 -0
- package/dist/src/Buffer.js +62 -0
- package/dist/src/Callbacks.d.ts +20 -0
- package/dist/src/Callbacks.d.ts.map +1 -0
- package/dist/src/Callbacks.js +18 -0
- package/dist/src/Console.d.ts +78 -0
- package/dist/src/Console.d.ts.map +1 -0
- package/dist/src/Console.js +103 -0
- package/dist/src/Crypto.d.ts +72 -39
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +89 -54
- package/dist/src/Eq.d.ts +97 -0
- package/dist/src/Eq.d.ts.map +1 -0
- package/dist/src/Eq.js +167 -0
- package/dist/src/Error.d.ts +14 -10
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +43 -11
- package/dist/src/Evolu/Config.d.ts +69 -0
- package/dist/src/Evolu/Config.d.ts.map +1 -0
- package/dist/src/Evolu/Config.js +9 -0
- package/dist/src/Evolu/Db.d.ts +126 -0
- package/dist/src/Evolu/Db.d.ts.map +1 -0
- package/dist/src/Evolu/Db.js +774 -0
- package/dist/src/Evolu/Diff.d.ts +43 -0
- package/dist/src/Evolu/Diff.d.ts.map +1 -0
- package/dist/src/Evolu/Diff.js +95 -0
- package/dist/src/Evolu/Evolu.d.ts +334 -0
- package/dist/src/Evolu/Evolu.d.ts.map +1 -0
- package/dist/src/Evolu/Evolu.js +434 -0
- package/dist/src/Evolu/Internal.d.ts +26 -0
- package/dist/src/Evolu/Internal.d.ts.map +1 -0
- package/dist/src/Evolu/Internal.js +25 -0
- package/dist/src/Evolu/Kysely.d.ts +6 -0
- package/dist/src/Evolu/Kysely.d.ts.map +1 -0
- package/dist/src/Evolu/Kysely.js +21 -0
- package/dist/src/Evolu/Owner.d.ts +155 -0
- package/dist/src/Evolu/Owner.d.ts.map +1 -0
- package/dist/src/Evolu/Owner.js +126 -0
- package/dist/src/Evolu/Platform.d.ts +23 -0
- package/dist/src/Evolu/Platform.d.ts.map +1 -0
- package/dist/src/Evolu/Platform.js +1 -0
- package/dist/src/Evolu/Protocol.d.ts +401 -0
- package/dist/src/Evolu/Protocol.d.ts.map +1 -0
- package/dist/src/Evolu/Protocol.js +1151 -0
- package/dist/src/Evolu/Public.d.ts +18 -0
- package/dist/src/Evolu/Public.d.ts.map +1 -0
- package/dist/src/Evolu/Public.js +11 -0
- package/dist/src/Evolu/PublicKysely.d.ts +148 -0
- package/dist/src/Evolu/PublicKysely.d.ts.map +1 -0
- package/dist/src/Evolu/PublicKysely.js +185 -0
- package/dist/src/Evolu/Query.d.ts +63 -0
- package/dist/src/Evolu/Query.d.ts.map +1 -0
- package/dist/src/Evolu/Query.js +61 -0
- package/dist/src/Evolu/Relay.d.ts +13 -0
- package/dist/src/Evolu/Relay.d.ts.map +1 -0
- package/dist/src/Evolu/Relay.js +109 -0
- package/dist/src/Evolu/Schema.d.ts +201 -0
- package/dist/src/Evolu/Schema.d.ts.map +1 -0
- package/dist/src/Evolu/Schema.js +150 -0
- package/dist/src/Evolu/Storage.d.ts +49 -0
- package/dist/src/Evolu/Storage.d.ts.map +1 -0
- package/dist/src/Evolu/Storage.js +1111 -0
- package/dist/src/Evolu/Sync.d.ts +59 -0
- package/dist/src/Evolu/Sync.d.ts.map +1 -0
- package/dist/src/Evolu/Sync.js +29 -0
- package/dist/src/Evolu/Timestamp.d.ts +106 -0
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -0
- package/dist/src/Evolu/Timestamp.js +179 -0
- package/dist/src/Function.d.ts +54 -0
- package/dist/src/Function.d.ts.map +1 -0
- package/dist/src/Function.js +38 -0
- package/dist/src/ManyToManyMap.d.ts +26 -0
- package/dist/src/ManyToManyMap.d.ts.map +1 -0
- package/dist/src/ManyToManyMap.js +92 -0
- package/dist/src/NanoId.d.ts +27 -0
- package/dist/src/NanoId.d.ts.map +1 -0
- package/dist/src/NanoId.js +6 -0
- package/dist/src/Number.d.ts +42 -0
- package/dist/src/Number.d.ts.map +1 -0
- package/dist/src/Number.js +55 -0
- package/dist/src/Object.d.ts +35 -0
- package/dist/src/Object.d.ts.map +1 -0
- package/dist/src/Object.js +36 -0
- package/dist/src/Order.d.ts +90 -0
- package/dist/src/Order.d.ts.map +1 -0
- package/dist/src/Order.js +85 -0
- package/dist/src/Promise.d.ts +180 -0
- package/dist/src/Promise.d.ts.map +1 -0
- package/dist/src/Promise.js +176 -0
- package/dist/src/Random.d.ts +52 -0
- package/dist/src/Random.d.ts.map +1 -0
- package/dist/src/Random.js +29 -0
- package/dist/src/Ref.d.ts +40 -0
- package/dist/src/Ref.d.ts.map +1 -0
- package/dist/src/Ref.js +13 -0
- package/dist/src/Result.d.ts +421 -0
- package/dist/src/Result.d.ts.map +1 -0
- package/dist/src/Result.js +357 -0
- package/dist/src/Skiplist.d.ts +23 -0
- package/dist/src/Skiplist.d.ts.map +1 -0
- package/dist/src/Skiplist.js +58 -0
- package/dist/src/Sqlite.d.ts +116 -52
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +183 -67
- package/dist/src/Store.d.ts +45 -8
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +33 -17
- package/dist/src/String.d.ts +2 -0
- package/dist/src/String.d.ts.map +1 -0
- package/dist/src/String.js +14 -0
- package/dist/src/Time.d.ts +20 -0
- package/dist/src/Time.d.ts.map +1 -0
- package/dist/src/Time.js +25 -0
- package/dist/src/Type.d.ts +1937 -0
- package/dist/src/Type.d.ts.map +1 -0
- package/dist/src/Type.js +2002 -0
- package/dist/src/Types.d.ts +188 -0
- package/dist/src/Types.d.ts.map +1 -0
- package/dist/src/Types.js +6 -0
- package/dist/src/WebSocket.d.ts +112 -0
- package/dist/src/WebSocket.d.ts.map +1 -0
- package/dist/src/WebSocket.js +139 -0
- package/dist/src/Worker.d.ts +44 -0
- package/dist/src/Worker.d.ts.map +1 -0
- package/dist/src/Worker.js +66 -0
- package/dist/src/index.d.ts +24 -11
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +24 -11
- package/package.json +29 -38
- package/src/Array.ts +39 -0
- package/src/Assert.ts +116 -0
- package/src/BigInt.ts +29 -0
- package/src/Buffer.ts +175 -0
- package/src/Callbacks.ts +43 -0
- package/src/Console.ts +159 -0
- package/src/Crypto.ts +169 -115
- package/src/Eq.ts +204 -0
- package/src/Error.ts +57 -20
- package/src/Evolu/Config.ts +83 -0
- package/src/Evolu/Db.ts +1275 -0
- package/src/Evolu/Diff.ts +142 -0
- package/src/Evolu/Evolu.ts +947 -0
- package/src/Evolu/Internal.ts +26 -0
- package/src/Evolu/Kysely.ts +38 -0
- package/src/Evolu/Owner.ts +296 -0
- package/src/Evolu/Platform.ts +27 -0
- package/src/Evolu/Protocol.ts +1857 -0
- package/src/Evolu/Public.ts +43 -0
- package/src/Evolu/PublicKysely.ts +240 -0
- package/src/Evolu/Query.ts +167 -0
- package/src/Evolu/Relay.ts +142 -0
- package/src/Evolu/Schema.ts +417 -0
- package/src/Evolu/Storage.ts +1281 -0
- package/src/Evolu/Sync.ts +105 -0
- package/src/Evolu/Timestamp.ts +311 -0
- package/src/Function.ts +58 -0
- package/src/ManyToManyMap.ts +140 -0
- package/src/NanoId.ts +39 -0
- package/src/Number.ts +90 -0
- package/src/Object.ts +64 -0
- package/src/Order.ts +113 -0
- package/src/Promise.ts +295 -0
- package/src/Random.ts +68 -0
- package/src/Ref.ts +63 -0
- package/src/Result.ts +453 -0
- package/src/Skiplist.ts +102 -0
- package/src/Sqlite.ts +366 -153
- package/src/Store.ts +79 -36
- package/src/String.ts +10 -0
- package/src/Time.ts +36 -0
- package/src/Type.ts +3978 -0
- package/src/Types.ts +209 -0
- package/src/WebSocket.ts +273 -0
- package/src/Worker.ts +129 -0
- package/src/index.ts +24 -11
- package/dist/src/Config.d.ts +0 -56
- package/dist/src/Config.d.ts.map +0 -1
- package/dist/src/Config.js +0 -39
- package/dist/src/Crdt.d.ts +0 -89
- package/dist/src/Crdt.d.ts.map +0 -1
- package/dist/src/Crdt.js +0 -181
- package/dist/src/Db.d.ts +0 -107
- package/dist/src/Db.d.ts.map +0 -1
- package/dist/src/Db.js +0 -443
- package/dist/src/Diff.d.ts +0 -27
- package/dist/src/Diff.d.ts.map +0 -1
- package/dist/src/Diff.js +0 -84
- package/dist/src/Evolu.d.ts +0 -426
- package/dist/src/Evolu.d.ts.map +0 -1
- package/dist/src/Evolu.js +0 -333
- package/dist/src/Model.d.ts +0 -141
- package/dist/src/Model.d.ts.map +0 -1
- package/dist/src/Model.js +0 -125
- package/dist/src/Murmurhash.d.ts +0 -2
- package/dist/src/Murmurhash.d.ts.map +0 -1
- package/dist/src/Murmurhash.js +0 -60
- package/dist/src/Owner.d.ts +0 -33
- package/dist/src/Owner.d.ts.map +0 -1
- package/dist/src/Owner.js +0 -26
- package/dist/src/Platform.d.ts +0 -37
- package/dist/src/Platform.d.ts.map +0 -1
- package/dist/src/Platform.js +0 -11
- package/dist/src/Protobuf.d.ts +0 -81
- package/dist/src/Protobuf.d.ts.map +0 -1
- package/dist/src/Protobuf.js +0 -92
- package/dist/src/Public.d.ts +0 -13
- package/dist/src/Public.d.ts.map +0 -1
- package/dist/src/Public.js +0 -6
- package/dist/src/Socket.d.ts +0 -8
- package/dist/src/Socket.d.ts.map +0 -1
- package/dist/src/Socket.js +0 -51
- package/dist/src/Sql.d.ts +0 -12
- package/dist/src/Sql.d.ts.map +0 -1
- package/dist/src/Sql.js +0 -30
- package/dist/src/Sync.d.ts +0 -70
- package/dist/src/Sync.d.ts.map +0 -1
- package/dist/src/Sync.js +0 -127
- package/src/Config.ts +0 -119
- package/src/Crdt.ts +0 -361
- package/src/Db.ts +0 -955
- package/src/Diff.ts +0 -114
- package/src/Evolu.ts +0 -1016
- package/src/Model.ts +0 -233
- package/src/Murmurhash.ts +0 -70
- package/src/Owner.ts +0 -69
- package/src/Platform.ts +0 -47
- package/src/Protobuf.ts +0 -155
- package/src/Public.ts +0 -12
- package/src/Socket.ts +0 -83
- package/src/Sql.ts +0 -41
- package/src/Sync.ts +0 -315
|
@@ -0,0 +1,1151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evolu Protocol
|
|
3
|
+
*
|
|
4
|
+
* Evolu Protocol is a local-first, end-to-end encrypted binary synchronization
|
|
5
|
+
* protocol optimized for minimal size and maximum speed. It enables data sync
|
|
6
|
+
* between a client and a relay, clients in a peer-to-peer (P2P) setup, or
|
|
7
|
+
* relays with each other.
|
|
8
|
+
*
|
|
9
|
+
* Evolu Protocol is designed for SQLite but can be extended to any database. It
|
|
10
|
+
* implements [Range-Based Set Reconciliation](https://arxiv.org/abs/2212.13567)
|
|
11
|
+
* by Aljoscha Meyer.
|
|
12
|
+
*
|
|
13
|
+
* To learn how RBSR works, check
|
|
14
|
+
* [Negentropy](https://logperiodic.com/rbsr.html). Evolu Protocol is similar to
|
|
15
|
+
* Negentropy but uses different encoding and also provides data transfer and
|
|
16
|
+
* ownership.
|
|
17
|
+
*
|
|
18
|
+
* ### Message Structure
|
|
19
|
+
*
|
|
20
|
+
* | Field | Notes |
|
|
21
|
+
* | :----------------------------- | :------------------------- |
|
|
22
|
+
* | **Header** | |
|
|
23
|
+
* | - {@link protocolVersion} | |
|
|
24
|
+
* | - {@link OwnerId} | |
|
|
25
|
+
* | - {@link ProtocolErrorCode} | In non-initiator response. |
|
|
26
|
+
* | **Messages** | |
|
|
27
|
+
* | - {@link NonNegativeInt} | A number of messages. |
|
|
28
|
+
* | - {@link EncryptedCrdtMessage} | |
|
|
29
|
+
* | - {@link WriteKey} | In initiator request. |
|
|
30
|
+
* | **Ranges** | |
|
|
31
|
+
* | - {@link NonNegativeInt} | Number of ranges. |
|
|
32
|
+
* | - {@link Range} | |
|
|
33
|
+
*
|
|
34
|
+
* Every protocol message belongs to an owner.
|
|
35
|
+
*
|
|
36
|
+
* ### Synchronization
|
|
37
|
+
*
|
|
38
|
+
* - **Messages**: Sends {@link EncryptedCrdtMessage}s in either direction.
|
|
39
|
+
* - **Ranges**: Determines messages to sync. Usage varies by transport—e.g., sent
|
|
40
|
+
* only on WebSocket connection open or with every fetch request.
|
|
41
|
+
*
|
|
42
|
+
* Synchronization involves an initiator and a non-initiator. The **initiator**
|
|
43
|
+
* is typically a client, and the **non-initiator** is typically a relay. Each
|
|
44
|
+
* side processes the received message and responds with a new `ProtocolMessage`
|
|
45
|
+
* if further sync is needed or possible, continuing until both sides are
|
|
46
|
+
* synchronized.
|
|
47
|
+
*
|
|
48
|
+
* Both **Messages** and **Ranges** are optional, allowing each side to send,
|
|
49
|
+
* sync, or only subscribe data as needed.
|
|
50
|
+
*
|
|
51
|
+
* When the initiator sends data, the {@link WriteKey} is **required** in
|
|
52
|
+
* **Messages** as a secure token proving the initiator can write changes. The
|
|
53
|
+
* non-initiator responds without a {@link WriteKey}, since the initiator’s
|
|
54
|
+
* request already signals it wants data. If the non-initiator detects an issue
|
|
55
|
+
* (e.g., an invalid {@link WriteKey} causing a {@link ProtocolWriteKeyError}, or
|
|
56
|
+
* a write failure causing a {@link ProtocolWriteError}), it sends an error code
|
|
57
|
+
* via the `Error` field in the header back to the initiator. In relay-to-relay
|
|
58
|
+
* or P2P sync, both sides may require the {@link WriteKey} depending on who is
|
|
59
|
+
* the initiator.
|
|
60
|
+
*
|
|
61
|
+
* ### Message Size Limit
|
|
62
|
+
*
|
|
63
|
+
* The protocol enforces a strict maximum size for all messages, defined by
|
|
64
|
+
* {@link maxProtocolMessageSize}. This ensures every `ProtocolMessage` is less
|
|
65
|
+
* than or equal to this limit, eliminating the need for applications to
|
|
66
|
+
* fragment and reconstruct messages during transmission.
|
|
67
|
+
*
|
|
68
|
+
* ### Why Binary?
|
|
69
|
+
*
|
|
70
|
+
* The protocol avoids JSON because:
|
|
71
|
+
*
|
|
72
|
+
* - Encrypted data doesn’t compress well, unlike plain JSON.
|
|
73
|
+
* - Message size must be controlled during creation.
|
|
74
|
+
* - Sequential byte reading is faster than parsing and can avoid conversions.
|
|
75
|
+
*
|
|
76
|
+
* It uses structure-aware encoding, significantly outperforming generic binary
|
|
77
|
+
* serialization formats with the following optimizations:
|
|
78
|
+
*
|
|
79
|
+
* - **NonNegativeInt:** Up to 33% smaller than MessagePack.
|
|
80
|
+
* - **Base64Url Strings:** Up to 25% size reduction.
|
|
81
|
+
* - **DateIso:** Up to 75% smaller.
|
|
82
|
+
* - **Timestamp Encoding:** Delta encoding for milliseconds and run-length
|
|
83
|
+
* encoding (RLE) for counters and NodeIds.
|
|
84
|
+
* - **Small Integers (0 to 19):** Reduces size by 1 byte per integer.
|
|
85
|
+
*
|
|
86
|
+
* To avoid reinventing serialization where it’s unnecessary—like for JSON and
|
|
87
|
+
* certain numbers—the Evolu Protocol relies on MessagePack.
|
|
88
|
+
*
|
|
89
|
+
* ### Versioning
|
|
90
|
+
*
|
|
91
|
+
* The initiator sends a versioned `ProtocolMessage`. If the non-initiator uses
|
|
92
|
+
* a different version, it responds with a message containing only its protocol
|
|
93
|
+
* version—**without an `ownerId`**. This allows the initiator to check protocol
|
|
94
|
+
* compatibility, for example, by sending version-only messages to multiple
|
|
95
|
+
* relays before starting synchronization.
|
|
96
|
+
*
|
|
97
|
+
* @module
|
|
98
|
+
*/
|
|
99
|
+
import { sha256 } from "@noble/hashes/sha2";
|
|
100
|
+
import { pack, unpack, unpackMultiple } from "msgpackr";
|
|
101
|
+
import { isNonEmptyReadonlyArray } from "../Array.js";
|
|
102
|
+
import { assert } from "../Assert.js";
|
|
103
|
+
import { BufferError, bytesToHex, bytesToUtf8, concatBytes, createBuffer, hexToBytes, utf8ToBytes, } from "../Buffer.js";
|
|
104
|
+
import { padmePaddingLength, } from "../Crypto.js";
|
|
105
|
+
import { eqArrayNumber } from "../Eq.js";
|
|
106
|
+
import { computeBalancedBuckets } from "../Number.js";
|
|
107
|
+
import { objectToEntries } from "../Object.js";
|
|
108
|
+
import { err, ok } from "../Result.js";
|
|
109
|
+
import { Base64Url, DateIsoString, idTypeValueLength, JsonValueFromString, maxLength, NonNegativeInt, Number, } from "../Type.js";
|
|
110
|
+
import { writeKeyLength, } from "./Owner.js";
|
|
111
|
+
import { binaryTimestampToTimestamp, Counter, Millis, timestampToBinaryTimestamp, } from "./Timestamp.js";
|
|
112
|
+
/** Maximum size of the entire protocol message in bytes. */
|
|
113
|
+
export const maxProtocolMessageSize = 1_000_000;
|
|
114
|
+
/** Maximum size of the ranges in bytes. */
|
|
115
|
+
export const maxProtocolMessageRangesSize = 30_000;
|
|
116
|
+
/** Evolu Protocol version. */
|
|
117
|
+
export const protocolVersion = 0;
|
|
118
|
+
export const ProtocolErrorCode = {
|
|
119
|
+
NoError: 0,
|
|
120
|
+
/** A code for {@link ProtocolWriteKeyError}. */
|
|
121
|
+
WriteKeyError: 1,
|
|
122
|
+
/** A code for {@link ProtocolWriteError}. */
|
|
123
|
+
WriteError: 2,
|
|
124
|
+
/** A code for {@link ProtocolSyncError}. */
|
|
125
|
+
SyncError: 3,
|
|
126
|
+
};
|
|
127
|
+
export const RangeType = {
|
|
128
|
+
Fingerprint: 1,
|
|
129
|
+
Skip: 0,
|
|
130
|
+
Timestamps: 2,
|
|
131
|
+
};
|
|
132
|
+
export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
|
|
133
|
+
export const fingerprintSize = 12;
|
|
134
|
+
/** A fingerprint of an empty range. */
|
|
135
|
+
export const zeroFingerprint = new Uint8Array(fingerprintSize);
|
|
136
|
+
/**
|
|
137
|
+
* Creates a {@link ProtocolMessage} from CRDT messages.
|
|
138
|
+
*
|
|
139
|
+
* If the message size would exceed {@link maxProtocolMessageSize}, the protocol
|
|
140
|
+
* ensures all messages will be sent in the next round(s) even over
|
|
141
|
+
* unidirectional and stateless transports.
|
|
142
|
+
*/
|
|
143
|
+
export const createProtocolMessageFromCrdtMessages = (deps) => (owner, messages, maxSize) => {
|
|
144
|
+
const buffer = createProtocolMessageBuffer(owner.id, {
|
|
145
|
+
totalMaxSize: maxSize ?? maxProtocolMessageSize,
|
|
146
|
+
writeKey: owner.writeKey,
|
|
147
|
+
});
|
|
148
|
+
let notAllMessagesSent = false;
|
|
149
|
+
for (const message of messages) {
|
|
150
|
+
const change = encodeAndEncryptDbChange(deps)(message.change, owner.encryptionKey);
|
|
151
|
+
const encryptedCrdtMessage = { timestamp: message.timestamp, change };
|
|
152
|
+
if (buffer.canAddMessage(encryptedCrdtMessage)) {
|
|
153
|
+
buffer.addMessage(encryptedCrdtMessage);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
notAllMessagesSent = true;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (notAllMessagesSent) {
|
|
161
|
+
/**
|
|
162
|
+
* DEV: If not all messages fit due to size limits, we trigger a sync
|
|
163
|
+
* continuation by appending a Range with a random fingerprint. This
|
|
164
|
+
* ensures the receiver always responds with ranges, prompting another
|
|
165
|
+
* sync round.
|
|
166
|
+
*
|
|
167
|
+
* The ideal approach would be to send three ranges (skip, fingerprint,
|
|
168
|
+
* skip) where the fingerprint of unsent messages would act as narrow sync
|
|
169
|
+
* probe. I think we can send {@link zeroFingerprint} which can be
|
|
170
|
+
* interpreted as an indication that the other side should reply with
|
|
171
|
+
* {@link TimestampsRange}, so no need to restart syncing.
|
|
172
|
+
*
|
|
173
|
+
* For now, using a random fingerprint avoids extra complexity and is good
|
|
174
|
+
* enough for this case.
|
|
175
|
+
*/
|
|
176
|
+
const randomFingerprint = deps.createRandomBytes(fingerprintSize);
|
|
177
|
+
// There is always a space for Fingerprint with InfiniteUpperBound.
|
|
178
|
+
buffer.addRange({
|
|
179
|
+
type: RangeType.Fingerprint,
|
|
180
|
+
upperBound: InfiniteUpperBound,
|
|
181
|
+
fingerprint: randomFingerprint,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return buffer.unwrap();
|
|
185
|
+
};
|
|
186
|
+
/** Creates a {@link ProtocolMessage} for sync. */
|
|
187
|
+
export const createProtocolMessageForSync = (deps) => (ownerId) => {
|
|
188
|
+
const buffer = createProtocolMessageBuffer(ownerId);
|
|
189
|
+
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
190
|
+
const size = deps.storage.getSize(binaryOwnerId);
|
|
191
|
+
// Errors are handled by the storage.
|
|
192
|
+
if (size == null)
|
|
193
|
+
return null;
|
|
194
|
+
splitRange(deps)(binaryOwnerId, 0, size, InfiniteUpperBound, buffer);
|
|
195
|
+
return buffer.unwrap();
|
|
196
|
+
};
|
|
197
|
+
export const createProtocolMessageBuffer = (ownerId, options = {}) => {
|
|
198
|
+
const { errorCode, writeKey, totalMaxSize = maxProtocolMessageSize, rangesMaxSize = maxProtocolMessageRangesSize, version = protocolVersion, } = options;
|
|
199
|
+
const buffers = {
|
|
200
|
+
header: createBuffer(),
|
|
201
|
+
messages: {
|
|
202
|
+
timestamps: createTimestampsBuffer(),
|
|
203
|
+
dbChanges: createBuffer(),
|
|
204
|
+
},
|
|
205
|
+
ranges: {
|
|
206
|
+
timestamps: createTimestampsBuffer(),
|
|
207
|
+
types: createBuffer(),
|
|
208
|
+
payloads: createBuffer(),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
encodeNonNegativeInt(buffers.header, version);
|
|
212
|
+
buffers.header.extend(ownerIdToBinaryOwnerId(ownerId));
|
|
213
|
+
if (errorCode != null)
|
|
214
|
+
buffers.header.extend([errorCode]);
|
|
215
|
+
let isLastRangeInfinite = false;
|
|
216
|
+
const isWithinSizeLimits = () => getSize() <= totalMaxSize;
|
|
217
|
+
const getSize = () => (getHeaderAndMessagesSize() + getRangesSize());
|
|
218
|
+
const getHeaderAndMessagesSize = () => buffers.header.getLength() +
|
|
219
|
+
buffers.messages.timestamps.getLength() +
|
|
220
|
+
buffers.messages.dbChanges.getLength() +
|
|
221
|
+
(buffers.messages.timestamps.getCount() > 0 && writeKey
|
|
222
|
+
? writeKeyLength
|
|
223
|
+
: 0);
|
|
224
|
+
const getRangesSize = () => buffers.ranges.timestamps.getCount() > 0
|
|
225
|
+
? buffers.ranges.timestamps.getLength() +
|
|
226
|
+
buffers.ranges.types.getLength() +
|
|
227
|
+
buffers.ranges.payloads.getLength() +
|
|
228
|
+
safeMargins.remainingRange
|
|
229
|
+
: 0;
|
|
230
|
+
/**
|
|
231
|
+
* We calculated worst-case sizes as closely as possible and added a small
|
|
232
|
+
* safety margin, since computing exact worst cases is difficult due to
|
|
233
|
+
* variable-length, run-length, and delta encoding.
|
|
234
|
+
*
|
|
235
|
+
* Runtime assertions (`assert`) are used to guarantee that size limits are
|
|
236
|
+
* never exceeded. If a limit is exceeded, the assertion will fail at the
|
|
237
|
+
* precise location, making it easy to identify and fix the issue.
|
|
238
|
+
*
|
|
239
|
+
* While it would be possible to avoid the safety margin by snapshotting
|
|
240
|
+
* buffer states and rolling back changes, this would likely impact
|
|
241
|
+
* performance. If someone has time and wants to experiment with this
|
|
242
|
+
* approach, contributions are welcome.
|
|
243
|
+
*/
|
|
244
|
+
const safeMargins = {
|
|
245
|
+
remainingRange: fingerprintSize + 10, // bytes: range type + possible increased count varint
|
|
246
|
+
timestamp: 30, // bytes: max millis + max count + NodeId
|
|
247
|
+
dbChangeLength: 8, // bytes: maximum encoded DbChange length varint
|
|
248
|
+
splitRange: 800, // bytes: worst case is around 650 bytes
|
|
249
|
+
timestampsRange: 50, // bytes: range type + its upperBound + possible increased count varint
|
|
250
|
+
};
|
|
251
|
+
const addMessageSafeMargin = safeMargins.timestamp +
|
|
252
|
+
safeMargins.dbChangeLength +
|
|
253
|
+
safeMargins.remainingRange;
|
|
254
|
+
return {
|
|
255
|
+
canAddMessage: (message) => getSize() + addMessageSafeMargin + message.change.length <= totalMaxSize,
|
|
256
|
+
addMessage: (message) => {
|
|
257
|
+
buffers.messages.timestamps.add(message.timestamp);
|
|
258
|
+
encodeLength(buffers.messages.dbChanges, message.change);
|
|
259
|
+
buffers.messages.dbChanges.extend(message.change);
|
|
260
|
+
assert(isWithinSizeLimits(), "the message is too big");
|
|
261
|
+
},
|
|
262
|
+
canSplitRange: () => {
|
|
263
|
+
return getRangesSize() + safeMargins.splitRange <= rangesMaxSize;
|
|
264
|
+
},
|
|
265
|
+
canAddTimestampsRangeAndMessage: (timestamps, message) => {
|
|
266
|
+
const rangesNewSize = getRangesSize() + timestamps.getLength() + safeMargins.timestampsRange;
|
|
267
|
+
return (rangesNewSize <= rangesMaxSize &&
|
|
268
|
+
(message
|
|
269
|
+
? getHeaderAndMessagesSize() +
|
|
270
|
+
rangesNewSize +
|
|
271
|
+
addMessageSafeMargin +
|
|
272
|
+
message.change.length <=
|
|
273
|
+
totalMaxSize
|
|
274
|
+
: true));
|
|
275
|
+
},
|
|
276
|
+
addRange: (range) => {
|
|
277
|
+
assert(!isLastRangeInfinite, "Cannot add a range after an InfiniteUpperBound range");
|
|
278
|
+
isLastRangeInfinite = range.upperBound === InfiniteUpperBound;
|
|
279
|
+
/**
|
|
280
|
+
* We don't have to encode InfiniteUpperBound timestamp since it's always
|
|
281
|
+
* the last because ranges cover the whole universe. For partial sync, we
|
|
282
|
+
* use SkipRange.
|
|
283
|
+
*/
|
|
284
|
+
if (range.upperBound !== InfiniteUpperBound)
|
|
285
|
+
buffers.ranges.timestamps.add(binaryTimestampToTimestamp(range.upperBound));
|
|
286
|
+
else {
|
|
287
|
+
buffers.ranges.timestamps.addInfinite();
|
|
288
|
+
}
|
|
289
|
+
encodeNonNegativeInt(buffers.ranges.types, range.type);
|
|
290
|
+
switch (range.type) {
|
|
291
|
+
case RangeType.Skip:
|
|
292
|
+
break;
|
|
293
|
+
case RangeType.Fingerprint:
|
|
294
|
+
buffers.ranges.payloads.extend(range.fingerprint);
|
|
295
|
+
break;
|
|
296
|
+
case RangeType.Timestamps: {
|
|
297
|
+
range.timestamps.append(buffers.ranges.payloads);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
assert(isWithinSizeLimits(), `the range ${range.type} is too big`);
|
|
302
|
+
},
|
|
303
|
+
unwrap: () => {
|
|
304
|
+
if (buffers.ranges.timestamps.getCount() > 0) {
|
|
305
|
+
assert(isLastRangeInfinite, "The last range's upperBound must be InfiniteUpperBound");
|
|
306
|
+
}
|
|
307
|
+
buffers.messages.timestamps.append(buffers.header);
|
|
308
|
+
buffers.header.extend(buffers.messages.dbChanges.unwrap());
|
|
309
|
+
if (buffers.messages.timestamps.getCount() > 0 && writeKey)
|
|
310
|
+
buffers.header.extend(writeKey);
|
|
311
|
+
if (buffers.ranges.timestamps.getCount() > 0) {
|
|
312
|
+
buffers.ranges.timestamps.append(buffers.header);
|
|
313
|
+
buffers.header.extend(buffers.ranges.types.unwrap());
|
|
314
|
+
buffers.header.extend(buffers.ranges.payloads.unwrap());
|
|
315
|
+
}
|
|
316
|
+
return buffers.header.unwrap();
|
|
317
|
+
},
|
|
318
|
+
getSize,
|
|
319
|
+
};
|
|
320
|
+
};
|
|
321
|
+
export const createTimestampsBuffer = () => {
|
|
322
|
+
let count = 0;
|
|
323
|
+
const countBuffer = createBuffer();
|
|
324
|
+
const syncCount = () => {
|
|
325
|
+
countBuffer.reset();
|
|
326
|
+
encodeNonNegativeInt(countBuffer, count);
|
|
327
|
+
};
|
|
328
|
+
syncCount();
|
|
329
|
+
const millisBuffer = createBuffer();
|
|
330
|
+
let previousMillis = 0;
|
|
331
|
+
const counterEncoder = createRunLengthEncoder((buffer, value) => {
|
|
332
|
+
encodeNonNegativeInt(buffer, value);
|
|
333
|
+
});
|
|
334
|
+
const nodeIdEncoder = createRunLengthEncoder((buffer, value) => {
|
|
335
|
+
encodeNodeId(buffer, value);
|
|
336
|
+
});
|
|
337
|
+
return {
|
|
338
|
+
add: (timestamp) => {
|
|
339
|
+
const delta = timestamp.millis - previousMillis;
|
|
340
|
+
assert(NonNegativeInt.is(delta), "The delta must be NonNegativeInt");
|
|
341
|
+
count++;
|
|
342
|
+
syncCount();
|
|
343
|
+
previousMillis = timestamp.millis;
|
|
344
|
+
encodeNonNegativeInt(millisBuffer, delta);
|
|
345
|
+
counterEncoder.add(timestamp.counter);
|
|
346
|
+
nodeIdEncoder.add(timestamp.nodeId);
|
|
347
|
+
},
|
|
348
|
+
addInfinite: () => {
|
|
349
|
+
count++;
|
|
350
|
+
syncCount();
|
|
351
|
+
},
|
|
352
|
+
getCount: () => count,
|
|
353
|
+
getLength: () => countBuffer.getLength() +
|
|
354
|
+
millisBuffer.getLength() +
|
|
355
|
+
counterEncoder.getLength() +
|
|
356
|
+
nodeIdEncoder.getLength(),
|
|
357
|
+
append: (buffer) => {
|
|
358
|
+
buffer.extend(countBuffer.unwrap());
|
|
359
|
+
buffer.extend(millisBuffer.unwrap());
|
|
360
|
+
buffer.extend(counterEncoder.unwrap());
|
|
361
|
+
buffer.extend(nodeIdEncoder.unwrap());
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
const createRunLengthEncoder = (encodeValue) => {
|
|
366
|
+
const buffer = createBuffer();
|
|
367
|
+
let previousLength = 0;
|
|
368
|
+
let previousValue = null;
|
|
369
|
+
let runLength = 0;
|
|
370
|
+
return {
|
|
371
|
+
add: (value) => {
|
|
372
|
+
if (value === previousValue) {
|
|
373
|
+
runLength++;
|
|
374
|
+
buffer.truncate(previousLength);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
previousValue = value;
|
|
378
|
+
runLength = 1;
|
|
379
|
+
}
|
|
380
|
+
previousLength = buffer.getLength();
|
|
381
|
+
encodeValue(buffer, value);
|
|
382
|
+
encodeNonNegativeInt(buffer, runLength);
|
|
383
|
+
},
|
|
384
|
+
getLength: () => buffer.getLength(),
|
|
385
|
+
unwrap: () => buffer.unwrap(),
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
export const applyProtocolMessageAsClient = (deps) => (inputMessage, { getWriteKey, version = protocolVersion, totalMaxSize, rangesMaxSize, } = {}) => tryDecodeProtocolData(inputMessage, (input) => {
|
|
389
|
+
const requestedVersion = decodeNonNegativeInt(input);
|
|
390
|
+
if (requestedVersion !== version) {
|
|
391
|
+
return err({
|
|
392
|
+
type: "ProtocolUnsupportedVersionError",
|
|
393
|
+
unsupportedVersion: requestedVersion,
|
|
394
|
+
isInitiator: version < requestedVersion,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
const ownerId = decodeOwnerId(input);
|
|
398
|
+
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
399
|
+
const errorCode = input.shift();
|
|
400
|
+
if (errorCode !== ProtocolErrorCode.NoError) {
|
|
401
|
+
switch (errorCode) {
|
|
402
|
+
case ProtocolErrorCode.WriteKeyError:
|
|
403
|
+
return err({
|
|
404
|
+
type: "ProtocolWriteKeyError",
|
|
405
|
+
});
|
|
406
|
+
case ProtocolErrorCode.WriteError:
|
|
407
|
+
return err({
|
|
408
|
+
type: "ProtocolWriteError",
|
|
409
|
+
});
|
|
410
|
+
case ProtocolErrorCode.SyncError:
|
|
411
|
+
return err({
|
|
412
|
+
type: "ProtocolSyncError",
|
|
413
|
+
});
|
|
414
|
+
default:
|
|
415
|
+
throw new ProtocolDecodeError(`Invalid ProtocolErrorCode: ${errorCode}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const messages = decodeMessages(input);
|
|
419
|
+
if (isNonEmptyReadonlyArray(messages) &&
|
|
420
|
+
!deps.storage.writeMessages(binaryOwnerId, messages)) {
|
|
421
|
+
return ok(null);
|
|
422
|
+
}
|
|
423
|
+
if (!getWriteKey)
|
|
424
|
+
return ok(null);
|
|
425
|
+
const writeKey = getWriteKey(ownerId);
|
|
426
|
+
if (writeKey == null)
|
|
427
|
+
return ok(null);
|
|
428
|
+
const output = createProtocolMessageBuffer(ownerId, {
|
|
429
|
+
writeKey,
|
|
430
|
+
totalMaxSize,
|
|
431
|
+
rangesMaxSize,
|
|
432
|
+
});
|
|
433
|
+
return sync(deps)("initiator", input, output, binaryOwnerId);
|
|
434
|
+
});
|
|
435
|
+
export const applyProtocolMessageAsRelay = (deps) => (inputMessage, { subscribe, broadcast, totalMaxSize, rangesMaxSize, } = {},
|
|
436
|
+
/** For testing purposes only; should not be used in production. */
|
|
437
|
+
version = protocolVersion) => tryDecodeProtocolData(inputMessage, (input) => {
|
|
438
|
+
const requestedVersion = decodeNonNegativeInt(input);
|
|
439
|
+
if (requestedVersion !== version) {
|
|
440
|
+
// Non-initiator responds with its version.
|
|
441
|
+
const output = createBuffer();
|
|
442
|
+
encodeNonNegativeInt(output, version);
|
|
443
|
+
return ok(output.unwrap());
|
|
444
|
+
}
|
|
445
|
+
const ownerId = decodeOwnerId(input);
|
|
446
|
+
const binaryOwnerId = ownerIdToBinaryOwnerId(ownerId);
|
|
447
|
+
subscribe?.(ownerId);
|
|
448
|
+
const messages = decodeMessages(input);
|
|
449
|
+
if (isNonEmptyReadonlyArray(messages)) {
|
|
450
|
+
const messagesEnd = inputMessage.length - input.getLength();
|
|
451
|
+
const writeKey = input.shiftN(writeKeyLength);
|
|
452
|
+
const writeKeyIsValid = deps.storage.validateWriteKey(binaryOwnerId, writeKey);
|
|
453
|
+
if (!writeKeyIsValid)
|
|
454
|
+
return ok(createProtocolMessageBuffer(ownerId, {
|
|
455
|
+
errorCode: ProtocolErrorCode.WriteKeyError,
|
|
456
|
+
}).unwrap());
|
|
457
|
+
if (broadcast) {
|
|
458
|
+
// Instead of encoding a new protocol message, we reuse the inputMessage.
|
|
459
|
+
const broadcastMessage = concatBytes(inputMessage.slice(0, 17), new Uint8Array([ProtocolErrorCode.NoError]), inputMessage.slice(17, messagesEnd));
|
|
460
|
+
broadcast(ownerId, broadcastMessage);
|
|
461
|
+
}
|
|
462
|
+
if (!deps.storage.writeMessages(binaryOwnerId, messages))
|
|
463
|
+
return ok(createProtocolMessageBuffer(ownerId, {
|
|
464
|
+
errorCode: ProtocolErrorCode.WriteError,
|
|
465
|
+
}).unwrap());
|
|
466
|
+
}
|
|
467
|
+
const output = createProtocolMessageBuffer(ownerId, {
|
|
468
|
+
errorCode: ProtocolErrorCode.NoError,
|
|
469
|
+
totalMaxSize,
|
|
470
|
+
rangesMaxSize,
|
|
471
|
+
});
|
|
472
|
+
return sync(deps)("non-initiator", input, output, binaryOwnerId);
|
|
473
|
+
});
|
|
474
|
+
/**
|
|
475
|
+
* Wraps Evolu Protocol decoding functions, which use exceptions instead of
|
|
476
|
+
* {@link Result} to provide stack traces for debugging and reduce allocation
|
|
477
|
+
* overhead in success cases.
|
|
478
|
+
*/
|
|
479
|
+
const tryDecodeProtocolData = (data, callback) => {
|
|
480
|
+
try {
|
|
481
|
+
return callback(createBuffer(data));
|
|
482
|
+
}
|
|
483
|
+
catch (error) {
|
|
484
|
+
if (error instanceof ProtocolDecodeError || error instanceof BufferError)
|
|
485
|
+
return err({
|
|
486
|
+
type: "ProtocolInvalidDataError",
|
|
487
|
+
data,
|
|
488
|
+
error,
|
|
489
|
+
});
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
/**
|
|
494
|
+
* Error thrown for internal protocol validation failures, such as invalid data
|
|
495
|
+
* or type errors.
|
|
496
|
+
*/
|
|
497
|
+
class ProtocolDecodeError extends Error {
|
|
498
|
+
constructor(message) {
|
|
499
|
+
super(message);
|
|
500
|
+
this.name = this.constructor.name;
|
|
501
|
+
Error.captureStackTrace(this, this.constructor);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const decodeMessages = (buffer) => {
|
|
505
|
+
const timestamps = decodeTimestamps(buffer);
|
|
506
|
+
const messages = [];
|
|
507
|
+
for (const timestamp of timestamps) {
|
|
508
|
+
const changeLength = decodeLength(buffer);
|
|
509
|
+
const change = buffer.shiftN(changeLength);
|
|
510
|
+
messages.push({ timestamp, change });
|
|
511
|
+
}
|
|
512
|
+
return messages;
|
|
513
|
+
};
|
|
514
|
+
const sync = (deps) => (role, input, output, ownerId) => {
|
|
515
|
+
const ranges = decodeRanges(input);
|
|
516
|
+
if (!isNonEmptyReadonlyArray(ranges)) {
|
|
517
|
+
// Nothing to sync.
|
|
518
|
+
return ok(null);
|
|
519
|
+
}
|
|
520
|
+
const binaryOwnerId = binaryOwnerIdToOwnerId(ownerId);
|
|
521
|
+
const outputInitialSize = output.getSize();
|
|
522
|
+
const syncFail = () => {
|
|
523
|
+
// Only the relay (non-initiator) reports sync errors, not the client (initiator).
|
|
524
|
+
if (role === "initiator") {
|
|
525
|
+
return ok(null);
|
|
526
|
+
}
|
|
527
|
+
const message = createProtocolMessageBuffer(binaryOwnerId, {
|
|
528
|
+
errorCode: ProtocolErrorCode.SyncError,
|
|
529
|
+
});
|
|
530
|
+
return ok(message.unwrap());
|
|
531
|
+
};
|
|
532
|
+
const storageSize = deps.storage.getSize(ownerId);
|
|
533
|
+
if (storageSize == null)
|
|
534
|
+
return syncFail();
|
|
535
|
+
let prevUpperBound = null;
|
|
536
|
+
let prevIndex = 0;
|
|
537
|
+
let skip = false;
|
|
538
|
+
let nonSkipRangeAdded = false;
|
|
539
|
+
const skipRange = (range) => {
|
|
540
|
+
// The last range, if any non skip was added, must have InfiniteUpperBound.
|
|
541
|
+
if (nonSkipRangeAdded && range.upperBound === InfiniteUpperBound) {
|
|
542
|
+
output.addRange({
|
|
543
|
+
type: RangeType.Skip,
|
|
544
|
+
upperBound: InfiniteUpperBound,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
skip = true;
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
const coalesceSkipsBeforeAdd = () => {
|
|
552
|
+
// Set to true because we are going to add a non skip range.
|
|
553
|
+
nonSkipRangeAdded = true;
|
|
554
|
+
if (skip) {
|
|
555
|
+
skip = false;
|
|
556
|
+
assert(prevUpperBound != null, "prevUpperBound is null");
|
|
557
|
+
// There is always a space for a skip range before adding.
|
|
558
|
+
output.addRange({
|
|
559
|
+
type: RangeType.Skip,
|
|
560
|
+
upperBound: prevUpperBound,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
// When we don't have a space...
|
|
565
|
+
const addFingerprintForRemainingRange = (begin) => {
|
|
566
|
+
const fingerprint = deps.storage.fingerprint(ownerId, begin, storageSize);
|
|
567
|
+
if (!fingerprint)
|
|
568
|
+
return false;
|
|
569
|
+
// There is always a space for a ramaining range.
|
|
570
|
+
output.addRange({
|
|
571
|
+
type: RangeType.Fingerprint,
|
|
572
|
+
upperBound: InfiniteUpperBound,
|
|
573
|
+
fingerprint,
|
|
574
|
+
});
|
|
575
|
+
return true;
|
|
576
|
+
};
|
|
577
|
+
for (const range of ranges) {
|
|
578
|
+
const currentUpperBound = range.upperBound;
|
|
579
|
+
const lower = prevIndex;
|
|
580
|
+
let upper = deps.storage.findLowerBound(ownerId, prevIndex, storageSize, currentUpperBound);
|
|
581
|
+
if (upper == null)
|
|
582
|
+
return syncFail();
|
|
583
|
+
switch (range.type) {
|
|
584
|
+
case RangeType.Skip: {
|
|
585
|
+
skipRange(range);
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
case RangeType.Fingerprint: {
|
|
589
|
+
const ourFingerprint = deps.storage.fingerprint(ownerId, lower, upper);
|
|
590
|
+
if (ourFingerprint == null)
|
|
591
|
+
return syncFail();
|
|
592
|
+
if (eqArrayNumber(range.fingerprint, ourFingerprint)) {
|
|
593
|
+
skipRange(range);
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
if (output.canSplitRange()) {
|
|
597
|
+
coalesceSkipsBeforeAdd();
|
|
598
|
+
splitRange(deps)(ownerId, lower, upper, currentUpperBound, output);
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
if (!addFingerprintForRemainingRange(upper))
|
|
602
|
+
return syncFail();
|
|
603
|
+
return ok(output.unwrap());
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
case RangeType.Timestamps: {
|
|
609
|
+
let endBound = currentUpperBound;
|
|
610
|
+
const timestampsWeNeed = new Map(range.timestamps.map((t) => [t.join(), true]));
|
|
611
|
+
const ourTimestamps = createTimestampsBuffer();
|
|
612
|
+
let storageError = false;
|
|
613
|
+
let exceeded = false;
|
|
614
|
+
deps.storage.iterate(ownerId, lower, upper, (timestamp, index) => {
|
|
615
|
+
const timestampString = timestamp.join();
|
|
616
|
+
const timestampBinary = binaryTimestampToTimestamp(timestamp);
|
|
617
|
+
let message = null;
|
|
618
|
+
if (timestampsWeNeed.has(timestampString)) {
|
|
619
|
+
timestampsWeNeed.delete(timestampString);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
const dbChange = deps.storage.readDbChange(ownerId, timestamp);
|
|
623
|
+
if (dbChange == null) {
|
|
624
|
+
storageError = true;
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
message = {
|
|
628
|
+
timestamp: timestampBinary,
|
|
629
|
+
change: dbChange,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
if (!output.canAddTimestampsRangeAndMessage(ourTimestamps, message)) {
|
|
633
|
+
exceeded = true;
|
|
634
|
+
endBound = timestamp;
|
|
635
|
+
upper = index;
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
ourTimestamps.add(timestampBinary);
|
|
639
|
+
if (message)
|
|
640
|
+
output.addMessage(message);
|
|
641
|
+
return true;
|
|
642
|
+
});
|
|
643
|
+
if (storageError) {
|
|
644
|
+
return syncFail();
|
|
645
|
+
}
|
|
646
|
+
const addRange = () => {
|
|
647
|
+
coalesceSkipsBeforeAdd();
|
|
648
|
+
output.addRange({
|
|
649
|
+
type: RangeType.Timestamps,
|
|
650
|
+
upperBound: endBound,
|
|
651
|
+
timestamps: ourTimestamps,
|
|
652
|
+
});
|
|
653
|
+
};
|
|
654
|
+
if (exceeded) {
|
|
655
|
+
addRange();
|
|
656
|
+
if (!addFingerprintForRemainingRange(upper)) {
|
|
657
|
+
return syncFail();
|
|
658
|
+
}
|
|
659
|
+
return ok(output.unwrap());
|
|
660
|
+
}
|
|
661
|
+
// If we need something, we have to respond with our timestamps.
|
|
662
|
+
if (timestampsWeNeed.size > 0) {
|
|
663
|
+
addRange();
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
skipRange(range);
|
|
667
|
+
}
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
prevIndex = upper;
|
|
672
|
+
prevUpperBound = currentUpperBound;
|
|
673
|
+
}
|
|
674
|
+
// If all ranges were skipped, there are no changes and sync is complete.
|
|
675
|
+
const hasChange = output.getSize() > outputInitialSize;
|
|
676
|
+
return ok(hasChange ? output.unwrap() : null);
|
|
677
|
+
};
|
|
678
|
+
const splitRange = (deps) => (ownerId, lower, upper, upperBound, buffer) => {
|
|
679
|
+
const itemCount = (upper - lower);
|
|
680
|
+
const buckets = computeBalancedBuckets(itemCount);
|
|
681
|
+
if (!buckets.ok) {
|
|
682
|
+
const range = {
|
|
683
|
+
type: RangeType.Timestamps,
|
|
684
|
+
upperBound,
|
|
685
|
+
timestamps: createTimestampsBuffer(),
|
|
686
|
+
};
|
|
687
|
+
deps.storage.iterate(ownerId, 0, itemCount, (timestamp) => {
|
|
688
|
+
range.timestamps.add(binaryTimestampToTimestamp(timestamp));
|
|
689
|
+
return true;
|
|
690
|
+
});
|
|
691
|
+
buffer.addRange(range);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
// Check Storage.ts `fingerprint` and `fingerprintRanges` docs.
|
|
695
|
+
const fingerprintRangesBuckets = lower === 0
|
|
696
|
+
? buckets.value
|
|
697
|
+
: [lower, ...buckets.value.map((b) => (b + lower))];
|
|
698
|
+
const fingerprintRanges = deps.storage.fingerprintRanges(ownerId, fingerprintRangesBuckets, upperBound);
|
|
699
|
+
// Errors are handled by the storage.
|
|
700
|
+
if (fingerprintRanges == null)
|
|
701
|
+
return;
|
|
702
|
+
const rangesToUse = lower > 0 ? fingerprintRanges.slice(1) : fingerprintRanges;
|
|
703
|
+
for (const range of rangesToUse) {
|
|
704
|
+
buffer.addRange(range);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
const decodeRanges = (buffer) => {
|
|
708
|
+
if (buffer.getLength() === 0)
|
|
709
|
+
return [];
|
|
710
|
+
const rangesCount = decodeNonNegativeInt(buffer);
|
|
711
|
+
if (rangesCount === 0)
|
|
712
|
+
return [];
|
|
713
|
+
const timestampsCount = (rangesCount - 1);
|
|
714
|
+
const timestamps = decodeTimestamps(buffer, timestampsCount);
|
|
715
|
+
const rangeTypes = [];
|
|
716
|
+
for (let i = 0; i < rangesCount; i++) {
|
|
717
|
+
const rangeType = decodeNonNegativeInt(buffer);
|
|
718
|
+
switch (rangeType) {
|
|
719
|
+
case RangeType.Fingerprint:
|
|
720
|
+
case RangeType.Skip:
|
|
721
|
+
case RangeType.Timestamps:
|
|
722
|
+
rangeTypes.push(rangeType);
|
|
723
|
+
break;
|
|
724
|
+
default:
|
|
725
|
+
throw new ProtocolDecodeError(`Invalid RangeType: ${rangeType}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const ranges = [];
|
|
729
|
+
for (let i = 0; i < rangesCount; i++) {
|
|
730
|
+
const upperBound = i < timestampsCount
|
|
731
|
+
? timestampToBinaryTimestamp(timestamps[i])
|
|
732
|
+
: InfiniteUpperBound;
|
|
733
|
+
const rangeType = rangeTypes[i];
|
|
734
|
+
switch (rangeType) {
|
|
735
|
+
case RangeType.Skip:
|
|
736
|
+
ranges.push({ type: RangeType.Skip, upperBound });
|
|
737
|
+
break;
|
|
738
|
+
case RangeType.Fingerprint: {
|
|
739
|
+
const fingerprint = buffer.shiftN(fingerprintSize);
|
|
740
|
+
ranges.push({
|
|
741
|
+
type: RangeType.Fingerprint,
|
|
742
|
+
upperBound,
|
|
743
|
+
fingerprint,
|
|
744
|
+
});
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case RangeType.Timestamps: {
|
|
748
|
+
const timestamps = decodeTimestamps(buffer).map(timestampToBinaryTimestamp);
|
|
749
|
+
ranges.push({
|
|
750
|
+
type: RangeType.Timestamps,
|
|
751
|
+
upperBound,
|
|
752
|
+
timestamps,
|
|
753
|
+
});
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return ranges;
|
|
759
|
+
};
|
|
760
|
+
/** Decodes an array of sorted timestamps with delta-encoded millis. */
|
|
761
|
+
const decodeTimestamps = (buffer, length) => {
|
|
762
|
+
length ??= decodeNonNegativeInt(buffer);
|
|
763
|
+
let previousMillis = 0;
|
|
764
|
+
const millises = [];
|
|
765
|
+
for (let i = 0; i < length; i++) {
|
|
766
|
+
const deltaMillis = decodeNonNegativeInt(buffer);
|
|
767
|
+
const millis = Millis.from(previousMillis + deltaMillis);
|
|
768
|
+
if (!millis.ok)
|
|
769
|
+
throw new Error(millis.error.type);
|
|
770
|
+
millises.push(millis.value);
|
|
771
|
+
previousMillis = millis.value;
|
|
772
|
+
}
|
|
773
|
+
const counters = [];
|
|
774
|
+
let counterIndex = 0;
|
|
775
|
+
while (counterIndex < length) {
|
|
776
|
+
const counter = Counter.from(decodeNonNegativeInt(buffer));
|
|
777
|
+
if (!counter.ok)
|
|
778
|
+
throw new Error(counter.error.type);
|
|
779
|
+
const runLength = decodeNonNegativeInt(buffer);
|
|
780
|
+
for (let i = 0; i < runLength; i++) {
|
|
781
|
+
counters.push(counter.value);
|
|
782
|
+
counterIndex++;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
const nodeIds = [];
|
|
786
|
+
let nodeIdIndex = 0;
|
|
787
|
+
while (nodeIdIndex < length) {
|
|
788
|
+
const nodeId = decodeNodeId(buffer);
|
|
789
|
+
const runLength = decodeNonNegativeInt(buffer);
|
|
790
|
+
for (let i = 0; i < runLength; i++) {
|
|
791
|
+
nodeIds.push(nodeId);
|
|
792
|
+
nodeIdIndex++;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const timestamps = [];
|
|
796
|
+
for (let i = 0; i < length; i++) {
|
|
797
|
+
timestamps.push({
|
|
798
|
+
millis: millises[i],
|
|
799
|
+
counter: counters[i],
|
|
800
|
+
nodeId: nodeIds[i],
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return timestamps;
|
|
804
|
+
};
|
|
805
|
+
export const binaryIdLength = 16;
|
|
806
|
+
export const idToBinaryId = (id) => base64Url256ToBytes(id);
|
|
807
|
+
export const binaryIdToId = (binaryId) => decodeId(createBuffer(binaryId));
|
|
808
|
+
export const ownerIdToBinaryOwnerId = (ownerId) => base64Url256ToBytes(ownerId);
|
|
809
|
+
export const binaryOwnerIdToOwnerId = (binaryOwnerId) => decodeOwnerId(createBuffer(binaryOwnerId));
|
|
810
|
+
/**
|
|
811
|
+
* Base64Url string with maximum length of 256 characters. Encoding strings as
|
|
812
|
+
* Base64UrlString saves up to 25% in size compared to regular strings.
|
|
813
|
+
*/
|
|
814
|
+
export const Base64Url256 = maxLength(256)(Base64Url);
|
|
815
|
+
/**
|
|
816
|
+
* Alphabet used for Base64Url encoding. This is copied from the `nanoid`
|
|
817
|
+
* library to avoid dependency on a specific version of `nanoid`.
|
|
818
|
+
*/
|
|
819
|
+
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
820
|
+
/**
|
|
821
|
+
* Converts a Base64Url string to a Uint8Array for binary storage. This encoding
|
|
822
|
+
* is more space-efficient than UTF-8 for Base64Url strings.
|
|
823
|
+
*/
|
|
824
|
+
export const base64Url256ToBytes = (string) => {
|
|
825
|
+
const totalBits = string.length * 6; // 6 bits per character
|
|
826
|
+
const byteLength = Math.ceil(totalBits / 8);
|
|
827
|
+
const value = new globalThis.Uint8Array(byteLength);
|
|
828
|
+
let bitBuffer = 0;
|
|
829
|
+
let bitsInBuffer = 0;
|
|
830
|
+
let byteIndex = 0;
|
|
831
|
+
for (const char of string) {
|
|
832
|
+
const charValue = urlAlphabet.indexOf(char);
|
|
833
|
+
bitBuffer = (bitBuffer << 6) | charValue;
|
|
834
|
+
bitsInBuffer += 6;
|
|
835
|
+
while (bitsInBuffer >= 8) {
|
|
836
|
+
bitsInBuffer -= 8;
|
|
837
|
+
value[byteIndex++] = (bitBuffer >> bitsInBuffer) & 0xff;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (bitsInBuffer > 0 && byteIndex < byteLength) {
|
|
841
|
+
value[byteIndex] = (bitBuffer << (8 - bitsInBuffer)) & 0xff;
|
|
842
|
+
}
|
|
843
|
+
return value;
|
|
844
|
+
};
|
|
845
|
+
export const decodeBase64Url256 = (buffer, stringLength) => {
|
|
846
|
+
const bytes = buffer.shiftN(Math.ceil((stringLength * 6) / 8));
|
|
847
|
+
let bitBuffer = 0;
|
|
848
|
+
let bitsInBuffer = 0;
|
|
849
|
+
let string = "";
|
|
850
|
+
for (const byte of bytes) {
|
|
851
|
+
bitBuffer = (bitBuffer << 8) | byte;
|
|
852
|
+
bitsInBuffer += 8;
|
|
853
|
+
while (bitsInBuffer >= 6) {
|
|
854
|
+
bitsInBuffer -= 6;
|
|
855
|
+
if (string.length < stringLength) {
|
|
856
|
+
const charValue = (bitBuffer >> bitsInBuffer) & 0x3f;
|
|
857
|
+
if (charValue < 0 || charValue >= urlAlphabet.length) {
|
|
858
|
+
throw new ProtocolDecodeError("invalid charValue");
|
|
859
|
+
}
|
|
860
|
+
string += urlAlphabet[charValue];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
const result = Base64Url256.from(string);
|
|
865
|
+
if (!result.ok)
|
|
866
|
+
throw new ProtocolDecodeError(result.error.type);
|
|
867
|
+
return result.value;
|
|
868
|
+
};
|
|
869
|
+
const decodeId = (buffer) => decodeBase64Url256(buffer, idTypeValueLength);
|
|
870
|
+
/** Not all 16 bytes are valid {@link OwnerId}. */
|
|
871
|
+
const decodeOwnerId = (buffer) => decodeId(buffer);
|
|
872
|
+
/**
|
|
873
|
+
* Evolu uses MessagePack to handle all number variants except for
|
|
874
|
+
* NonNegativeInt. For NonNegativeInt, Evolu provides more efficient encoding.
|
|
875
|
+
*/
|
|
876
|
+
export const encodeNumber = (buffer, number) => {
|
|
877
|
+
buffer.extend(pack(number));
|
|
878
|
+
};
|
|
879
|
+
export const decodeNumber = (buffer) => {
|
|
880
|
+
let number;
|
|
881
|
+
let end;
|
|
882
|
+
unpackMultiple(buffer.unwrap(), (n, _, e) => {
|
|
883
|
+
number = n;
|
|
884
|
+
end = e;
|
|
885
|
+
return false;
|
|
886
|
+
});
|
|
887
|
+
const endResult = NonNegativeInt.fromUnknown(end);
|
|
888
|
+
if (!endResult.ok)
|
|
889
|
+
throw new ProtocolDecodeError(endResult.error.type);
|
|
890
|
+
const numberResult = Number.fromUnknown(number);
|
|
891
|
+
if (!numberResult.ok)
|
|
892
|
+
throw new ProtocolDecodeError(numberResult.error.type);
|
|
893
|
+
buffer.shiftN(endResult.value);
|
|
894
|
+
return numberResult.value;
|
|
895
|
+
};
|
|
896
|
+
export const binaryTimestampToFingerprint = (timestamp) => {
|
|
897
|
+
const hash = sha256(timestamp).slice(0, fingerprintSize);
|
|
898
|
+
return hash;
|
|
899
|
+
};
|
|
900
|
+
/**
|
|
901
|
+
* Encodes and encrypts a {@link DbChange} using the provided owner's encryption
|
|
902
|
+
* key. Returns an encrypted binary representation as {@link EncryptedDbChange}.
|
|
903
|
+
*/
|
|
904
|
+
export const encodeAndEncryptDbChange = (deps) => (change, key) => {
|
|
905
|
+
const buffer = createBuffer();
|
|
906
|
+
encodeBase64Url256(buffer, change.table);
|
|
907
|
+
buffer.extend(idToBinaryId(change.id));
|
|
908
|
+
const entries = objectToEntries(change.values).map(([column, value]) => {
|
|
909
|
+
return [column, value];
|
|
910
|
+
});
|
|
911
|
+
encodeLength(buffer, entries);
|
|
912
|
+
for (const [column, value] of entries) {
|
|
913
|
+
encodeBase64Url256(buffer, column);
|
|
914
|
+
encodeSqliteValue(buffer, value);
|
|
915
|
+
}
|
|
916
|
+
const paddingLength = padmePaddingLength(buffer.getLength());
|
|
917
|
+
// Add zero bytes as PADMÉ padding - these will be ignored during decoding.
|
|
918
|
+
buffer.extend(new Uint8Array(paddingLength));
|
|
919
|
+
const { nonce, ciphertext } = deps.symmetricCrypto.encrypt(buffer.unwrap(), key);
|
|
920
|
+
buffer.reset();
|
|
921
|
+
buffer.extend(nonce);
|
|
922
|
+
encodeLength(buffer, ciphertext);
|
|
923
|
+
buffer.extend(ciphertext);
|
|
924
|
+
return buffer.unwrap();
|
|
925
|
+
};
|
|
926
|
+
/**
|
|
927
|
+
* Decrypts and decodes an {@link EncryptedDbChange} using the provided owner's
|
|
928
|
+
* encryption key.
|
|
929
|
+
*/
|
|
930
|
+
export const decryptAndDecodeDbChange = (deps) => (change, key) => tryDecodeProtocolData(change, (buffer) => {
|
|
931
|
+
const nonce = buffer.shiftN(deps.symmetricCrypto.nonceLength);
|
|
932
|
+
const ciphertextLength = decodeLength(buffer);
|
|
933
|
+
const ciphertext = buffer.shiftN(ciphertextLength);
|
|
934
|
+
const plaintextBytes = deps.symmetricCrypto.decrypt(ciphertext, key, nonce);
|
|
935
|
+
if (!plaintextBytes.ok)
|
|
936
|
+
return plaintextBytes;
|
|
937
|
+
buffer.reset();
|
|
938
|
+
buffer.extend(plaintextBytes.value);
|
|
939
|
+
const table = decodeBase64Url256WithLength(buffer);
|
|
940
|
+
const id = decodeId(buffer);
|
|
941
|
+
const length = decodeLength(buffer);
|
|
942
|
+
const values = Object.create(null);
|
|
943
|
+
for (let i = 0; i < length; i++) {
|
|
944
|
+
const column = decodeBase64Url256WithLength(buffer);
|
|
945
|
+
const value = decodeSqliteValue(buffer);
|
|
946
|
+
values[column] = value;
|
|
947
|
+
}
|
|
948
|
+
const dbChange = { table, id, values };
|
|
949
|
+
return ok(dbChange);
|
|
950
|
+
});
|
|
951
|
+
/**
|
|
952
|
+
* Encodes a non-negative integer into a variable-length integer format. It's
|
|
953
|
+
* more efficient than encoding via {@link encodeNumber}.
|
|
954
|
+
*
|
|
955
|
+
* https://en.wikipedia.org/wiki/Variable-length_quantity
|
|
956
|
+
*/
|
|
957
|
+
export const encodeNonNegativeInt = (buffer, int) => {
|
|
958
|
+
if (int === 0) {
|
|
959
|
+
buffer.extend([0]);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
let remaining = BigInt(int);
|
|
963
|
+
const bytes = [];
|
|
964
|
+
while (remaining !== 0n) {
|
|
965
|
+
const byte = globalThis.Number(remaining & 127n);
|
|
966
|
+
bytes.push(byte);
|
|
967
|
+
remaining >>= 7n;
|
|
968
|
+
}
|
|
969
|
+
for (let i = 0; i < bytes.length - 1; i++) {
|
|
970
|
+
bytes[i] |= 128;
|
|
971
|
+
}
|
|
972
|
+
buffer.extend(bytes);
|
|
973
|
+
};
|
|
974
|
+
/**
|
|
975
|
+
* Decodes a non-negative integer from a variable-length integer format.
|
|
976
|
+
*
|
|
977
|
+
* https://en.wikipedia.org/wiki/Variable-length_quantity
|
|
978
|
+
*/
|
|
979
|
+
export const decodeNonNegativeInt = (buffer) => {
|
|
980
|
+
let result = 0n;
|
|
981
|
+
let shift = 0n;
|
|
982
|
+
let byte;
|
|
983
|
+
// 8 is the smallest required count
|
|
984
|
+
for (let byteCount = 0; byteCount < 8; byteCount++) {
|
|
985
|
+
byte = buffer.shift();
|
|
986
|
+
result |= BigInt(byte & 127) << shift;
|
|
987
|
+
if ((byte & 128) === 0)
|
|
988
|
+
break;
|
|
989
|
+
shift += 7n;
|
|
990
|
+
}
|
|
991
|
+
const int = NonNegativeInt.from(globalThis.Number(result));
|
|
992
|
+
if (!int.ok)
|
|
993
|
+
throw new ProtocolDecodeError(int.error.type);
|
|
994
|
+
return int.value;
|
|
995
|
+
};
|
|
996
|
+
export const encodeLength = (buffer, value) => {
|
|
997
|
+
encodeNonNegativeInt(buffer, value.length);
|
|
998
|
+
};
|
|
999
|
+
export const decodeLength = decodeNonNegativeInt;
|
|
1000
|
+
export const encodeString = (buffer, value) => {
|
|
1001
|
+
const bytes = utf8ToBytes(value);
|
|
1002
|
+
encodeLength(buffer, bytes);
|
|
1003
|
+
buffer.extend(bytes);
|
|
1004
|
+
};
|
|
1005
|
+
export const decodeString = (buffer) => {
|
|
1006
|
+
const length = decodeLength(buffer);
|
|
1007
|
+
const bytes = buffer.shiftN(length);
|
|
1008
|
+
return bytesToUtf8(bytes);
|
|
1009
|
+
};
|
|
1010
|
+
export const encodeNodeId = (buffer, nodeId) => {
|
|
1011
|
+
buffer.extend(hexToBytes(nodeId));
|
|
1012
|
+
};
|
|
1013
|
+
export const decodeNodeId = (buffer) => {
|
|
1014
|
+
const bytes = buffer.shiftN(8);
|
|
1015
|
+
return bytesToHex(bytes);
|
|
1016
|
+
};
|
|
1017
|
+
export const encodeBase64Url256 = (buffer, string) => {
|
|
1018
|
+
encodeLength(buffer, string);
|
|
1019
|
+
buffer.extend(base64Url256ToBytes(string));
|
|
1020
|
+
};
|
|
1021
|
+
export const decodeBase64Url256WithLength = (buffer) => {
|
|
1022
|
+
const length = decodeLength(buffer);
|
|
1023
|
+
return decodeBase64Url256(buffer, length);
|
|
1024
|
+
};
|
|
1025
|
+
// Small ints are encoded into ProtocolValueType, saving one byte per int.
|
|
1026
|
+
const isSmallInt = (value) => value >= 0 && value < 20;
|
|
1027
|
+
export const ProtocolValueType = {
|
|
1028
|
+
// 0-19 small ints
|
|
1029
|
+
// SQLite types
|
|
1030
|
+
String: 20,
|
|
1031
|
+
Number: 21,
|
|
1032
|
+
Null: 22,
|
|
1033
|
+
Binary: 23,
|
|
1034
|
+
// We can add more types for other DBs or anything else later.
|
|
1035
|
+
// Optimized types
|
|
1036
|
+
Id: 30,
|
|
1037
|
+
Base64Url256: 31,
|
|
1038
|
+
NonNegativeInt: 32,
|
|
1039
|
+
Json: 33,
|
|
1040
|
+
// new Date().toISOString() - 24 bytes
|
|
1041
|
+
// encoded with fixed length - 8 bytes
|
|
1042
|
+
// encode as NonNegativeInt - 6 bytes (additional 25% reduction)
|
|
1043
|
+
DateIsoWithNonNegativeTime: 34,
|
|
1044
|
+
DateIsoWithNegativeTime: 35, // 9 bytes
|
|
1045
|
+
// TODO: Operations (from 40)
|
|
1046
|
+
// Increment, Decrement, Patch, whatever.
|
|
1047
|
+
};
|
|
1048
|
+
export const encodeSqliteValue = (buffer, value) => {
|
|
1049
|
+
if (value === null) {
|
|
1050
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Null);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
switch (typeof value) {
|
|
1054
|
+
case "string": {
|
|
1055
|
+
const dateIsoString = DateIsoString.from(value);
|
|
1056
|
+
if (dateIsoString.ok) {
|
|
1057
|
+
const time = new Date(dateIsoString.value).getTime();
|
|
1058
|
+
if (NonNegativeInt.is(time)) {
|
|
1059
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.DateIsoWithNonNegativeTime);
|
|
1060
|
+
encodeNonNegativeInt(buffer, time);
|
|
1061
|
+
}
|
|
1062
|
+
else {
|
|
1063
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.DateIsoWithNegativeTime);
|
|
1064
|
+
encodeNumber(buffer, time);
|
|
1065
|
+
}
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const base64Url256 = Base64Url256.from(value);
|
|
1069
|
+
if (base64Url256.ok) {
|
|
1070
|
+
if (base64Url256.value.length === idTypeValueLength) {
|
|
1071
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Id);
|
|
1072
|
+
buffer.extend(base64Url256ToBytes(base64Url256.value));
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Base64Url256);
|
|
1076
|
+
encodeBase64Url256(buffer, base64Url256.value);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const jsonValue = JsonValueFromString.fromParent(value);
|
|
1080
|
+
if (jsonValue.ok) {
|
|
1081
|
+
const jsonBytes = pack(jsonValue.value);
|
|
1082
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Json);
|
|
1083
|
+
encodeLength(buffer, jsonBytes);
|
|
1084
|
+
buffer.extend(jsonBytes);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.String);
|
|
1088
|
+
encodeString(buffer, value);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
case "number": {
|
|
1092
|
+
if (NonNegativeInt.is(value)) {
|
|
1093
|
+
if (isSmallInt(value)) {
|
|
1094
|
+
encodeNonNegativeInt(buffer, value);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.NonNegativeInt);
|
|
1098
|
+
encodeNonNegativeInt(buffer, value);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Number);
|
|
1102
|
+
encodeNumber(buffer, value);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Binary);
|
|
1107
|
+
encodeLength(buffer, value);
|
|
1108
|
+
buffer.extend(value);
|
|
1109
|
+
};
|
|
1110
|
+
export const decodeSqliteValue = (buffer) => {
|
|
1111
|
+
const type = decodeNonNegativeInt(buffer);
|
|
1112
|
+
if (isSmallInt(type)) {
|
|
1113
|
+
return type;
|
|
1114
|
+
}
|
|
1115
|
+
switch (type) {
|
|
1116
|
+
case ProtocolValueType.String:
|
|
1117
|
+
return decodeString(buffer);
|
|
1118
|
+
case ProtocolValueType.Number:
|
|
1119
|
+
return decodeNumber(buffer);
|
|
1120
|
+
case ProtocolValueType.Null:
|
|
1121
|
+
return null;
|
|
1122
|
+
case ProtocolValueType.Binary: {
|
|
1123
|
+
const length = decodeLength(buffer);
|
|
1124
|
+
return buffer.shiftN(length);
|
|
1125
|
+
}
|
|
1126
|
+
case ProtocolValueType.Id: {
|
|
1127
|
+
return decodeId(buffer);
|
|
1128
|
+
}
|
|
1129
|
+
case ProtocolValueType.Base64Url256:
|
|
1130
|
+
return decodeBase64Url256WithLength(buffer);
|
|
1131
|
+
case ProtocolValueType.NonNegativeInt:
|
|
1132
|
+
return decodeNonNegativeInt(buffer);
|
|
1133
|
+
case ProtocolValueType.Json: {
|
|
1134
|
+
const length = decodeLength(buffer);
|
|
1135
|
+
const bytes = buffer.shiftN(length);
|
|
1136
|
+
return JSON.stringify(unpack(bytes));
|
|
1137
|
+
}
|
|
1138
|
+
case ProtocolValueType.DateIsoWithNonNegativeTime:
|
|
1139
|
+
case ProtocolValueType.DateIsoWithNegativeTime: {
|
|
1140
|
+
const time = type === ProtocolValueType.DateIsoWithNonNegativeTime
|
|
1141
|
+
? decodeNonNegativeInt(buffer)
|
|
1142
|
+
: decodeNumber(buffer);
|
|
1143
|
+
const dateIsoString = DateIsoString.fromParent(new Date(time).toISOString());
|
|
1144
|
+
if (!dateIsoString.ok)
|
|
1145
|
+
throw new ProtocolDecodeError(dateIsoString.error.type);
|
|
1146
|
+
return dateIsoString.value;
|
|
1147
|
+
}
|
|
1148
|
+
default:
|
|
1149
|
+
throw new ProtocolDecodeError("invalid ProtocolValueType");
|
|
1150
|
+
}
|
|
1151
|
+
};
|