@loro-dev/flock 3.1.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/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
 
@@ -57,7 +69,17 @@ export type VersionVectorEntry = {
57
69
  logicalCounter: number;
58
70
  };
59
71
 
60
- export type VersionVector = Record<string, VersionVectorEntry>;
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
+ }
61
83
 
62
84
  export type Value =
63
85
  | string
@@ -77,7 +99,10 @@ export type ExportRecord = {
77
99
  m?: MetadataMap;
78
100
  };
79
101
 
80
- export type ExportBundle = { version: number, entries: Record<string, ExportRecord> };
102
+ export type ExportBundle = {
103
+ version: number;
104
+ entries: Record<string, ExportRecord>;
105
+ };
81
106
 
82
107
  export type EntryClock = {
83
108
  physicalTime: number;
@@ -85,6 +110,12 @@ export type EntryClock = {
85
110
  peerId: string;
86
111
  };
87
112
 
113
+ export type EntryInfo = {
114
+ data?: Value;
115
+ metadata: MetadataMap;
116
+ clock: EntryClock;
117
+ };
118
+
88
119
  export type ExportPayload = {
89
120
  data?: Value;
90
121
  metadata?: MetadataMap;
@@ -182,6 +213,7 @@ export type EventBatch = {
182
213
  };
183
214
 
184
215
  const textEncoder = new TextEncoder();
216
+ const textDecoder = new TextDecoder();
185
217
 
186
218
  function utf8ByteLength(value: string): number {
187
219
  return textEncoder.encode(value).length;
@@ -193,7 +225,10 @@ function isValidPeerId(peerId: unknown): peerId is string {
193
225
 
194
226
  function createRandomPeerId(): string {
195
227
  const id = new Uint8Array(32);
196
- if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
228
+ if (
229
+ typeof crypto !== "undefined" &&
230
+ typeof crypto.getRandomValues === "function"
231
+ ) {
197
232
  crypto.getRandomValues(id);
198
233
  } else {
199
234
  for (let i = 0; i < 32; i += 1) {
@@ -208,46 +243,274 @@ function normalizePeerId(peerId?: string): string {
208
243
  return createRandomPeerId();
209
244
  }
210
245
  if (!isValidPeerId(peerId)) {
211
- 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.");
212
247
  }
213
248
  return peerId;
214
249
  }
215
250
 
216
- function encodeVersionVector(vv?: VersionVector): RawVersionVector | undefined {
217
- if (!vv) {
218
- return undefined;
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;
219
261
  }
220
- const raw: RawVersionVector = {};
221
- for (const [peer, entry] of Object.entries(vv)) {
222
- if (!entry) {
223
- continue;
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;
224
267
  }
225
- if (!isValidPeerId(peer)) {
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)) {
226
281
  continue;
227
282
  }
228
283
  const { physicalTime, logicalCounter } = entry;
229
- if (typeof physicalTime !== "number" || Number.isNaN(physicalTime)) {
284
+ if (
285
+ typeof physicalTime !== "number" ||
286
+ !Number.isFinite(physicalTime) ||
287
+ typeof logicalCounter !== "number" ||
288
+ !Number.isFinite(logicalCounter)
289
+ ) {
230
290
  continue;
231
291
  }
232
- if (!Number.isFinite(logicalCounter)) {
233
- continue;
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;
234
307
  }
235
- raw[peer] = [physicalTime, Math.trunc(logicalCounter)];
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");
422
+ }
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];
236
494
  }
237
495
  return raw;
238
496
  }
239
497
 
240
- function normalizePruneBefore(pruneTombstonesBefore?: number): number | undefined {
498
+ function normalizePruneBefore(
499
+ pruneTombstonesBefore?: number,
500
+ ): number | undefined {
241
501
  if (pruneTombstonesBefore === undefined) {
242
502
  return undefined;
243
503
  }
244
- if (typeof pruneTombstonesBefore !== "number" || !Number.isFinite(pruneTombstonesBefore)) {
504
+ if (
505
+ typeof pruneTombstonesBefore !== "number" ||
506
+ !Number.isFinite(pruneTombstonesBefore)
507
+ ) {
245
508
  return undefined;
246
509
  }
247
510
  return pruneTombstonesBefore;
248
511
  }
249
512
 
250
- function decodeVersionVector(raw: unknown): VersionVector {
513
+ function decodeVersionVectorFromRaw(raw: unknown): VersionVector {
251
514
  if (raw === null || typeof raw !== "object") {
252
515
  return {};
253
516
  }
@@ -263,7 +526,10 @@ function decodeVersionVector(raw: unknown): VersionVector {
263
526
  if (typeof physicalTime !== "number" || !Number.isFinite(physicalTime)) {
264
527
  continue;
265
528
  }
266
- if (typeof logicalCounter !== "number" || !Number.isFinite(logicalCounter)) {
529
+ if (
530
+ typeof logicalCounter !== "number" ||
531
+ !Number.isFinite(logicalCounter)
532
+ ) {
267
533
  continue;
268
534
  }
269
535
  result[peer] = {
@@ -284,6 +550,23 @@ function encodeBound(bound?: ScanBound): Record<string, unknown> | undefined {
284
550
  return { kind: bound.kind, key: bound.key.slice() };
285
551
  }
286
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
+
287
570
  function decodeEventBatch(raw: unknown): EventBatch {
288
571
  if (!raw || typeof raw !== "object") {
289
572
  return { source: "local", events: [] };
@@ -338,8 +621,9 @@ function normalizeRawEventPayload(
338
621
  return result;
339
622
  }
340
623
 
341
- const structuredCloneFn: (<T>(value: T) => T) | undefined =
342
- (globalThis as typeof globalThis & { structuredClone?: <T>(value: T) => T }).structuredClone;
624
+ const structuredCloneFn: (<T>(value: T) => T) | undefined = (
625
+ globalThis as typeof globalThis & { structuredClone?: <T>(value: T) => T }
626
+ ).structuredClone;
343
627
 
344
628
  function cloneJson<T>(value: T): T {
345
629
  if (value === undefined) {
@@ -367,10 +651,16 @@ function cloneMetadata(metadata: unknown): MetadataMap | undefined {
367
651
  return cloneJson(metadata as MetadataMap);
368
652
  }
369
653
 
654
+ function normalizeMetadataMap(metadata: unknown): MetadataMap {
655
+ const cloned = cloneMetadata(metadata);
656
+ return cloned ?? {};
657
+ }
658
+
370
659
  function decodeClock(record: ExportRecord): EntryClock {
371
660
  const rawClock = typeof record.c === "string" ? record.c : "";
372
661
  const firstComma = rawClock.indexOf(",");
373
- const secondComma = firstComma === -1 ? -1 : rawClock.indexOf(",", firstComma + 1);
662
+ const secondComma =
663
+ firstComma === -1 ? -1 : rawClock.indexOf(",", firstComma + 1);
374
664
  if (firstComma === -1 || secondComma === -1) {
375
665
  return { physicalTime: 0, logicalCounter: 0, peerId: "" };
376
666
  }
@@ -387,6 +677,29 @@ function decodeClock(record: ExportRecord): EntryClock {
387
677
  };
388
678
  }
389
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
+
390
703
  function createExportPayload(record: ExportRecord): ExportPayload {
391
704
  const payload: ExportPayload = {};
392
705
  if (record.d !== undefined) {
@@ -399,10 +712,7 @@ function createExportPayload(record: ExportRecord): ExportPayload {
399
712
  return payload;
400
713
  }
401
714
 
402
- function createPutPayload(
403
- value: Value,
404
- metadata?: MetadataMap,
405
- ): ExportPayload {
715
+ function createPutPayload(value: Value, metadata?: MetadataMap): ExportPayload {
406
716
  const payload: ExportPayload = { data: cloneJson(value) };
407
717
  const cleanMetadata = cloneMetadata(metadata);
408
718
  if (cleanMetadata !== undefined) {
@@ -511,10 +821,7 @@ function isExportOptions(value: unknown): value is ExportOptions {
511
821
  value !== null &&
512
822
  (Object.prototype.hasOwnProperty.call(value, "hooks") ||
513
823
  Object.prototype.hasOwnProperty.call(value, "from") ||
514
- Object.prototype.hasOwnProperty.call(
515
- value,
516
- "pruneTombstonesBefore",
517
- ) ||
824
+ Object.prototype.hasOwnProperty.call(value, "pruneTombstonesBefore") ||
518
825
  Object.prototype.hasOwnProperty.call(value, "peerId"))
519
826
  );
520
827
  }
@@ -564,7 +871,13 @@ export class Flock {
564
871
  now?: number,
565
872
  ): void {
566
873
  const metadataClone = cloneMetadata(metadata);
567
- put_with_meta_ffi(this.inner, key, JSON.stringify(value), metadataClone, now);
874
+ put_with_meta_ffi(
875
+ this.inner,
876
+ key,
877
+ JSON.stringify(value),
878
+ metadataClone,
879
+ now,
880
+ );
568
881
  }
569
882
 
570
883
  private async putWithMetaWithHooks(
@@ -601,15 +914,19 @@ export class Flock {
601
914
 
602
915
  /**
603
916
  * Put a value into the flock. If the given entry already exists, this insert will be skipped.
604
- * @param key
605
- * @param value
606
- * @param now
917
+ * @param key
918
+ * @param value
919
+ * @param now
607
920
  */
608
921
  put(key: KeyPart[], value: Value, now?: number): void {
609
922
  put_json_ffi(this.inner, key, JSON.stringify(value), now);
610
923
  }
611
924
 
612
- putWithMeta(key: KeyPart[], value: Value, options?: PutWithMetaOptions): void | Promise<void> {
925
+ putWithMeta(
926
+ key: KeyPart[],
927
+ value: Value,
928
+ options?: PutWithMetaOptions,
929
+ ): void | Promise<void> {
613
930
  const opts = options ?? {};
614
931
  if (opts.hooks?.transform) {
615
932
  return this.putWithMetaWithHooks(key, value, opts);
@@ -623,8 +940,8 @@ export class Flock {
623
940
 
624
941
  /**
625
942
  * Delete a value from the flock. If the given entry does not exist, this delete will be skipped.
626
- * @param key
627
- * @param now
943
+ * @param key
944
+ * @param now
628
945
  */
629
946
  delete(key: KeyPart[], now?: number): void {
630
947
  delete_ffi(this.inner, key, now);
@@ -634,12 +951,24 @@ export class Flock {
634
951
  return get_ffi(this.inner, key) as Value | undefined;
635
952
  }
636
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
+
637
966
  merge(other: Flock): void {
638
967
  merge(this.inner, other.inner);
639
968
  }
640
969
 
641
970
  version(): VersionVector {
642
- return decodeVersionVector(version_ffi(this.inner));
971
+ return decodeVersionVectorFromRaw(version_ffi(this.inner));
643
972
  }
644
973
 
645
974
  private exportJsonInternal(
@@ -648,16 +977,19 @@ export class Flock {
648
977
  peerId?: string,
649
978
  ): ExportBundle {
650
979
  const pruneBefore = normalizePruneBefore(pruneTombstonesBefore);
651
- const normalizedPeerId = peerId !== undefined && isValidPeerId(peerId) ? peerId : undefined;
980
+ const normalizedPeerId =
981
+ peerId !== undefined && isValidPeerId(peerId) ? peerId : undefined;
652
982
  return export_json_ffi(
653
983
  this.inner,
654
- encodeVersionVector(from),
984
+ encodeVersionVectorForFfi(from),
655
985
  pruneBefore,
656
986
  normalizedPeerId,
657
987
  ) as ExportBundle;
658
988
  }
659
989
 
660
- private async exportJsonWithHooks(options: ExportOptions): Promise<ExportBundle> {
990
+ private async exportJsonWithHooks(
991
+ options: ExportOptions,
992
+ ): Promise<ExportBundle> {
661
993
  const base = this.exportJsonInternal(
662
994
  options.from,
663
995
  options.pruneTombstonesBefore,
@@ -684,10 +1016,7 @@ export class Flock {
684
1016
 
685
1017
  exportJson(): ExportBundle;
686
1018
  exportJson(from: VersionVector): ExportBundle;
687
- exportJson(
688
- from: VersionVector,
689
- pruneTombstonesBefore: number,
690
- ): ExportBundle;
1019
+ exportJson(from: VersionVector, pruneTombstonesBefore: number): ExportBundle;
691
1020
  exportJson(options: ExportOptions): Promise<ExportBundle>;
692
1021
  exportJson(
693
1022
  arg?: VersionVector | ExportOptions,
@@ -703,11 +1032,15 @@ export class Flock {
703
1032
  }
704
1033
 
705
1034
  private importJsonInternal(bundle: ExportBundle): ImportReport {
706
- const report = import_json_ffi(this.inner, bundle) as RawImportReport | undefined;
1035
+ const report = import_json_ffi(this.inner, bundle) as
1036
+ | RawImportReport
1037
+ | undefined;
707
1038
  return decodeImportReport(report);
708
1039
  }
709
1040
 
710
- private async importJsonWithHooks(options: ImportOptions): Promise<ImportReport> {
1041
+ private async importJsonWithHooks(
1042
+ options: ImportOptions,
1043
+ ): Promise<ImportReport> {
711
1044
  const preprocess = options.hooks?.preprocess;
712
1045
  const working = preprocess ? cloneBundle(options.bundle) : options.bundle;
713
1046
  const skippedByHooks: Array<{ key: KeyPart[]; reason: string }> = [];
@@ -747,6 +1080,13 @@ export class Flock {
747
1080
  return this.importJsonInternal(arg);
748
1081
  }
749
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
+
750
1090
  getMaxPhysicalTime(): number {
751
1091
  return Number(get_max_physical_time_ffi(this.inner));
752
1092
  }
@@ -787,7 +1127,9 @@ export class Flock {
787
1127
  const start = encodeBound(options.start);
788
1128
  const end = encodeBound(options.end);
789
1129
  const prefix = options.prefix ? options.prefix.slice() : undefined;
790
- const rows = scan_ffi(this.inner, start, end, prefix) as RawScanRow[] | undefined;
1130
+ const rows = scan_ffi(this.inner, start, end, prefix) as
1131
+ | RawScanRow[]
1132
+ | undefined;
791
1133
  if (!Array.isArray(rows)) {
792
1134
  return [];
793
1135
  }