@loro-dev/flock 3.0.0 → 4.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/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/_moon_flock.d.ts +10 -2
- package/src/_moon_flock.ts +9055 -8513
- package/src/index.ts +396 -48
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
newFlock,
|
|
3
3
|
get_ffi,
|
|
4
|
+
get_entry_ffi,
|
|
4
5
|
put_json_ffi,
|
|
5
6
|
put_with_meta_ffi,
|
|
6
7
|
delete_ffi,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
set_peer_id,
|
|
9
10
|
export_json_ffi,
|
|
10
11
|
import_json_ffi,
|
|
12
|
+
import_json_str_ffi,
|
|
11
13
|
version_ffi,
|
|
12
14
|
get_max_physical_time_ffi,
|
|
13
15
|
peer_id_ffi,
|
|
@@ -32,6 +34,16 @@ type RawEventEntry = {
|
|
|
32
34
|
payload?: RawEventPayload;
|
|
33
35
|
};
|
|
34
36
|
type RawEventBatch = { source?: string; events?: RawEventEntry[] };
|
|
37
|
+
type RawEntryClock = {
|
|
38
|
+
physicalTime?: number;
|
|
39
|
+
logicalCounter?: number;
|
|
40
|
+
peerId?: string;
|
|
41
|
+
};
|
|
42
|
+
type RawEntryInfo = {
|
|
43
|
+
data?: Value;
|
|
44
|
+
metadata?: MetadataMap;
|
|
45
|
+
clock?: RawEntryClock;
|
|
46
|
+
};
|
|
35
47
|
|
|
36
48
|
type MaybePromise<T> = T | Promise<T>;
|
|
37
49
|
|
|
@@ -39,6 +51,7 @@ type ExportOptions = {
|
|
|
39
51
|
from?: VersionVector;
|
|
40
52
|
hooks?: ExportHooks;
|
|
41
53
|
pruneTombstonesBefore?: number;
|
|
54
|
+
peerId?: string;
|
|
42
55
|
};
|
|
43
56
|
|
|
44
57
|
type ImportOptions = {
|
|
@@ -56,7 +69,17 @@ export type VersionVectorEntry = {
|
|
|
56
69
|
logicalCounter: number;
|
|
57
70
|
};
|
|
58
71
|
|
|
59
|
-
export
|
|
72
|
+
export interface VersionVector {
|
|
73
|
+
[peer: string]: VersionVectorEntry | undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function encodeVersionVector(vector: VersionVector): Uint8Array {
|
|
77
|
+
return encodeVersionVectorBinary(vector);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function decodeVersionVector(bytes: Uint8Array): VersionVector {
|
|
81
|
+
return decodeVersionVectorBinary(bytes);
|
|
82
|
+
}
|
|
60
83
|
|
|
61
84
|
export type Value =
|
|
62
85
|
| string
|
|
@@ -76,7 +99,10 @@ export type ExportRecord = {
|
|
|
76
99
|
m?: MetadataMap;
|
|
77
100
|
};
|
|
78
101
|
|
|
79
|
-
export type ExportBundle = {
|
|
102
|
+
export type ExportBundle = {
|
|
103
|
+
version: number;
|
|
104
|
+
entries: Record<string, ExportRecord>;
|
|
105
|
+
};
|
|
80
106
|
|
|
81
107
|
export type EntryClock = {
|
|
82
108
|
physicalTime: number;
|
|
@@ -84,6 +110,12 @@ export type EntryClock = {
|
|
|
84
110
|
peerId: string;
|
|
85
111
|
};
|
|
86
112
|
|
|
113
|
+
export type EntryInfo = {
|
|
114
|
+
data?: Value;
|
|
115
|
+
metadata: MetadataMap;
|
|
116
|
+
clock: EntryClock;
|
|
117
|
+
};
|
|
118
|
+
|
|
87
119
|
export type ExportPayload = {
|
|
88
120
|
data?: Value;
|
|
89
121
|
metadata?: MetadataMap;
|
|
@@ -181,6 +213,7 @@ export type EventBatch = {
|
|
|
181
213
|
};
|
|
182
214
|
|
|
183
215
|
const textEncoder = new TextEncoder();
|
|
216
|
+
const textDecoder = new TextDecoder();
|
|
184
217
|
|
|
185
218
|
function utf8ByteLength(value: string): number {
|
|
186
219
|
return textEncoder.encode(value).length;
|
|
@@ -192,7 +225,10 @@ function isValidPeerId(peerId: unknown): peerId is string {
|
|
|
192
225
|
|
|
193
226
|
function createRandomPeerId(): string {
|
|
194
227
|
const id = new Uint8Array(32);
|
|
195
|
-
if (
|
|
228
|
+
if (
|
|
229
|
+
typeof crypto !== "undefined" &&
|
|
230
|
+
typeof crypto.getRandomValues === "function"
|
|
231
|
+
) {
|
|
196
232
|
crypto.getRandomValues(id);
|
|
197
233
|
} else {
|
|
198
234
|
for (let i = 0; i < 32; i += 1) {
|
|
@@ -207,46 +243,274 @@ function normalizePeerId(peerId?: string): string {
|
|
|
207
243
|
return createRandomPeerId();
|
|
208
244
|
}
|
|
209
245
|
if (!isValidPeerId(peerId)) {
|
|
210
|
-
throw new TypeError("peerId must be a UTF-8 string under 128 bytes");
|
|
246
|
+
throw new TypeError("peerId must be a UTF-8 string under 128 bytes.");
|
|
211
247
|
}
|
|
212
248
|
return peerId;
|
|
213
249
|
}
|
|
214
250
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
251
|
+
type EncodableVersionVectorEntry = {
|
|
252
|
+
peer: string;
|
|
253
|
+
peerBytes: Uint8Array;
|
|
254
|
+
timestamp: number;
|
|
255
|
+
counter: number;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
function comparePeerBytes(a: Uint8Array, b: Uint8Array): number {
|
|
259
|
+
if (a === b) {
|
|
260
|
+
return 0;
|
|
218
261
|
}
|
|
219
|
-
const
|
|
220
|
-
for (
|
|
221
|
-
|
|
222
|
-
|
|
262
|
+
const limit = Math.min(a.length, b.length);
|
|
263
|
+
for (let i = 0; i < limit; i += 1) {
|
|
264
|
+
const diff = a[i] - b[i];
|
|
265
|
+
if (diff !== 0) {
|
|
266
|
+
return diff;
|
|
223
267
|
}
|
|
224
|
-
|
|
268
|
+
}
|
|
269
|
+
return a.length - b.length;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function collectEncodableVersionVectorEntries(
|
|
273
|
+
vv?: VersionVector,
|
|
274
|
+
): EncodableVersionVectorEntry[] {
|
|
275
|
+
if (!vv || typeof vv !== "object") {
|
|
276
|
+
return [];
|
|
277
|
+
}
|
|
278
|
+
const entries: EncodableVersionVectorEntry[] = [];
|
|
279
|
+
for (const [peer, entry] of Object.entries(vv)) {
|
|
280
|
+
if (!entry || !isValidPeerId(peer)) {
|
|
225
281
|
continue;
|
|
226
282
|
}
|
|
227
283
|
const { physicalTime, logicalCounter } = entry;
|
|
228
|
-
if (
|
|
284
|
+
if (
|
|
285
|
+
typeof physicalTime !== "number" ||
|
|
286
|
+
!Number.isFinite(physicalTime) ||
|
|
287
|
+
typeof logicalCounter !== "number" ||
|
|
288
|
+
!Number.isFinite(logicalCounter)
|
|
289
|
+
) {
|
|
229
290
|
continue;
|
|
230
291
|
}
|
|
231
|
-
|
|
232
|
-
|
|
292
|
+
const peerBytes = textEncoder.encode(peer);
|
|
293
|
+
entries.push({
|
|
294
|
+
peer,
|
|
295
|
+
peerBytes,
|
|
296
|
+
timestamp: Math.trunc(physicalTime),
|
|
297
|
+
counter: Math.max(0, Math.trunc(logicalCounter)),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
entries.sort((a, b) => {
|
|
301
|
+
if (a.timestamp !== b.timestamp) {
|
|
302
|
+
return a.timestamp - b.timestamp;
|
|
303
|
+
}
|
|
304
|
+
const peerCmp = comparePeerBytes(a.peerBytes, b.peerBytes);
|
|
305
|
+
if (peerCmp !== 0) {
|
|
306
|
+
return peerCmp;
|
|
307
|
+
}
|
|
308
|
+
return a.counter - b.counter;
|
|
309
|
+
});
|
|
310
|
+
return entries;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function writeUnsignedLeb128(value: number, out: number[]): void {
|
|
314
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
315
|
+
throw new TypeError("leb128 values must be finite and non-negative");
|
|
316
|
+
}
|
|
317
|
+
let remaining = Math.trunc(value);
|
|
318
|
+
if (remaining === 0) {
|
|
319
|
+
out.push(0);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
while (remaining > 0) {
|
|
323
|
+
const byte = remaining % 0x80;
|
|
324
|
+
remaining = Math.floor(remaining / 0x80);
|
|
325
|
+
out.push(remaining > 0 ? byte | 0x80 : byte);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function writeVarStringBytes(bytes: Uint8Array, out: number[]): void {
|
|
330
|
+
writeUnsignedLeb128(bytes.length, out);
|
|
331
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
332
|
+
out.push(bytes[i]);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const VERSION_VECTOR_MAGIC = new Uint8Array([86, 69, 86, 69]); // "VEVE"
|
|
337
|
+
|
|
338
|
+
function encodeVersionVectorBinary(vv?: VersionVector): Uint8Array {
|
|
339
|
+
const entries = collectEncodableVersionVectorEntries(vv);
|
|
340
|
+
const buffer: number[] = Array.from(VERSION_VECTOR_MAGIC);
|
|
341
|
+
if (entries.length === 0) {
|
|
342
|
+
return Uint8Array.from(buffer);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let lastTimestamp = 0;
|
|
346
|
+
for (let i = 0; i < entries.length; i += 1) {
|
|
347
|
+
const entry = entries[i];
|
|
348
|
+
if (entry.timestamp < 0) {
|
|
349
|
+
throw new TypeError("timestamp must be non-negative");
|
|
350
|
+
}
|
|
351
|
+
if (i === 0) {
|
|
352
|
+
writeUnsignedLeb128(entry.timestamp, buffer);
|
|
353
|
+
lastTimestamp = entry.timestamp;
|
|
354
|
+
} else {
|
|
355
|
+
const delta = entry.timestamp - lastTimestamp;
|
|
356
|
+
if (delta < 0) {
|
|
357
|
+
throw new TypeError("version vector timestamps must be non-decreasing");
|
|
358
|
+
}
|
|
359
|
+
writeUnsignedLeb128(delta, buffer);
|
|
360
|
+
lastTimestamp = entry.timestamp;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
writeUnsignedLeb128(entry.counter, buffer);
|
|
364
|
+
writeVarStringBytes(entry.peerBytes, buffer);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return Uint8Array.from(buffer);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function decodeUnsignedLeb128(
|
|
371
|
+
bytes: Uint8Array,
|
|
372
|
+
offset: number,
|
|
373
|
+
): [number, number] {
|
|
374
|
+
let result = 0;
|
|
375
|
+
let shift = 0;
|
|
376
|
+
let consumed = 0;
|
|
377
|
+
while (offset + consumed < bytes.length) {
|
|
378
|
+
const byte = bytes[offset + consumed];
|
|
379
|
+
consumed += 1;
|
|
380
|
+
result |= (byte & 0x7f) << shift;
|
|
381
|
+
if ((byte & 0x80) === 0) {
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
shift += 7;
|
|
385
|
+
}
|
|
386
|
+
return [result, consumed];
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function decodeVarString(bytes: Uint8Array, offset: number): [string, number] {
|
|
390
|
+
const [length, used] = decodeUnsignedLeb128(bytes, offset);
|
|
391
|
+
const start = offset + used;
|
|
392
|
+
const end = start + length;
|
|
393
|
+
if (end > bytes.length) {
|
|
394
|
+
throw new TypeError("varString length exceeds buffer");
|
|
395
|
+
}
|
|
396
|
+
const slice = bytes.subarray(start, end);
|
|
397
|
+
return [textDecoder.decode(slice), used + length];
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function hasMagic(bytes: Uint8Array): boolean {
|
|
401
|
+
return (
|
|
402
|
+
bytes.length >= 4 &&
|
|
403
|
+
bytes[0] === VERSION_VECTOR_MAGIC[0] &&
|
|
404
|
+
bytes[1] === VERSION_VECTOR_MAGIC[1] &&
|
|
405
|
+
bytes[2] === VERSION_VECTOR_MAGIC[2] &&
|
|
406
|
+
bytes[3] === VERSION_VECTOR_MAGIC[3]
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function decodeLegacyVersionVector(bytes: Uint8Array): VersionVector {
|
|
411
|
+
let offset = 0;
|
|
412
|
+
const [count, usedCount] = decodeUnsignedLeb128(bytes, offset);
|
|
413
|
+
offset += usedCount;
|
|
414
|
+
const [baseTimestamp, usedBase] = decodeUnsignedLeb128(bytes, offset);
|
|
415
|
+
offset += usedBase;
|
|
416
|
+
const vv: VersionVector = {};
|
|
417
|
+
for (let i = 0; i < count; i += 1) {
|
|
418
|
+
const [peer, usedPeer] = decodeVarString(bytes, offset);
|
|
419
|
+
offset += usedPeer;
|
|
420
|
+
if (!isValidPeerId(peer)) {
|
|
421
|
+
throw new TypeError("invalid peer id in encoded version vector");
|
|
233
422
|
}
|
|
234
|
-
|
|
423
|
+
const [delta, usedDelta] = decodeUnsignedLeb128(bytes, offset);
|
|
424
|
+
offset += usedDelta;
|
|
425
|
+
const [counter, usedCounter] = decodeUnsignedLeb128(bytes, offset);
|
|
426
|
+
offset += usedCounter;
|
|
427
|
+
vv[peer] = {
|
|
428
|
+
physicalTime: baseTimestamp + delta,
|
|
429
|
+
logicalCounter: counter,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return vv;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function decodeNewVersionVector(bytes: Uint8Array): VersionVector {
|
|
436
|
+
let offset = 4;
|
|
437
|
+
const vv: VersionVector = {};
|
|
438
|
+
if (offset === bytes.length) {
|
|
439
|
+
return vv;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const [firstTimestamp, usedTs] = decodeUnsignedLeb128(bytes, offset);
|
|
443
|
+
offset += usedTs;
|
|
444
|
+
const [firstCounter, usedCounter] = decodeUnsignedLeb128(bytes, offset);
|
|
445
|
+
offset += usedCounter;
|
|
446
|
+
const [firstPeer, usedPeer] = decodeVarString(bytes, offset);
|
|
447
|
+
offset += usedPeer;
|
|
448
|
+
if (!isValidPeerId(firstPeer)) {
|
|
449
|
+
throw new TypeError("invalid peer id in encoded version vector");
|
|
450
|
+
}
|
|
451
|
+
vv[firstPeer] = {
|
|
452
|
+
physicalTime: firstTimestamp,
|
|
453
|
+
logicalCounter: firstCounter,
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
let lastTimestamp = firstTimestamp;
|
|
457
|
+
while (offset < bytes.length) {
|
|
458
|
+
const [delta, usedDelta] = decodeUnsignedLeb128(bytes, offset);
|
|
459
|
+
offset += usedDelta;
|
|
460
|
+
const [counter, usedCtr] = decodeUnsignedLeb128(bytes, offset);
|
|
461
|
+
offset += usedCtr;
|
|
462
|
+
const [peer, usedPeerLen] = decodeVarString(bytes, offset);
|
|
463
|
+
offset += usedPeerLen;
|
|
464
|
+
if (!isValidPeerId(peer)) {
|
|
465
|
+
throw new TypeError("invalid peer id in encoded version vector");
|
|
466
|
+
}
|
|
467
|
+
const timestamp = lastTimestamp + delta;
|
|
468
|
+
if (timestamp < lastTimestamp) {
|
|
469
|
+
throw new TypeError("version vector timestamps must be non-decreasing");
|
|
470
|
+
}
|
|
471
|
+
vv[peer] = { physicalTime: timestamp, logicalCounter: counter };
|
|
472
|
+
lastTimestamp = timestamp;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return vv;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function decodeVersionVectorBinary(bytes: Uint8Array): VersionVector {
|
|
479
|
+
if (hasMagic(bytes)) {
|
|
480
|
+
return decodeNewVersionVector(bytes);
|
|
481
|
+
}
|
|
482
|
+
return decodeLegacyVersionVector(bytes);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function encodeVersionVectorForFfi(
|
|
486
|
+
vv?: VersionVector,
|
|
487
|
+
): RawVersionVector | undefined {
|
|
488
|
+
if (!vv) {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
const raw: RawVersionVector = {};
|
|
492
|
+
for (const entry of collectEncodableVersionVectorEntries(vv)) {
|
|
493
|
+
raw[entry.peer] = [entry.timestamp, entry.counter];
|
|
235
494
|
}
|
|
236
495
|
return raw;
|
|
237
496
|
}
|
|
238
497
|
|
|
239
|
-
function normalizePruneBefore(
|
|
498
|
+
function normalizePruneBefore(
|
|
499
|
+
pruneTombstonesBefore?: number,
|
|
500
|
+
): number | undefined {
|
|
240
501
|
if (pruneTombstonesBefore === undefined) {
|
|
241
502
|
return undefined;
|
|
242
503
|
}
|
|
243
|
-
if (
|
|
504
|
+
if (
|
|
505
|
+
typeof pruneTombstonesBefore !== "number" ||
|
|
506
|
+
!Number.isFinite(pruneTombstonesBefore)
|
|
507
|
+
) {
|
|
244
508
|
return undefined;
|
|
245
509
|
}
|
|
246
510
|
return pruneTombstonesBefore;
|
|
247
511
|
}
|
|
248
512
|
|
|
249
|
-
function
|
|
513
|
+
function decodeVersionVectorFromRaw(raw: unknown): VersionVector {
|
|
250
514
|
if (raw === null || typeof raw !== "object") {
|
|
251
515
|
return {};
|
|
252
516
|
}
|
|
@@ -262,7 +526,10 @@ function decodeVersionVector(raw: unknown): VersionVector {
|
|
|
262
526
|
if (typeof physicalTime !== "number" || !Number.isFinite(physicalTime)) {
|
|
263
527
|
continue;
|
|
264
528
|
}
|
|
265
|
-
if (
|
|
529
|
+
if (
|
|
530
|
+
typeof logicalCounter !== "number" ||
|
|
531
|
+
!Number.isFinite(logicalCounter)
|
|
532
|
+
) {
|
|
266
533
|
continue;
|
|
267
534
|
}
|
|
268
535
|
result[peer] = {
|
|
@@ -283,6 +550,23 @@ function encodeBound(bound?: ScanBound): Record<string, unknown> | undefined {
|
|
|
283
550
|
return { kind: bound.kind, key: bound.key.slice() };
|
|
284
551
|
}
|
|
285
552
|
|
|
553
|
+
function decodeEntryInfo(raw: unknown): EntryInfo | undefined {
|
|
554
|
+
if (!raw || typeof raw !== "object") {
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
const info = raw as RawEntryInfo;
|
|
558
|
+
const clock = normalizeEntryClock(info.clock);
|
|
559
|
+
if (!clock) {
|
|
560
|
+
return undefined;
|
|
561
|
+
}
|
|
562
|
+
const metadata = normalizeMetadataMap(info.metadata);
|
|
563
|
+
const result: EntryInfo = { metadata, clock };
|
|
564
|
+
if ("data" in info) {
|
|
565
|
+
result.data = cloneJson(info.data as Value);
|
|
566
|
+
}
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
|
|
286
570
|
function decodeEventBatch(raw: unknown): EventBatch {
|
|
287
571
|
if (!raw || typeof raw !== "object") {
|
|
288
572
|
return { source: "local", events: [] };
|
|
@@ -337,8 +621,9 @@ function normalizeRawEventPayload(
|
|
|
337
621
|
return result;
|
|
338
622
|
}
|
|
339
623
|
|
|
340
|
-
const structuredCloneFn: (<T>(value: T) => T) | undefined =
|
|
341
|
-
|
|
624
|
+
const structuredCloneFn: (<T>(value: T) => T) | undefined = (
|
|
625
|
+
globalThis as typeof globalThis & { structuredClone?: <T>(value: T) => T }
|
|
626
|
+
).structuredClone;
|
|
342
627
|
|
|
343
628
|
function cloneJson<T>(value: T): T {
|
|
344
629
|
if (value === undefined) {
|
|
@@ -366,10 +651,16 @@ function cloneMetadata(metadata: unknown): MetadataMap | undefined {
|
|
|
366
651
|
return cloneJson(metadata as MetadataMap);
|
|
367
652
|
}
|
|
368
653
|
|
|
654
|
+
function normalizeMetadataMap(metadata: unknown): MetadataMap {
|
|
655
|
+
const cloned = cloneMetadata(metadata);
|
|
656
|
+
return cloned ?? {};
|
|
657
|
+
}
|
|
658
|
+
|
|
369
659
|
function decodeClock(record: ExportRecord): EntryClock {
|
|
370
660
|
const rawClock = typeof record.c === "string" ? record.c : "";
|
|
371
661
|
const firstComma = rawClock.indexOf(",");
|
|
372
|
-
const secondComma =
|
|
662
|
+
const secondComma =
|
|
663
|
+
firstComma === -1 ? -1 : rawClock.indexOf(",", firstComma + 1);
|
|
373
664
|
if (firstComma === -1 || secondComma === -1) {
|
|
374
665
|
return { physicalTime: 0, logicalCounter: 0, peerId: "" };
|
|
375
666
|
}
|
|
@@ -386,6 +677,29 @@ function decodeClock(record: ExportRecord): EntryClock {
|
|
|
386
677
|
};
|
|
387
678
|
}
|
|
388
679
|
|
|
680
|
+
function normalizeEntryClock(
|
|
681
|
+
clock: RawEntryClock | undefined,
|
|
682
|
+
): EntryClock | undefined {
|
|
683
|
+
if (!clock || typeof clock !== "object") {
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
const { physicalTime, logicalCounter, peerId } = clock;
|
|
687
|
+
if (typeof physicalTime !== "number" || !Number.isFinite(physicalTime)) {
|
|
688
|
+
return undefined;
|
|
689
|
+
}
|
|
690
|
+
if (typeof logicalCounter !== "number" || !Number.isFinite(logicalCounter)) {
|
|
691
|
+
return undefined;
|
|
692
|
+
}
|
|
693
|
+
if (!isValidPeerId(peerId)) {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
physicalTime,
|
|
698
|
+
logicalCounter: Math.trunc(logicalCounter),
|
|
699
|
+
peerId,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
389
703
|
function createExportPayload(record: ExportRecord): ExportPayload {
|
|
390
704
|
const payload: ExportPayload = {};
|
|
391
705
|
if (record.d !== undefined) {
|
|
@@ -398,10 +712,7 @@ function createExportPayload(record: ExportRecord): ExportPayload {
|
|
|
398
712
|
return payload;
|
|
399
713
|
}
|
|
400
714
|
|
|
401
|
-
function createPutPayload(
|
|
402
|
-
value: Value,
|
|
403
|
-
metadata?: MetadataMap,
|
|
404
|
-
): ExportPayload {
|
|
715
|
+
function createPutPayload(value: Value, metadata?: MetadataMap): ExportPayload {
|
|
405
716
|
const payload: ExportPayload = { data: cloneJson(value) };
|
|
406
717
|
const cleanMetadata = cloneMetadata(metadata);
|
|
407
718
|
if (cleanMetadata !== undefined) {
|
|
@@ -510,10 +821,8 @@ function isExportOptions(value: unknown): value is ExportOptions {
|
|
|
510
821
|
value !== null &&
|
|
511
822
|
(Object.prototype.hasOwnProperty.call(value, "hooks") ||
|
|
512
823
|
Object.prototype.hasOwnProperty.call(value, "from") ||
|
|
513
|
-
Object.prototype.hasOwnProperty.call(
|
|
514
|
-
|
|
515
|
-
"pruneTombstonesBefore",
|
|
516
|
-
))
|
|
824
|
+
Object.prototype.hasOwnProperty.call(value, "pruneTombstonesBefore") ||
|
|
825
|
+
Object.prototype.hasOwnProperty.call(value, "peerId"))
|
|
517
826
|
);
|
|
518
827
|
}
|
|
519
828
|
|
|
@@ -562,7 +871,13 @@ export class Flock {
|
|
|
562
871
|
now?: number,
|
|
563
872
|
): void {
|
|
564
873
|
const metadataClone = cloneMetadata(metadata);
|
|
565
|
-
put_with_meta_ffi(
|
|
874
|
+
put_with_meta_ffi(
|
|
875
|
+
this.inner,
|
|
876
|
+
key,
|
|
877
|
+
JSON.stringify(value),
|
|
878
|
+
metadataClone,
|
|
879
|
+
now,
|
|
880
|
+
);
|
|
566
881
|
}
|
|
567
882
|
|
|
568
883
|
private async putWithMetaWithHooks(
|
|
@@ -599,15 +914,19 @@ export class Flock {
|
|
|
599
914
|
|
|
600
915
|
/**
|
|
601
916
|
* Put a value into the flock. If the given entry already exists, this insert will be skipped.
|
|
602
|
-
* @param key
|
|
603
|
-
* @param value
|
|
604
|
-
* @param now
|
|
917
|
+
* @param key
|
|
918
|
+
* @param value
|
|
919
|
+
* @param now
|
|
605
920
|
*/
|
|
606
921
|
put(key: KeyPart[], value: Value, now?: number): void {
|
|
607
922
|
put_json_ffi(this.inner, key, JSON.stringify(value), now);
|
|
608
923
|
}
|
|
609
924
|
|
|
610
|
-
putWithMeta(
|
|
925
|
+
putWithMeta(
|
|
926
|
+
key: KeyPart[],
|
|
927
|
+
value: Value,
|
|
928
|
+
options?: PutWithMetaOptions,
|
|
929
|
+
): void | Promise<void> {
|
|
611
930
|
const opts = options ?? {};
|
|
612
931
|
if (opts.hooks?.transform) {
|
|
613
932
|
return this.putWithMetaWithHooks(key, value, opts);
|
|
@@ -621,8 +940,8 @@ export class Flock {
|
|
|
621
940
|
|
|
622
941
|
/**
|
|
623
942
|
* Delete a value from the flock. If the given entry does not exist, this delete will be skipped.
|
|
624
|
-
* @param key
|
|
625
|
-
* @param now
|
|
943
|
+
* @param key
|
|
944
|
+
* @param now
|
|
626
945
|
*/
|
|
627
946
|
delete(key: KeyPart[], now?: number): void {
|
|
628
947
|
delete_ffi(this.inner, key, now);
|
|
@@ -632,30 +951,49 @@ export class Flock {
|
|
|
632
951
|
return get_ffi(this.inner, key) as Value | undefined;
|
|
633
952
|
}
|
|
634
953
|
|
|
954
|
+
/**
|
|
955
|
+
* Returns the full entry payload (data, metadata, and clock) for a key.
|
|
956
|
+
*
|
|
957
|
+
* Unlike `get`, this distinguishes between a missing key (`undefined`) and a
|
|
958
|
+
* tombstone (returns the clock and metadata with `data` omitted). Metadata is
|
|
959
|
+
* cloned and defaults to `{}` when absent.
|
|
960
|
+
*/
|
|
961
|
+
getEntry(key: KeyPart[]): EntryInfo | undefined {
|
|
962
|
+
const raw = get_entry_ffi(this.inner, key) as RawEntryInfo | undefined;
|
|
963
|
+
return decodeEntryInfo(raw);
|
|
964
|
+
}
|
|
965
|
+
|
|
635
966
|
merge(other: Flock): void {
|
|
636
967
|
merge(this.inner, other.inner);
|
|
637
968
|
}
|
|
638
969
|
|
|
639
970
|
version(): VersionVector {
|
|
640
|
-
return
|
|
971
|
+
return decodeVersionVectorFromRaw(version_ffi(this.inner));
|
|
641
972
|
}
|
|
642
973
|
|
|
643
974
|
private exportJsonInternal(
|
|
644
975
|
from?: VersionVector,
|
|
645
976
|
pruneTombstonesBefore?: number,
|
|
977
|
+
peerId?: string,
|
|
646
978
|
): ExportBundle {
|
|
647
979
|
const pruneBefore = normalizePruneBefore(pruneTombstonesBefore);
|
|
980
|
+
const normalizedPeerId =
|
|
981
|
+
peerId !== undefined && isValidPeerId(peerId) ? peerId : undefined;
|
|
648
982
|
return export_json_ffi(
|
|
649
983
|
this.inner,
|
|
650
|
-
|
|
984
|
+
encodeVersionVectorForFfi(from),
|
|
651
985
|
pruneBefore,
|
|
986
|
+
normalizedPeerId,
|
|
652
987
|
) as ExportBundle;
|
|
653
988
|
}
|
|
654
989
|
|
|
655
|
-
private async exportJsonWithHooks(
|
|
990
|
+
private async exportJsonWithHooks(
|
|
991
|
+
options: ExportOptions,
|
|
992
|
+
): Promise<ExportBundle> {
|
|
656
993
|
const base = this.exportJsonInternal(
|
|
657
994
|
options.from,
|
|
658
995
|
options.pruneTombstonesBefore,
|
|
996
|
+
options.peerId,
|
|
659
997
|
);
|
|
660
998
|
const transform = options.hooks?.transform;
|
|
661
999
|
if (!transform) {
|
|
@@ -678,10 +1016,7 @@ export class Flock {
|
|
|
678
1016
|
|
|
679
1017
|
exportJson(): ExportBundle;
|
|
680
1018
|
exportJson(from: VersionVector): ExportBundle;
|
|
681
|
-
exportJson(
|
|
682
|
-
from: VersionVector,
|
|
683
|
-
pruneTombstonesBefore: number,
|
|
684
|
-
): ExportBundle;
|
|
1019
|
+
exportJson(from: VersionVector, pruneTombstonesBefore: number): ExportBundle;
|
|
685
1020
|
exportJson(options: ExportOptions): Promise<ExportBundle>;
|
|
686
1021
|
exportJson(
|
|
687
1022
|
arg?: VersionVector | ExportOptions,
|
|
@@ -697,11 +1032,15 @@ export class Flock {
|
|
|
697
1032
|
}
|
|
698
1033
|
|
|
699
1034
|
private importJsonInternal(bundle: ExportBundle): ImportReport {
|
|
700
|
-
const report = import_json_ffi(this.inner, bundle) as
|
|
1035
|
+
const report = import_json_ffi(this.inner, bundle) as
|
|
1036
|
+
| RawImportReport
|
|
1037
|
+
| undefined;
|
|
701
1038
|
return decodeImportReport(report);
|
|
702
1039
|
}
|
|
703
1040
|
|
|
704
|
-
private async importJsonWithHooks(
|
|
1041
|
+
private async importJsonWithHooks(
|
|
1042
|
+
options: ImportOptions,
|
|
1043
|
+
): Promise<ImportReport> {
|
|
705
1044
|
const preprocess = options.hooks?.preprocess;
|
|
706
1045
|
const working = preprocess ? cloneBundle(options.bundle) : options.bundle;
|
|
707
1046
|
const skippedByHooks: Array<{ key: KeyPart[]; reason: string }> = [];
|
|
@@ -741,6 +1080,13 @@ export class Flock {
|
|
|
741
1080
|
return this.importJsonInternal(arg);
|
|
742
1081
|
}
|
|
743
1082
|
|
|
1083
|
+
importJsonStr(bundle: string): ImportReport {
|
|
1084
|
+
const report = import_json_str_ffi(this.inner, bundle) as
|
|
1085
|
+
| RawImportReport
|
|
1086
|
+
| undefined;
|
|
1087
|
+
return decodeImportReport(report);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
744
1090
|
getMaxPhysicalTime(): number {
|
|
745
1091
|
return Number(get_max_physical_time_ffi(this.inner));
|
|
746
1092
|
}
|
|
@@ -781,7 +1127,9 @@ export class Flock {
|
|
|
781
1127
|
const start = encodeBound(options.start);
|
|
782
1128
|
const end = encodeBound(options.end);
|
|
783
1129
|
const prefix = options.prefix ? options.prefix.slice() : undefined;
|
|
784
|
-
const rows = scan_ffi(this.inner, start, end, prefix) as
|
|
1130
|
+
const rows = scan_ffi(this.inner, start, end, prefix) as
|
|
1131
|
+
| RawScanRow[]
|
|
1132
|
+
| undefined;
|
|
785
1133
|
if (!Array.isArray(rows)) {
|
|
786
1134
|
return [];
|
|
787
1135
|
}
|