@loro-dev/flock 1.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 ADDED
@@ -0,0 +1,758 @@
1
+ import {
2
+ newFlock,
3
+ get_ffi,
4
+ put_json_ffi,
5
+ put_with_meta_ffi,
6
+ delete_ffi,
7
+ merge,
8
+ set_peer_id,
9
+ export_json_ffi,
10
+ import_json_ffi,
11
+ version_ffi,
12
+ get_max_physical_time_ffi,
13
+ peer_id_ffi,
14
+ kv_to_json_ffi,
15
+ digest_hex_ffi,
16
+ put_mvr_ffi,
17
+ get_mvr_ffi,
18
+ scan_ffi,
19
+ subscribe_ffi,
20
+ check_consistency_ffi,
21
+ check_invariants_ffi,
22
+ from_json_ffi,
23
+ } from "./_moon_flock";
24
+
25
+ type RawVersionVector = Record<string, [number, number]>;
26
+ type RawScanRow = { key: KeyPart[]; raw: ExportRecord; value?: Value };
27
+ type RawEventPayload = { data?: Value; metadata?: MetadataMap };
28
+ type RawEventEntry = {
29
+ key?: KeyPart[];
30
+ value?: Value;
31
+ metadata?: MetadataMap;
32
+ payload?: RawEventPayload;
33
+ };
34
+ type RawEventBatch = { source?: string; events?: RawEventEntry[] };
35
+
36
+ type MaybePromise<T> = T | Promise<T>;
37
+
38
+ type ExportOptions = {
39
+ from?: VersionVector;
40
+ hooks?: ExportHooks;
41
+ };
42
+
43
+ type ImportOptions = {
44
+ bundle: ExportBundle;
45
+ hooks?: ImportHooks;
46
+ };
47
+
48
+ type RawImportReport = {
49
+ accepted?: number;
50
+ skipped?: Array<{ key?: KeyPart[]; reason?: string }>;
51
+ };
52
+
53
+ export type VersionVectorEntry = {
54
+ physicalTime: number;
55
+ logicalCounter: number;
56
+ };
57
+
58
+ export type VersionVector = Record<string, VersionVectorEntry>;
59
+
60
+ export type Value =
61
+ | string
62
+ | number
63
+ | boolean
64
+ | null
65
+ | Array<Value>
66
+ | { [key: string]: Value };
67
+
68
+ export type KeyPart = Value;
69
+
70
+ export type MetadataMap = Record<string, unknown>;
71
+
72
+ export type ExportRecord = {
73
+ c: string;
74
+ d?: Value;
75
+ m?: MetadataMap;
76
+ };
77
+
78
+ export type ExportBundle = Record<string, ExportRecord>;
79
+
80
+ export type EntryClock = {
81
+ physicalTime: number;
82
+ logicalCounter: number;
83
+ peerIdHex: string;
84
+ peerId: Uint8Array;
85
+ };
86
+
87
+ export type ExportPayload = {
88
+ data?: Value;
89
+ metadata?: MetadataMap;
90
+ };
91
+
92
+ export type ExportHookContext = {
93
+ key: KeyPart[];
94
+ clock: EntryClock;
95
+ raw: ExportRecord;
96
+ };
97
+
98
+ export type ExportHooks = {
99
+ transform?: (
100
+ context: ExportHookContext,
101
+ payload: ExportPayload,
102
+ ) => MaybePromise<ExportPayload | void>;
103
+ };
104
+
105
+ export type ImportPayload = ExportPayload;
106
+
107
+ export type ImportHookContext = ExportHookContext;
108
+
109
+ export type ImportAccept = {
110
+ accept: true;
111
+ };
112
+
113
+ export type ImportSkip = {
114
+ accept: false;
115
+ reason: string;
116
+ };
117
+
118
+ export type ImportDecision = ImportAccept | ImportSkip | ImportPayload | void;
119
+
120
+ export type ImportHooks = {
121
+ preprocess?: (
122
+ context: ImportHookContext,
123
+ payload: ImportPayload,
124
+ ) => MaybePromise<ImportDecision>;
125
+ };
126
+
127
+ export type ImportReport = {
128
+ accepted: number;
129
+ skipped: Array<{ key: KeyPart[]; reason: string }>;
130
+ };
131
+
132
+ export type PutPayload = ExportPayload;
133
+
134
+ export type PutHookContext = {
135
+ key: KeyPart[];
136
+ now?: number;
137
+ };
138
+
139
+ export type PutHooks = {
140
+ transform?: (
141
+ context: PutHookContext,
142
+ payload: PutPayload,
143
+ ) => MaybePromise<PutPayload | void>;
144
+ };
145
+
146
+ export type PutWithMetaOptions = {
147
+ metadata?: MetadataMap;
148
+ now?: number;
149
+ hooks?: PutHooks;
150
+ };
151
+
152
+ export type ScanBound =
153
+ | { kind: "inclusive"; key: KeyPart[] }
154
+ | { kind: "exclusive"; key: KeyPart[] }
155
+ | { kind: "unbounded" };
156
+
157
+ export type ScanOptions = {
158
+ start?: ScanBound;
159
+ end?: ScanBound;
160
+ prefix?: KeyPart[];
161
+ };
162
+
163
+ export type ScanRow = {
164
+ key: KeyPart[];
165
+ raw: ExportRecord;
166
+ value?: Value;
167
+ };
168
+
169
+ export type EventPayload = ExportPayload;
170
+
171
+ export type Event = {
172
+ key: KeyPart[];
173
+ value?: Value;
174
+ metadata?: MetadataMap;
175
+ payload: EventPayload;
176
+ };
177
+
178
+ export type EventBatch = {
179
+ source: string;
180
+ events: Event[];
181
+ };
182
+
183
+ function bytesToHex(bytes: Uint8Array): string {
184
+ let hex = "";
185
+ for (let i = 0; i < bytes.length; i += 1) {
186
+ hex += bytes[i].toString(16).padStart(2, "0");
187
+ }
188
+ return hex;
189
+ }
190
+
191
+ function hexToBytes(hex: string): Uint8Array {
192
+ if (hex.length % 2 !== 0) {
193
+ throw new TypeError("peerId hex must have even length");
194
+ }
195
+ const bytes = new Uint8Array(hex.length / 2);
196
+ for (let i = 0; i < bytes.length; i += 1) {
197
+ const byte = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
198
+ if (Number.isNaN(byte)) {
199
+ throw new TypeError("peerId hex contains invalid characters");
200
+ }
201
+ bytes[i] = byte;
202
+ }
203
+ return bytes;
204
+ }
205
+
206
+ function createRandomPeerId(): Uint8Array {
207
+ const id = new Uint8Array(32);
208
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
209
+ crypto.getRandomValues(id);
210
+ } else {
211
+ for (let i = 0; i < 32; i += 1) {
212
+ id[i] = Math.floor(Math.random() * 256);
213
+ }
214
+ }
215
+ return id;
216
+ }
217
+
218
+ function normalizePeerId(peerId?: Uint8Array): Uint8Array {
219
+ if (peerId === undefined) {
220
+ return createRandomPeerId();
221
+ }
222
+ if (!(peerId instanceof Uint8Array)) {
223
+ throw new TypeError("peerId must be a Uint8Array");
224
+ }
225
+ return new Uint8Array(peerId);
226
+ }
227
+
228
+ function encodeVersionVector(vv?: VersionVector): RawVersionVector | undefined {
229
+ if (!vv) {
230
+ return undefined;
231
+ }
232
+ const raw: RawVersionVector = {};
233
+ for (const [peer, entry] of Object.entries(vv)) {
234
+ if (!entry) {
235
+ continue;
236
+ }
237
+ const { physicalTime, logicalCounter } = entry;
238
+ if (typeof physicalTime !== "number" || Number.isNaN(physicalTime)) {
239
+ continue;
240
+ }
241
+ if (!Number.isFinite(logicalCounter)) {
242
+ continue;
243
+ }
244
+ raw[peer] = [physicalTime, Math.trunc(logicalCounter)];
245
+ }
246
+ return raw;
247
+ }
248
+
249
+ function decodeVersionVector(raw: unknown): VersionVector {
250
+ if (raw === null || typeof raw !== "object") {
251
+ return {};
252
+ }
253
+ const result: VersionVector = {};
254
+ for (const [peer, value] of Object.entries(raw as Record<string, unknown>)) {
255
+ if (!Array.isArray(value) || value.length < 2) {
256
+ continue;
257
+ }
258
+ const [physicalTime, logicalCounter] = value;
259
+ if (typeof physicalTime !== "number" || !Number.isFinite(physicalTime)) {
260
+ continue;
261
+ }
262
+ if (typeof logicalCounter !== "number" || !Number.isFinite(logicalCounter)) {
263
+ continue;
264
+ }
265
+ result[peer] = {
266
+ physicalTime,
267
+ logicalCounter: Math.trunc(logicalCounter),
268
+ };
269
+ }
270
+ return result;
271
+ }
272
+
273
+ function encodeBound(bound?: ScanBound): Record<string, unknown> | undefined {
274
+ if (!bound) {
275
+ return undefined;
276
+ }
277
+ if (bound.kind === "unbounded") {
278
+ return { kind: "unbounded" };
279
+ }
280
+ return { kind: bound.kind, key: bound.key.slice() };
281
+ }
282
+
283
+ function decodeEventBatch(raw: unknown): EventBatch {
284
+ if (!raw || typeof raw !== "object") {
285
+ return { source: "local", events: [] };
286
+ }
287
+ const batch = raw as RawEventBatch;
288
+ const source = typeof batch.source === "string" ? batch.source : "local";
289
+ const eventsRaw = Array.isArray(batch.events) ? batch.events : [];
290
+ const events = eventsRaw
291
+ .filter((entry): entry is RawEventEntry => Boolean(entry))
292
+ .map((entry) => buildEvent(entry));
293
+ return { source, events };
294
+ }
295
+
296
+ function buildEvent(entry: RawEventEntry): Event {
297
+ const key = Array.isArray(entry.key) ? entry.key : [];
298
+ const payload = buildEventPayload(entry);
299
+ return {
300
+ key,
301
+ value: payload.data,
302
+ metadata: cloneMetadata(payload.metadata),
303
+ payload,
304
+ };
305
+ }
306
+
307
+ function buildEventPayload(entry: RawEventEntry): EventPayload {
308
+ const base: ExportPayload = {};
309
+ if ("value" in entry) {
310
+ base.data = cloneJson(entry.value as Value);
311
+ }
312
+ const entryMetadata = cloneMetadata(entry.metadata);
313
+ if (entryMetadata !== undefined) {
314
+ base.metadata = entryMetadata;
315
+ }
316
+ const update = normalizeRawEventPayload(entry.payload);
317
+ return mergePayload(base, update);
318
+ }
319
+
320
+ function normalizeRawEventPayload(
321
+ payload: RawEventPayload | undefined,
322
+ ): ExportPayload | undefined {
323
+ if (!payload || typeof payload !== "object") {
324
+ return undefined;
325
+ }
326
+ const result: ExportPayload = {};
327
+ if ("data" in payload) {
328
+ result.data = cloneJson(payload.data as Value);
329
+ }
330
+ const metadata = cloneMetadata(payload.metadata);
331
+ if (metadata !== undefined) {
332
+ result.metadata = metadata;
333
+ }
334
+ return result;
335
+ }
336
+
337
+ const structuredCloneFn: (<T>(value: T) => T) | undefined =
338
+ (globalThis as typeof globalThis & { structuredClone?: <T>(value: T) => T }).structuredClone;
339
+
340
+ function cloneJson<T>(value: T): T {
341
+ if (value === undefined) {
342
+ return value;
343
+ }
344
+ if (structuredCloneFn) {
345
+ return structuredCloneFn(value);
346
+ }
347
+ return JSON.parse(JSON.stringify(value)) as T;
348
+ }
349
+
350
+ function parseKeyString(key: string): KeyPart[] {
351
+ try {
352
+ const parsed = JSON.parse(key);
353
+ return Array.isArray(parsed) ? (parsed as KeyPart[]) : [];
354
+ } catch {
355
+ return [];
356
+ }
357
+ }
358
+
359
+ function cloneMetadata(metadata: unknown): MetadataMap | undefined {
360
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
361
+ return undefined;
362
+ }
363
+ return cloneJson(metadata as MetadataMap);
364
+ }
365
+
366
+ function decodeClock(record: ExportRecord): EntryClock {
367
+ const [physicalRaw, logicalRaw, peerHexRaw] = record.c.split(",");
368
+ const physicalTime = Number(physicalRaw);
369
+ const logicalCounter = Number(logicalRaw);
370
+ const peerIdHex = peerHexRaw ?? "";
371
+ let peerId: Uint8Array;
372
+ try {
373
+ peerId = hexToBytes(peerIdHex);
374
+ } catch {
375
+ peerId = new Uint8Array();
376
+ }
377
+ return {
378
+ physicalTime: Number.isFinite(physicalTime) ? physicalTime : 0,
379
+ logicalCounter: Number.isFinite(logicalCounter)
380
+ ? Math.trunc(logicalCounter)
381
+ : 0,
382
+ peerIdHex,
383
+ peerId,
384
+ };
385
+ }
386
+
387
+ function createExportPayload(record: ExportRecord): ExportPayload {
388
+ const payload: ExportPayload = {};
389
+ if (record.d !== undefined) {
390
+ payload.data = cloneJson(record.d);
391
+ }
392
+ const metadata = cloneMetadata(record.m);
393
+ if (metadata !== undefined) {
394
+ payload.metadata = metadata;
395
+ }
396
+ return payload;
397
+ }
398
+
399
+ function createPutPayload(
400
+ value: Value,
401
+ metadata?: MetadataMap,
402
+ ): ExportPayload {
403
+ const payload: ExportPayload = { data: cloneJson(value) };
404
+ const cleanMetadata = cloneMetadata(metadata);
405
+ if (cleanMetadata !== undefined) {
406
+ payload.metadata = cleanMetadata;
407
+ }
408
+ return payload;
409
+ }
410
+
411
+ function assignPayload(
412
+ target: ExportPayload,
413
+ source?: ExportPayload | void,
414
+ ): void {
415
+ if (!source || typeof source !== "object") {
416
+ return;
417
+ }
418
+ if ("data" in source) {
419
+ const value = source.data;
420
+ target.data = value === undefined ? undefined : cloneJson(value);
421
+ }
422
+ if ("metadata" in source) {
423
+ target.metadata = cloneMetadata(source.metadata);
424
+ }
425
+ }
426
+
427
+ function clonePayload(payload: ExportPayload | undefined): ExportPayload {
428
+ const result: ExportPayload = {};
429
+ assignPayload(result, payload);
430
+ return result;
431
+ }
432
+
433
+ function mergePayload(
434
+ base: ExportPayload,
435
+ update?: ExportPayload | void,
436
+ ): ExportPayload {
437
+ const result = clonePayload(base);
438
+ assignPayload(result, update);
439
+ return result;
440
+ }
441
+
442
+ function buildRecord(clock: string, payload: ExportPayload): ExportRecord {
443
+ const record: ExportRecord = { c: clock };
444
+ if (payload.data !== undefined) {
445
+ record.d = cloneJson(payload.data);
446
+ }
447
+ const metadata = cloneMetadata(payload.metadata);
448
+ if (metadata !== undefined) {
449
+ record.m = metadata;
450
+ }
451
+ return record;
452
+ }
453
+
454
+ function cloneRecord(record: ExportRecord): ExportRecord {
455
+ return buildRecord(record.c, createExportPayload(record));
456
+ }
457
+
458
+ function buildContext(key: string, record: ExportRecord): ExportHookContext {
459
+ return {
460
+ key: parseKeyString(key),
461
+ clock: decodeClock(record),
462
+ raw: cloneRecord(record),
463
+ };
464
+ }
465
+
466
+ function normalizeImportDecision(
467
+ decision: ImportDecision,
468
+ ): ImportAccept | ImportSkip {
469
+ if (!decision || typeof decision !== "object") {
470
+ return { accept: true };
471
+ }
472
+ if ("accept" in decision) {
473
+ const typed = decision as ImportAccept | ImportSkip;
474
+ if (typed.accept === false) {
475
+ return { accept: false, reason: typed.reason ?? "rejected" };
476
+ }
477
+ return { accept: true };
478
+ }
479
+ return { accept: true };
480
+ }
481
+
482
+ function decodeImportReport(raw: unknown): ImportReport {
483
+ if (!raw || typeof raw !== "object") {
484
+ return { accepted: 0, skipped: [] };
485
+ }
486
+ const report = raw as RawImportReport;
487
+ const accepted = typeof report.accepted === "number" ? report.accepted : 0;
488
+ const skippedRaw = Array.isArray(report.skipped) ? report.skipped : [];
489
+ const skipped = skippedRaw.map((entry) => {
490
+ const key = entry && Array.isArray(entry.key) ? entry.key : [];
491
+ const reason =
492
+ entry && typeof entry.reason === "string" ? entry.reason : "unknown";
493
+ return { key, reason };
494
+ });
495
+ return { accepted, skipped };
496
+ }
497
+
498
+ function cloneBundle(bundle: ExportBundle): ExportBundle {
499
+ const next: ExportBundle = {};
500
+ for (const [key, record] of Object.entries(bundle)) {
501
+ next[key] = cloneRecord(record);
502
+ }
503
+ return next;
504
+ }
505
+
506
+ function isExportOptions(value: unknown): value is ExportOptions {
507
+ return (
508
+ typeof value === "object" &&
509
+ value !== null &&
510
+ (Object.prototype.hasOwnProperty.call(value, "hooks") ||
511
+ Object.prototype.hasOwnProperty.call(value, "from"))
512
+ );
513
+ }
514
+
515
+ function isImportOptions(value: unknown): value is ImportOptions {
516
+ return (
517
+ typeof value === "object" &&
518
+ value !== null &&
519
+ Object.prototype.hasOwnProperty.call(value, "bundle")
520
+ );
521
+ }
522
+
523
+ export class Flock {
524
+ private inner: ReturnType<typeof newFlock>;
525
+
526
+ constructor(peerId?: Uint8Array) {
527
+ this.inner = newFlock(normalizePeerId(peerId));
528
+ }
529
+
530
+ private static fromInner(inner: ReturnType<typeof newFlock>): Flock {
531
+ const flock = new Flock();
532
+ flock.inner = inner;
533
+ return flock;
534
+ }
535
+
536
+ static fromJson(bundle: ExportBundle, peerId: Uint8Array): Flock {
537
+ const inner = from_json_ffi(bundle, bytesToHex(normalizePeerId(peerId)));
538
+ return Flock.fromInner(inner as ReturnType<typeof newFlock>);
539
+ }
540
+
541
+ static checkConsistency(a: Flock, b: Flock): boolean {
542
+ return Boolean(check_consistency_ffi(a.inner, b.inner));
543
+ }
544
+
545
+ checkInvariants(): void {
546
+ check_invariants_ffi(this.inner);
547
+ }
548
+
549
+ setPeerId(peerId: Uint8Array): void {
550
+ set_peer_id(this.inner, normalizePeerId(peerId));
551
+ }
552
+
553
+ private putWithMetaInternal(
554
+ key: KeyPart[],
555
+ value: Value,
556
+ metadata?: MetadataMap,
557
+ now?: number,
558
+ ): void {
559
+ const clonedValue = cloneJson(value);
560
+ const metadataClone = cloneMetadata(metadata);
561
+ put_with_meta_ffi(this.inner, key, clonedValue, metadataClone, now);
562
+ }
563
+
564
+ private async putWithMetaWithHooks(
565
+ key: KeyPart[],
566
+ value: Value,
567
+ options: PutWithMetaOptions,
568
+ ): Promise<void> {
569
+ const basePayload = createPutPayload(value, options.metadata);
570
+ const transform = options.hooks?.transform;
571
+ if (!transform) {
572
+ this.putWithMetaInternal(key, value, options.metadata, options.now);
573
+ return;
574
+ }
575
+ await transform(
576
+ { key: key.slice(), now: options.now },
577
+ clonePayload(basePayload),
578
+ );
579
+ const finalValue = basePayload.data;
580
+ if (finalValue === undefined) {
581
+ throw new TypeError("putWithMeta requires a data value");
582
+ }
583
+ this.putWithMetaInternal(key, finalValue, basePayload.metadata, options.now);
584
+ }
585
+
586
+ put(key: KeyPart[], value: Value, now?: number): void {
587
+ put_json_ffi(this.inner, key, value, now);
588
+ }
589
+
590
+ putWithMeta(key: KeyPart[], value: Value, options?: PutWithMetaOptions): void | Promise<void> {
591
+ const opts = options ?? {};
592
+ if (opts.hooks?.transform) {
593
+ return this.putWithMetaWithHooks(key, value, opts);
594
+ }
595
+ this.putWithMetaInternal(key, value, opts.metadata, opts.now);
596
+ }
597
+
598
+ set(key: KeyPart[], value: Value, now?: number): void {
599
+ this.put(key, value, now);
600
+ }
601
+
602
+ delete(key: KeyPart[], now?: number): void {
603
+ delete_ffi(this.inner, key, now);
604
+ }
605
+
606
+ get(key: KeyPart[]): Value | undefined {
607
+ return get_ffi(this.inner, key) as Value | undefined;
608
+ }
609
+
610
+ merge(other: Flock): void {
611
+ merge(this.inner, other.inner);
612
+ }
613
+
614
+ version(): VersionVector {
615
+ return decodeVersionVector(version_ffi(this.inner));
616
+ }
617
+
618
+ private exportJsonInternal(from?: VersionVector): ExportBundle {
619
+ return export_json_ffi(this.inner, encodeVersionVector(from)) as ExportBundle;
620
+ }
621
+
622
+ private async exportJsonWithHooks(options: ExportOptions): Promise<ExportBundle> {
623
+ const base = this.exportJsonInternal(options.from);
624
+ const transform = options.hooks?.transform;
625
+ if (!transform) {
626
+ return base;
627
+ }
628
+ const result: ExportBundle = {};
629
+ for (const [key, record] of Object.entries(base)) {
630
+ const context = buildContext(key, record);
631
+ const basePayload = createExportPayload(record);
632
+ const transformed = await transform(context, clonePayload(basePayload));
633
+ const finalPayload = mergePayload(basePayload, transformed);
634
+ result[key] = buildRecord(record.c, finalPayload);
635
+ }
636
+ return result;
637
+ }
638
+
639
+ exportJson(): ExportBundle;
640
+ exportJson(from: VersionVector): ExportBundle;
641
+ exportJson(options: ExportOptions): Promise<ExportBundle>;
642
+ exportJson(
643
+ arg?: VersionVector | ExportOptions,
644
+ ): ExportBundle | Promise<ExportBundle> {
645
+ if (arg === undefined) {
646
+ return this.exportJsonInternal();
647
+ }
648
+ if (isExportOptions(arg)) {
649
+ return this.exportJsonWithHooks(arg);
650
+ }
651
+ return this.exportJsonInternal(arg);
652
+ }
653
+
654
+ private importJsonInternal(bundle: ExportBundle): ImportReport {
655
+ const report = import_json_ffi(this.inner, bundle) as RawImportReport | undefined;
656
+ return decodeImportReport(report);
657
+ }
658
+
659
+ private async importJsonWithHooks(options: ImportOptions): Promise<ImportReport> {
660
+ const preprocess = options.hooks?.preprocess;
661
+ const working = preprocess ? cloneBundle(options.bundle) : options.bundle;
662
+ const skippedByHooks: Array<{ key: KeyPart[]; reason: string }> = [];
663
+ if (preprocess) {
664
+ for (const key of Object.keys(working)) {
665
+ const record = working[key];
666
+ if (!record) {
667
+ continue;
668
+ }
669
+ const context = buildContext(key, record);
670
+ const basePayload = createExportPayload(record);
671
+ const decision = await preprocess(context, clonePayload(basePayload));
672
+ const normalized = normalizeImportDecision(decision);
673
+ if (!normalized.accept) {
674
+ skippedByHooks.push({ key: context.key, reason: normalized.reason });
675
+ delete working[key];
676
+ continue;
677
+ }
678
+ working[key] = buildRecord(record.c, basePayload);
679
+ }
680
+ }
681
+ const coreReport = this.importJsonInternal(working);
682
+ return {
683
+ accepted: coreReport.accepted,
684
+ skipped: skippedByHooks.concat(coreReport.skipped),
685
+ };
686
+ }
687
+
688
+ importJson(bundle: ExportBundle): ImportReport;
689
+ importJson(options: ImportOptions): Promise<ImportReport>;
690
+ importJson(
691
+ arg: ExportBundle | ImportOptions,
692
+ ): ImportReport | Promise<ImportReport> {
693
+ if (isImportOptions(arg)) {
694
+ return this.importJsonWithHooks(arg);
695
+ }
696
+ return this.importJsonInternal(arg);
697
+ }
698
+
699
+ getMaxPhysicalTime(): number {
700
+ return Number(get_max_physical_time_ffi(this.inner));
701
+ }
702
+
703
+ peerId(): Uint8Array {
704
+ const hex = peer_id_ffi(this.inner);
705
+ if (typeof hex !== "string") {
706
+ throw new TypeError("peerId ffi returned unexpected value");
707
+ }
708
+ return hexToBytes(hex);
709
+ }
710
+
711
+ digest(): string {
712
+ const hex = digest_hex_ffi(this.inner);
713
+ if (typeof hex !== "string") {
714
+ throw new TypeError("digest ffi returned unexpected value");
715
+ }
716
+ return hex;
717
+ }
718
+
719
+ kvToJson(): ExportBundle {
720
+ return kv_to_json_ffi(this.inner) as ExportBundle;
721
+ }
722
+
723
+ putMvr(key: KeyPart[], value: Value, now?: number): void {
724
+ put_mvr_ffi(this.inner, key, value, now);
725
+ }
726
+
727
+ getMvr(key: KeyPart[]): Value[] {
728
+ const raw = get_mvr_ffi(this.inner, key);
729
+ return Array.isArray(raw) ? (raw as Value[]) : [];
730
+ }
731
+
732
+ scan(options: ScanOptions = {}): ScanRow[] {
733
+ const start = encodeBound(options.start);
734
+ const end = encodeBound(options.end);
735
+ const prefix = options.prefix ? options.prefix.slice() : undefined;
736
+ const rows = scan_ffi(this.inner, start, end, prefix) as RawScanRow[] | undefined;
737
+ if (!Array.isArray(rows)) {
738
+ return [];
739
+ }
740
+ return rows
741
+ .filter((row): row is RawScanRow => Boolean(row))
742
+ .map((row) => ({
743
+ key: Array.isArray(row.key) ? row.key : [],
744
+ raw: row.raw,
745
+ value: row.value,
746
+ }));
747
+ }
748
+
749
+ subscribe(listener: (batch: EventBatch) => void): () => void {
750
+ const unsubscribe = subscribe_ffi(this.inner, (payload: unknown) => {
751
+ listener(decodeEventBatch(payload));
752
+ });
753
+ if (typeof unsubscribe !== "function") {
754
+ throw new TypeError("subscribe ffi did not return a function");
755
+ }
756
+ return unsubscribe as () => void;
757
+ }
758
+ }