@evolu/common 8.17.0 → 8.18.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/dist/src/Bytes.d.ts +31 -14
- package/dist/src/Bytes.d.ts.map +1 -1
- package/dist/src/Bytes.js +48 -8
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +5 -1
- package/dist/src/Polyfills.d.ts.map +1 -1
- package/dist/src/Polyfills.js +3 -2
- package/dist/src/Task.d.ts +4 -3
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +4 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +18 -7
- package/dist/src/local-first/Evolu.d.ts +10 -1
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +5 -1
- package/dist/src/local-first/Protocol.d.ts +215 -93
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +799 -489
- package/dist/src/local-first/Relay.d.ts +9 -1
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +41 -36
- package/dist/src/local-first/Shared.d.ts +75 -49
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +62 -42
- package/dist/src/local-first/Storage.d.ts +80 -18
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +20 -4
- package/package.json +1 -1
- package/src/Bytes.test.ts +212 -0
- package/src/Bytes.ts +67 -17
- package/src/Error.ts +5 -1
- package/src/Polyfills.ts +3 -2
- package/src/Task.ts +4 -3
- package/src/local-first/Db.ts +29 -14
- package/src/local-first/Evolu.test.ts +43 -12
- package/src/local-first/Evolu.ts +16 -2
- package/src/local-first/Protocol.test.ts +1541 -44
- package/src/local-first/Protocol.ts +1070 -605
- package/src/local-first/Relay.ts +80 -69
- package/src/local-first/Shared.ts +106 -67
- package/src/local-first/Storage.ts +100 -27
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { NonEmptyReadonlyArray } from "../Array.ts";
|
|
7
7
|
import type { Brand } from "../Brand.ts";
|
|
8
|
+
import type { UnknownError } from "../Error.ts";
|
|
8
9
|
import type { RandomDep } from "../Random.ts";
|
|
9
10
|
import type { SqliteDep } from "../Sqlite.ts";
|
|
10
11
|
import type { Task } from "../Task.ts";
|
|
@@ -32,7 +33,10 @@ export interface StorageConfig {
|
|
|
32
33
|
* asynchronous (for calling remote APIs).
|
|
33
34
|
*
|
|
34
35
|
* The callback returns a boolean rather than an error because error handling
|
|
35
|
-
* and logging are the responsibility of the callback implementation.
|
|
36
|
+
* and logging are the responsibility of the callback implementation. It must
|
|
37
|
+
* not throw or reject, because a throw is a defect that shuts the relay down
|
|
38
|
+
* for a supervisor to restart it; a callback calling a remote service catches
|
|
39
|
+
* its failure and returns whether to allow the write.
|
|
36
40
|
*
|
|
37
41
|
* Relay deployments configure this callback. Client applications observe a
|
|
38
42
|
* denied relay write as a {@link ProtocolQuotaError} in the `failure` of that
|
|
@@ -81,10 +85,9 @@ export interface StorageConfig {
|
|
|
81
85
|
* that satisfies this contract.
|
|
82
86
|
*
|
|
83
87
|
* {@link Storage.writeMessages} returns the {@link StorageWriteMessagesError}
|
|
84
|
-
* that made it store none of a batch. Implementations return
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* to wire error codes.
|
|
88
|
+
* that made it store none of a batch. Implementations return it without
|
|
89
|
+
* reporting it; the caller owns reporting. The client protocol forwards these
|
|
90
|
+
* errors unchanged, while the relay protocol maps them to wire error codes.
|
|
88
91
|
*
|
|
89
92
|
* The Storage API is synchronous because SQLite's synchronous API is the
|
|
90
93
|
* fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
|
|
@@ -95,29 +98,55 @@ export interface StorageConfig {
|
|
|
95
98
|
* for async validation logic before writing to storage. The write operation
|
|
96
99
|
* itself remains synchronous.
|
|
97
100
|
*
|
|
101
|
+
* Indexes count an owner's timestamps in order from 0, and none may exceed the
|
|
102
|
+
* owner's size from {@link Storage.getSize}. Sync reads the size once per
|
|
103
|
+
* message and derives every index from it.
|
|
104
|
+
*
|
|
98
105
|
* @group Core
|
|
99
106
|
*/
|
|
100
107
|
export interface Storage {
|
|
108
|
+
/** Returns the number of the owner's timestamps. */
|
|
101
109
|
readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt;
|
|
110
|
+
/**
|
|
111
|
+
* Returns the {@link Fingerprint} of the owner's timestamps from index `begin`
|
|
112
|
+
* up to, but not including, `end`, where `begin` is at most `end`.
|
|
113
|
+
*/
|
|
102
114
|
readonly fingerprint: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt) => Fingerprint;
|
|
103
115
|
/**
|
|
104
116
|
* Computes fingerprints with their upper bounds in one call.
|
|
105
117
|
*
|
|
118
|
+
* Each bucket is the end index of a range that starts at the previous bucket,
|
|
119
|
+
* or at 0 for the first, so the buckets must be ascending.
|
|
120
|
+
*
|
|
106
121
|
* This function can be replaced with many fingerprint/findLowerBound calls,
|
|
107
122
|
* but implementations can leverage it for batching and more efficient
|
|
108
123
|
* fingerprint computation.
|
|
109
124
|
*/
|
|
110
125
|
readonly fingerprintRanges: (ownerId: OwnerIdBytes, buckets: ReadonlyArray<NonNegativeInt>, upperBound?: RangeUpperBound) => ReadonlyArray<FingerprintRange>;
|
|
126
|
+
/**
|
|
127
|
+
* Returns the index of the owner's first timestamp not below `upperBound`,
|
|
128
|
+
* counted from the owner's first timestamp, or `end` when there is none or
|
|
129
|
+
* `upperBound` is {@link InfiniteUpperBound}.
|
|
130
|
+
*
|
|
131
|
+
* `begin` must be 0 or the result for an earlier bound not above
|
|
132
|
+
* `upperBound`, and `end` the owner's size, so the result lies in [`begin`,
|
|
133
|
+
* `end`]. When `end` is 0 or equals `begin`, the result is `end`. Sync meets
|
|
134
|
+
* this because the protocol rejects range upper bounds that decrease.
|
|
135
|
+
*/
|
|
111
136
|
readonly findLowerBound: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt, upperBound: RangeUpperBound) => NonNegativeInt;
|
|
137
|
+
/**
|
|
138
|
+
* Calls `callback` with the owner's timestamps and their indexes from `begin`
|
|
139
|
+
* up to, but not including, `end`, where `begin` is at most `end`, until the
|
|
140
|
+
* callback returns `false`.
|
|
141
|
+
*/
|
|
112
142
|
readonly iterate: (ownerId: OwnerIdBytes, begin: NonNegativeInt, end: NonNegativeInt, callback: (timestamp: TimestampBytes, index: NonNegativeInt) => boolean) => void;
|
|
113
143
|
/**
|
|
114
144
|
* Validates the {@link OwnerWriteKey} for the given {@link Owner}.
|
|
115
145
|
*
|
|
116
|
-
* Returns `true` if the write key is valid, `false` otherwise.
|
|
146
|
+
* Returns `true` if the write key is valid, `false` otherwise. A relay logs a
|
|
147
|
+
* throw and answers the request with {@link ProtocolWriteError}.
|
|
117
148
|
*/
|
|
118
149
|
readonly validateWriteKey: (ownerId: OwnerIdBytes, writeKey: OwnerWriteKey) => boolean;
|
|
119
|
-
/** Sets the {@link OwnerWriteKey} for the given {@link Owner}. */
|
|
120
|
-
readonly setWriteKey: (ownerId: OwnerIdBytes, writeKey: OwnerWriteKey) => void;
|
|
121
150
|
/**
|
|
122
151
|
* Write encrypted {@link CrdtMessage}s to storage.
|
|
123
152
|
*
|
|
@@ -131,7 +160,12 @@ export interface Storage {
|
|
|
131
160
|
* protocol logic handling during sync operations.
|
|
132
161
|
*/
|
|
133
162
|
readonly writeMessages: (ownerIdBytes: OwnerIdBytes, messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>) => Task<void, StorageWriteMessagesError>;
|
|
134
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Read encrypted {@link DbChange}s from storage.
|
|
165
|
+
*
|
|
166
|
+
* The returned array must not be modified or reused later, because sync
|
|
167
|
+
* references it until it finishes its answer.
|
|
168
|
+
*/
|
|
135
169
|
readonly readDbChange: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => EncryptedDbChange;
|
|
136
170
|
/** Delete all data for the given {@link Owner}. */
|
|
137
171
|
readonly deleteOwner: (ownerId: OwnerIdBytes) => void;
|
|
@@ -152,7 +186,7 @@ export interface StorageDep {
|
|
|
152
186
|
export interface StorageQuotaError extends OwnerError, Typed<"StorageQuotaError"> {
|
|
153
187
|
}
|
|
154
188
|
/**
|
|
155
|
-
*
|
|
189
|
+
* Reasons why {@link Storage.writeMessages} stored none of a batch.
|
|
156
190
|
*
|
|
157
191
|
* The built-in relay storage stores opaque encrypted messages and rejects
|
|
158
192
|
* batches over quota. The built-in client storage decrypts and validates
|
|
@@ -162,15 +196,38 @@ export interface StorageQuotaError extends OwnerError, Typed<"StorageQuotaError"
|
|
|
162
196
|
* never a reason to reject a batch. The contract permits quota checks on either
|
|
163
197
|
* side.
|
|
164
198
|
*
|
|
199
|
+
* Both built-in storages return {@link UnknownError} when SQLite fails the
|
|
200
|
+
* write, for example on a full disk, after rolling it back.
|
|
201
|
+
*
|
|
165
202
|
* @group Core
|
|
166
203
|
*/
|
|
167
|
-
export type StorageWriteMessagesError = StorageQuotaError;
|
|
168
|
-
/**
|
|
169
|
-
* A
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
204
|
+
export type StorageWriteMessagesError = StorageQuotaError | UnknownError;
|
|
205
|
+
/**
|
|
206
|
+
* A summary of a range of {@link TimestampBytes} for comparing ranges cheaply.
|
|
207
|
+
*
|
|
208
|
+
* It is the XOR of the first {@link fingerprintSize} bytes of the SHA-256 hash
|
|
209
|
+
* of each timestamp in the range, or {@link zeroFingerprint} for an empty
|
|
210
|
+
* range.
|
|
211
|
+
*
|
|
212
|
+
* XOR is linear, so fingerprints are not collision resistant: any 97 timestamps
|
|
213
|
+
* contain a nonempty subset whose fingerprints XOR to zero. Anyone who can
|
|
214
|
+
* write to a relay for an owner, which includes every collaborator of a
|
|
215
|
+
* {@link SharedOwner}, can store timestamps whose fingerprints XOR to the
|
|
216
|
+
* fingerprint of a chosen change. Two ranges that differ only by that change
|
|
217
|
+
* and those timestamps then compare equal, so sync skips them, and the change
|
|
218
|
+
* does not move between that relay and a peer. This was demonstrated end to
|
|
219
|
+
* end, and nothing reports it. {@link Evolu.requestSync} does not heal it,
|
|
220
|
+
* because the next round compares the same fingerprints. In protocol version 1,
|
|
221
|
+
* the only recovery is syncing through a relay that does not hold those
|
|
222
|
+
* timestamps. Binding the count or hashing the result changes every
|
|
223
|
+
* fingerprint, so it needs a new protocol version.
|
|
224
|
+
*
|
|
225
|
+
* A write-key holder can also store a change under a timestamp another device
|
|
226
|
+
* has not used yet, such as a later one with that device's NodeId, because
|
|
227
|
+
* relays accept timestamps from any time. A relay keeps the first change stored
|
|
228
|
+
* for a timestamp, so it does not store the device's change when it arrives,
|
|
229
|
+
* and peers that sync through that relay later get the stored one instead. A
|
|
230
|
+
* new fingerprint does not close this.
|
|
174
231
|
*
|
|
175
232
|
* @group Ranges
|
|
176
233
|
*/
|
|
@@ -275,6 +332,9 @@ export interface EncryptedCrdtMessage {
|
|
|
275
332
|
/**
|
|
276
333
|
* Encrypted DbChange
|
|
277
334
|
*
|
|
335
|
+
* A 24-byte XChaCha20-Poly1305 nonce, the ciphertext length, and the ciphertext
|
|
336
|
+
* ending with its 16-byte Poly1305 tag, so at least 41 bytes.
|
|
337
|
+
*
|
|
278
338
|
* @group Messages
|
|
279
339
|
*/
|
|
280
340
|
export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
|
|
@@ -367,7 +427,7 @@ export interface DbChange extends InferType<typeof DbChange> {
|
|
|
367
427
|
*
|
|
368
428
|
* @group SQLite
|
|
369
429
|
*/
|
|
370
|
-
export interface BaseSqliteStorage extends Omit<Storage, "validateWriteKey" | "
|
|
430
|
+
export interface BaseSqliteStorage extends Omit<Storage, "validateWriteKey" | "writeMessages" | "readDbChange"> {
|
|
371
431
|
/**
|
|
372
432
|
* Inserts a timestamp for an owner into the skiplist-based storage.
|
|
373
433
|
*
|
|
@@ -448,6 +508,8 @@ export declare const testFingerprintTimestamps: (timestamps: ReadonlyArray<Times
|
|
|
448
508
|
/**
|
|
449
509
|
* Reads the timestamp at a position within an owner's ordered timestamps.
|
|
450
510
|
*
|
|
511
|
+
* Throws when `index` is not below the owner's size.
|
|
512
|
+
*
|
|
451
513
|
* @group SQLite
|
|
452
514
|
*/
|
|
453
515
|
export declare const getTimestampByIndex: (deps: SqliteDep) => (ownerId: OwnerIdBytes, index: NonNegativeInt) => TimestampBytes;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/local-first/Storage.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAGzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"Storage.d.ts","sourceRoot":"","sources":["../../../src/local-first/Storage.ts"],"names":[],"mappings":"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"}
|
|
@@ -119,6 +119,10 @@ export const createBaseSqliteStorage = (deps) => ({
|
|
|
119
119
|
// A batch can hold hundreds of thousands of timestamps, too many to spread
|
|
120
120
|
// into concatBytes.
|
|
121
121
|
const concatenatedTimestamps = concatByteArrays(timestampsBytes);
|
|
122
|
+
// A batch has no known size, so unlike the CTEs bounded by
|
|
123
|
+
// skiplistMaxLevel, the planner would make the owner's timestamps the outer
|
|
124
|
+
// loop. A cross join keeps the order as written, so each timestamp is
|
|
125
|
+
// looked up by primary key.
|
|
122
126
|
const result = deps.sqlite.exec(sql `
|
|
123
127
|
with recursive
|
|
124
128
|
split_timestamps(timestampBytes, pos) as (
|
|
@@ -135,7 +139,7 @@ export const createBaseSqliteStorage = (deps) => ({
|
|
|
135
139
|
select s.timestampBytes
|
|
136
140
|
from
|
|
137
141
|
split_timestamps s
|
|
138
|
-
join evolu_timestamp t
|
|
142
|
+
cross join evolu_timestamp t
|
|
139
143
|
on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
|
|
140
144
|
`);
|
|
141
145
|
return result.rows.map((row) => row.timestampBytes);
|
|
@@ -183,6 +187,9 @@ export const createBaseSqliteStorage = (deps) => ({
|
|
|
183
187
|
deleteOwner: (ownerId) => {
|
|
184
188
|
deps.sqlite.exec(sql `
|
|
185
189
|
delete from evolu_timestamp where ownerId = ${ownerId};
|
|
190
|
+
`);
|
|
191
|
+
deps.sqlite.exec(sql `
|
|
192
|
+
delete from evolu_usage where ownerId = ${ownerId};
|
|
186
193
|
`);
|
|
187
194
|
},
|
|
188
195
|
});
|
|
@@ -956,6 +963,8 @@ const fingerprint = (deps) => (ownerId, begin, end) => {
|
|
|
956
963
|
*/
|
|
957
964
|
const fingerprintRanges = (deps) => (ownerId, buckets, upperBound = InfiniteUpperBound) => {
|
|
958
965
|
const bucketsJson = JSON.stringify(buckets);
|
|
966
|
+
// A bucket past the owner's timestamps would descend below level 1
|
|
967
|
+
// forever, so the walk stops there and the bucket has no row.
|
|
959
968
|
const result = deps.sqlite.exec(sql.prepared `
|
|
960
969
|
with
|
|
961
970
|
ml(ml) as (
|
|
@@ -1021,7 +1030,7 @@ const fingerprintRanges = (deps) => (ownerId, buckets, upperBound = InfiniteUppe
|
|
|
1021
1030
|
c1
|
|
1022
1031
|
left join evolu_timestamp as node
|
|
1023
1032
|
on not c1.b and node.ownerId = ${ownerId} and node.t = c1.nt
|
|
1024
|
-
where iif(c1.b, 1, c1.ic != c1.c)
|
|
1033
|
+
where iif(c1.b, 1, c1.ic != c1.c) and c1.dl >= 1
|
|
1025
1034
|
),
|
|
1026
1035
|
c2(h1, h2, t, rn) as (
|
|
1027
1036
|
select
|
|
@@ -1053,6 +1062,7 @@ const fingerprintRanges = (deps) => (ownerId, buckets, upperBound = InfiniteUppe
|
|
|
1053
1062
|
select b, cast(h1 as text) as h1, cast(h2 as text) as h2
|
|
1054
1063
|
from c3;
|
|
1055
1064
|
`);
|
|
1065
|
+
assert(result.rows.length === buckets.length, "bucket out of range");
|
|
1056
1066
|
const fingerprintRanges = result.rows.map((row, i, arr) => ({
|
|
1057
1067
|
type: RangeType.Fingerprint,
|
|
1058
1068
|
upperBound: i === arr.length - 1 ? upperBound : row.b,
|
|
@@ -1068,9 +1078,13 @@ const x = (a, b) => sql.raw(`(${a} | ${b}) - (${a} & ${b})`);
|
|
|
1068
1078
|
/**
|
|
1069
1079
|
* Reads the timestamp at a position within an owner's ordered timestamps.
|
|
1070
1080
|
*
|
|
1081
|
+
* Throws when `index` is not below the owner's size.
|
|
1082
|
+
*
|
|
1071
1083
|
* @group SQLite
|
|
1072
1084
|
*/
|
|
1073
1085
|
export const getTimestampByIndex = (deps) => (ownerId, index) => {
|
|
1086
|
+
// An index past the owner's timestamps would descend below level 1
|
|
1087
|
+
// forever, so the walk stops there and finds no row.
|
|
1074
1088
|
const result = deps.sqlite.exec(sql.prepared `
|
|
1075
1089
|
with
|
|
1076
1090
|
fi(b, cl, ic, pt, mt, nt, nc) as (
|
|
@@ -1136,13 +1150,15 @@ export const getTimestampByIndex = (deps) => (ownerId, index) => {
|
|
|
1136
1150
|
)
|
|
1137
1151
|
)
|
|
1138
1152
|
from fi
|
|
1139
|
-
where ic != ${index + 1}
|
|
1153
|
+
where ic != ${index + 1} and cl >= 1
|
|
1140
1154
|
)
|
|
1141
1155
|
select pt
|
|
1142
1156
|
from fi
|
|
1143
1157
|
where ic == ${index + 1};
|
|
1144
1158
|
`);
|
|
1145
|
-
|
|
1159
|
+
const row = result.rows.at(0);
|
|
1160
|
+
assert(row, "index out of range");
|
|
1161
|
+
return row.pt;
|
|
1146
1162
|
};
|
|
1147
1163
|
/**
|
|
1148
1164
|
* Reads owner usage from SQLite and returns default bounds when absent.
|
package/package.json
CHANGED
package/src/Bytes.test.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import * as fc from "fast-check";
|
|
1
2
|
import { describe, it, mock } from "node:test";
|
|
3
|
+
import { setFlagsFromString } from "node:v8";
|
|
4
|
+
import { runInNewContext } from "node:vm";
|
|
2
5
|
import {
|
|
6
|
+
assert,
|
|
3
7
|
assertEqual,
|
|
4
8
|
assertEqualBytes,
|
|
5
9
|
assertErr,
|
|
6
10
|
assertFalse,
|
|
7
11
|
assertInstanceOf,
|
|
12
|
+
assertNonNullable,
|
|
8
13
|
assertOk,
|
|
9
14
|
assertSame,
|
|
10
15
|
assertThrowsInstanceOf,
|
|
@@ -95,6 +100,23 @@ describe("BufferError", () => {
|
|
|
95
100
|
assertEqual(error.name, "BufferError");
|
|
96
101
|
assertEqual(error.message, "test error");
|
|
97
102
|
});
|
|
103
|
+
|
|
104
|
+
it("constructs without Error.captureStackTrace", () => {
|
|
105
|
+
const descriptor = globalThis.Object.getOwnPropertyDescriptor(
|
|
106
|
+
Error,
|
|
107
|
+
"captureStackTrace",
|
|
108
|
+
);
|
|
109
|
+
assertNonNullable(descriptor);
|
|
110
|
+
Reflect.deleteProperty(Error, "captureStackTrace");
|
|
111
|
+
try {
|
|
112
|
+
const error = new BufferError("x");
|
|
113
|
+
assertInstanceOf(error, BufferError);
|
|
114
|
+
assertEqual(error.message, "x");
|
|
115
|
+
assertThrowsInstanceOf(() => decodeJson([0xc1]), BufferError);
|
|
116
|
+
} finally {
|
|
117
|
+
globalThis.Object.defineProperty(Error, "captureStackTrace", descriptor);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
98
120
|
});
|
|
99
121
|
|
|
100
122
|
describe("concatByteArrays", () => {
|
|
@@ -279,6 +301,16 @@ describe("Buffer", () => {
|
|
|
279
301
|
assertEqual(buffer.getLength(), 2);
|
|
280
302
|
assertEqualBytes(buffer.unwrap(), [6, 7]);
|
|
281
303
|
});
|
|
304
|
+
|
|
305
|
+
it("has readonly members", () => {
|
|
306
|
+
const buffer = createBuffer();
|
|
307
|
+
const { shift } = buffer;
|
|
308
|
+
|
|
309
|
+
// @ts-expect-error Cannot assign to 'shift' because it is a read-only property.
|
|
310
|
+
buffer.shift = shift;
|
|
311
|
+
|
|
312
|
+
assertSame(buffer.shift, shift);
|
|
313
|
+
});
|
|
282
314
|
});
|
|
283
315
|
|
|
284
316
|
describe("number encoding", () => {
|
|
@@ -482,6 +514,31 @@ describe("string encoding", () => {
|
|
|
482
514
|
);
|
|
483
515
|
assertEqual(decodeString(buffer), value);
|
|
484
516
|
});
|
|
517
|
+
|
|
518
|
+
// Unlike the JSON codec, the string codec keeps the wire valid UTF-8, because
|
|
519
|
+
// deployed decoders read WTF-8 as three U+FFFD per lone surrogate.
|
|
520
|
+
it("replaces a lone surrogate with U+FFFD", () => {
|
|
521
|
+
const buffer = createBuffer();
|
|
522
|
+
encodeString(buffer, "a\uD800b");
|
|
523
|
+
|
|
524
|
+
assertEqualBytes(buffer.unwrap(), [5, 97, 0xef, 0xbf, 0xbd, 98]);
|
|
525
|
+
assertSame(decodeString(buffer), "a�b");
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
it("decodes invalid UTF-8 as U+FFFD", () => {
|
|
529
|
+
assertSame(
|
|
530
|
+
decodeString(createBuffer([5, 97, 0xed, 0xa0, 0x80, 98])),
|
|
531
|
+
"a���b",
|
|
532
|
+
);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it("keeps a byte order mark", () => {
|
|
536
|
+
for (const value of ["\uFEFFabc", "\uFEFF", "a\uFEFFb"]) {
|
|
537
|
+
const buffer = createBuffer();
|
|
538
|
+
encodeString(buffer, value);
|
|
539
|
+
assertSame(decodeString(buffer), value);
|
|
540
|
+
}
|
|
541
|
+
});
|
|
485
542
|
});
|
|
486
543
|
|
|
487
544
|
describe("run-length encoding", () => {
|
|
@@ -574,6 +631,140 @@ describe("run-length encoding", () => {
|
|
|
574
631
|
});
|
|
575
632
|
});
|
|
576
633
|
|
|
634
|
+
describe("run-length encoder checkpoint", () => {
|
|
635
|
+
const setupEncoder = (values: ReadonlyArray<number> = []) => {
|
|
636
|
+
const encoder =
|
|
637
|
+
createRunLengthEncoder<NonNegativeInt>(encodeNonNegativeInt);
|
|
638
|
+
for (const value of values) encoder.add(NonNegativeInt.orThrow(value));
|
|
639
|
+
return encoder;
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const decodeEncoder = (
|
|
643
|
+
encoder: ReturnType<typeof setupEncoder>,
|
|
644
|
+
length: number,
|
|
645
|
+
) => {
|
|
646
|
+
const buffer = createBuffer(encoder.unwrap());
|
|
647
|
+
const values = decodeRle(buffer, NonNegativeInt.orThrow(length), () =>
|
|
648
|
+
decodeNonNegativeInt(buffer),
|
|
649
|
+
);
|
|
650
|
+
assertSame(buffer.getLength(), 0);
|
|
651
|
+
return values;
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
it("restores a run rewritten in place", () => {
|
|
655
|
+
const encoder = setupEncoder([7, 5]);
|
|
656
|
+
const restore = encoder.checkpoint();
|
|
657
|
+
encoder.add(NonNegativeInt.orThrow(5));
|
|
658
|
+
encoder.add(NonNegativeInt.orThrow(5));
|
|
659
|
+
assertEqualBytes(encoder.unwrap(), [7, 1, 5, 3]);
|
|
660
|
+
|
|
661
|
+
restore();
|
|
662
|
+
assertEqualBytes(encoder.unwrap(), [7, 1, 5, 1]);
|
|
663
|
+
|
|
664
|
+
encoder.add(NonNegativeInt.orThrow(5));
|
|
665
|
+
assertEqualBytes(encoder.unwrap(), setupEncoder([7, 5, 5]).unwrap());
|
|
666
|
+
assertEqual(decodeEncoder(encoder, 3), [7, 5, 5]);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it("restores a run length across the 127/128 varint boundary", () => {
|
|
670
|
+
const run127 = Array.from({ length: 127 }, () => 5);
|
|
671
|
+
const encoder = setupEncoder(run127);
|
|
672
|
+
const restoreAt127 = encoder.checkpoint();
|
|
673
|
+
encoder.add(NonNegativeInt.orThrow(5));
|
|
674
|
+
assertEqualBytes(encoder.unwrap(), [5, 128, 1]);
|
|
675
|
+
const restoreAt128 = encoder.checkpoint();
|
|
676
|
+
encoder.add(NonNegativeInt.orThrow(5));
|
|
677
|
+
assertEqualBytes(encoder.unwrap(), [5, 129, 1]);
|
|
678
|
+
|
|
679
|
+
restoreAt128();
|
|
680
|
+
assertEqualBytes(encoder.unwrap(), [5, 128, 1]);
|
|
681
|
+
|
|
682
|
+
restoreAt127();
|
|
683
|
+
assertEqualBytes(encoder.unwrap(), [5, 127]);
|
|
684
|
+
assertEqual(encoder.getLength(), 2);
|
|
685
|
+
|
|
686
|
+
encoder.add(NonNegativeInt.orThrow(6));
|
|
687
|
+
assertEqualBytes(encoder.unwrap(), [5, 127, 6, 1]);
|
|
688
|
+
assertEqual(decodeEncoder(encoder, 128), [...run127, 6]);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
it("restores the previous value after a different value", () => {
|
|
692
|
+
const encoder = setupEncoder([5]);
|
|
693
|
+
const restore = encoder.checkpoint();
|
|
694
|
+
encoder.add(NonNegativeInt.orThrow(7));
|
|
695
|
+
encoder.add(NonNegativeInt.orThrow(7));
|
|
696
|
+
assertEqualBytes(encoder.unwrap(), [5, 1, 7, 2]);
|
|
697
|
+
|
|
698
|
+
restore();
|
|
699
|
+
assertEqualBytes(encoder.unwrap(), [5, 1]);
|
|
700
|
+
|
|
701
|
+
// Without the previous value, the next 5 would start a new run.
|
|
702
|
+
encoder.add(NonNegativeInt.orThrow(5));
|
|
703
|
+
assertEqualBytes(encoder.unwrap(), [5, 2]);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it("restores an empty encoder", () => {
|
|
707
|
+
const encoder = setupEncoder();
|
|
708
|
+
const restore = encoder.checkpoint();
|
|
709
|
+
encoder.add(NonNegativeInt.orThrow(5));
|
|
710
|
+
|
|
711
|
+
restore();
|
|
712
|
+
assertEqual(encoder.getLength(), 0);
|
|
713
|
+
assertEqualBytes(encoder.unwrap(), []);
|
|
714
|
+
|
|
715
|
+
encoder.add(NonNegativeInt.orThrow(5));
|
|
716
|
+
assertEqualBytes(encoder.unwrap(), [5, 1]);
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
it("encodes only the committed values after restores", () => {
|
|
720
|
+
fc.assert(
|
|
721
|
+
fc.property(
|
|
722
|
+
fc.array(
|
|
723
|
+
fc.oneof(
|
|
724
|
+
fc.record({
|
|
725
|
+
value: fc.constantFrom(0, 5, 300),
|
|
726
|
+
count: fc.integer({ min: 1, max: 200 }),
|
|
727
|
+
}),
|
|
728
|
+
fc.constant("checkpoint" as const),
|
|
729
|
+
fc.constant("restore" as const),
|
|
730
|
+
),
|
|
731
|
+
{ maxLength: 30 },
|
|
732
|
+
),
|
|
733
|
+
(operations) => {
|
|
734
|
+
const encoder = setupEncoder();
|
|
735
|
+
const committed: Array<number> = [];
|
|
736
|
+
const checkpoints: Array<{
|
|
737
|
+
readonly restore: () => void;
|
|
738
|
+
readonly length: number;
|
|
739
|
+
}> = [];
|
|
740
|
+
|
|
741
|
+
for (const operation of operations) {
|
|
742
|
+
if (operation === "checkpoint") {
|
|
743
|
+
checkpoints.push({
|
|
744
|
+
restore: encoder.checkpoint(),
|
|
745
|
+
length: committed.length,
|
|
746
|
+
});
|
|
747
|
+
} else if (operation === "restore") {
|
|
748
|
+
const checkpoint = checkpoints.pop();
|
|
749
|
+
if (!checkpoint) continue;
|
|
750
|
+
checkpoint.restore();
|
|
751
|
+
committed.length = checkpoint.length;
|
|
752
|
+
} else {
|
|
753
|
+
for (let i = 0; i < operation.count; i++) {
|
|
754
|
+
encoder.add(NonNegativeInt.orThrow(operation.value));
|
|
755
|
+
committed.push(operation.value);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
assertEqualBytes(encoder.unwrap(), setupEncoder(committed).unwrap());
|
|
761
|
+
assertEqual(decodeEncoder(encoder, committed.length), committed);
|
|
762
|
+
},
|
|
763
|
+
),
|
|
764
|
+
);
|
|
765
|
+
});
|
|
766
|
+
});
|
|
767
|
+
|
|
577
768
|
describe("JSON binary codec", () => {
|
|
578
769
|
it("matches fixed msgpackr 2.0.5 fixtures", () => {
|
|
579
770
|
const fixtures: ReadonlyArray<readonly [unknown, ReadonlyArray<number>]> = [
|
|
@@ -1164,6 +1355,27 @@ describe("JSON binary codec", () => {
|
|
|
1164
1355
|
}
|
|
1165
1356
|
});
|
|
1166
1357
|
|
|
1358
|
+
it("releases an oversized encoder arena", () => {
|
|
1359
|
+
setFlagsFromString("--expose-gc");
|
|
1360
|
+
const gc = runInNewContext("gc") as () => void;
|
|
1361
|
+
gc();
|
|
1362
|
+
gc();
|
|
1363
|
+
const before = process.memoryUsage().arrayBuffers;
|
|
1364
|
+
const buffer = createBuffer();
|
|
1365
|
+
|
|
1366
|
+
// A string reserves 3 bytes per UTF-16 code unit, so this one grows the
|
|
1367
|
+
// module-scope arena to 6 MB.
|
|
1368
|
+
encodeJsonValue(buffer, JsonValue.orThrow("x".repeat(2_000_000)));
|
|
1369
|
+
gc();
|
|
1370
|
+
gc();
|
|
1371
|
+
|
|
1372
|
+
const retained =
|
|
1373
|
+
process.memoryUsage().arrayBuffers - before - buffer.getCapacity();
|
|
1374
|
+
assert(retained < 1_000_000, "Expected the arena to be released.", {
|
|
1375
|
+
actual: retained,
|
|
1376
|
+
});
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1167
1379
|
it("converts unexpected decoder errors to BufferError", () => {
|
|
1168
1380
|
const errorBuffer = {
|
|
1169
1381
|
...createBuffer([0xc0]),
|