@loro-dev/flock 2.1.2 → 3.1.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
@@ -39,6 +39,7 @@ type ExportOptions = {
39
39
  from?: VersionVector;
40
40
  hooks?: ExportHooks;
41
41
  pruneTombstonesBefore?: number;
42
+ peerId?: string;
42
43
  };
43
44
 
44
45
  type ImportOptions = {
@@ -81,8 +82,7 @@ export type ExportBundle = { version: number, entries: Record<string, ExportReco
81
82
  export type EntryClock = {
82
83
  physicalTime: number;
83
84
  logicalCounter: number;
84
- peerIdHex: string;
85
- peerId: Uint8Array;
85
+ peerId: string;
86
86
  };
87
87
 
88
88
  export type ExportPayload = {
@@ -181,30 +181,17 @@ export type EventBatch = {
181
181
  events: Event[];
182
182
  };
183
183
 
184
- function bytesToHex(bytes: Uint8Array): string {
185
- let hex = "";
186
- for (let i = 0; i < bytes.length; i += 1) {
187
- hex += bytes[i].toString(16).padStart(2, "0");
188
- }
189
- return hex;
184
+ const textEncoder = new TextEncoder();
185
+
186
+ function utf8ByteLength(value: string): number {
187
+ return textEncoder.encode(value).length;
190
188
  }
191
189
 
192
- function hexToBytes(hex: string): Uint8Array {
193
- if (hex.length % 2 !== 0) {
194
- throw new TypeError("peerId hex must have even length");
195
- }
196
- const bytes = new Uint8Array(hex.length / 2);
197
- for (let i = 0; i < bytes.length; i += 1) {
198
- const byte = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
199
- if (Number.isNaN(byte)) {
200
- throw new TypeError("peerId hex contains invalid characters");
201
- }
202
- bytes[i] = byte;
203
- }
204
- return bytes;
190
+ function isValidPeerId(peerId: unknown): peerId is string {
191
+ return typeof peerId === "string" && utf8ByteLength(peerId) < 128;
205
192
  }
206
193
 
207
- function createRandomPeerId(): Uint8Array {
194
+ function createRandomPeerId(): string {
208
195
  const id = new Uint8Array(32);
209
196
  if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
210
197
  crypto.getRandomValues(id);
@@ -213,17 +200,17 @@ function createRandomPeerId(): Uint8Array {
213
200
  id[i] = Math.floor(Math.random() * 256);
214
201
  }
215
202
  }
216
- return id;
203
+ return Array.from(id, (byte) => byte.toString(16).padStart(2, "0")).join("");
217
204
  }
218
205
 
219
- function normalizePeerId(peerId?: Uint8Array): Uint8Array {
206
+ function normalizePeerId(peerId?: string): string {
220
207
  if (peerId === undefined) {
221
208
  return createRandomPeerId();
222
209
  }
223
- if (!(peerId instanceof Uint8Array)) {
224
- throw new TypeError("peerId must be a Uint8Array");
210
+ if (!isValidPeerId(peerId)) {
211
+ throw new TypeError("peerId must be a UTF-8 string under 128 bytes");
225
212
  }
226
- return new Uint8Array(peerId);
213
+ return peerId;
227
214
  }
228
215
 
229
216
  function encodeVersionVector(vv?: VersionVector): RawVersionVector | undefined {
@@ -235,6 +222,9 @@ function encodeVersionVector(vv?: VersionVector): RawVersionVector | undefined {
235
222
  if (!entry) {
236
223
  continue;
237
224
  }
225
+ if (!isValidPeerId(peer)) {
226
+ continue;
227
+ }
238
228
  const { physicalTime, logicalCounter } = entry;
239
229
  if (typeof physicalTime !== "number" || Number.isNaN(physicalTime)) {
240
230
  continue;
@@ -266,6 +256,9 @@ function decodeVersionVector(raw: unknown): VersionVector {
266
256
  if (!Array.isArray(value) || value.length < 2) {
267
257
  continue;
268
258
  }
259
+ if (!isValidPeerId(peer)) {
260
+ continue;
261
+ }
269
262
  const [physicalTime, logicalCounter] = value;
270
263
  if (typeof physicalTime !== "number" || !Number.isFinite(physicalTime)) {
271
264
  continue;
@@ -375,22 +368,21 @@ function cloneMetadata(metadata: unknown): MetadataMap | undefined {
375
368
  }
376
369
 
377
370
  function decodeClock(record: ExportRecord): EntryClock {
378
- const [physicalRaw, logicalRaw, peerHexRaw] = record.c.split(",");
379
- const physicalTime = Number(physicalRaw);
380
- const logicalCounter = Number(logicalRaw);
381
- const peerIdHex = peerHexRaw ?? "";
382
- let peerId: Uint8Array;
383
- try {
384
- peerId = hexToBytes(peerIdHex);
385
- } catch {
386
- peerId = new Uint8Array();
387
- }
371
+ const rawClock = typeof record.c === "string" ? record.c : "";
372
+ const firstComma = rawClock.indexOf(",");
373
+ const secondComma = firstComma === -1 ? -1 : rawClock.indexOf(",", firstComma + 1);
374
+ if (firstComma === -1 || secondComma === -1) {
375
+ return { physicalTime: 0, logicalCounter: 0, peerId: "" };
376
+ }
377
+ const physicalTime = Number(rawClock.slice(0, firstComma));
378
+ const logicalCounter = Number(rawClock.slice(firstComma + 1, secondComma));
379
+ const peerIdRaw = rawClock.slice(secondComma + 1);
380
+ const peerId = isValidPeerId(peerIdRaw) ? peerIdRaw : "";
388
381
  return {
389
382
  physicalTime: Number.isFinite(physicalTime) ? physicalTime : 0,
390
383
  logicalCounter: Number.isFinite(logicalCounter)
391
384
  ? Math.trunc(logicalCounter)
392
385
  : 0,
393
- peerIdHex,
394
386
  peerId,
395
387
  };
396
388
  }
@@ -522,7 +514,8 @@ function isExportOptions(value: unknown): value is ExportOptions {
522
514
  Object.prototype.hasOwnProperty.call(
523
515
  value,
524
516
  "pruneTombstonesBefore",
525
- ))
517
+ ) ||
518
+ Object.prototype.hasOwnProperty.call(value, "peerId"))
526
519
  );
527
520
  }
528
521
 
@@ -537,7 +530,7 @@ function isImportOptions(value: unknown): value is ImportOptions {
537
530
  export class Flock {
538
531
  private inner: ReturnType<typeof newFlock>;
539
532
 
540
- constructor(peerId?: Uint8Array) {
533
+ constructor(peerId?: string) {
541
534
  this.inner = newFlock(normalizePeerId(peerId));
542
535
  }
543
536
 
@@ -547,8 +540,8 @@ export class Flock {
547
540
  return flock;
548
541
  }
549
542
 
550
- static fromJson(bundle: ExportBundle, peerId: Uint8Array): Flock {
551
- const inner = from_json_ffi(bundle, bytesToHex(normalizePeerId(peerId)));
543
+ static fromJson(bundle: ExportBundle, peerId: string): Flock {
544
+ const inner = from_json_ffi(bundle, normalizePeerId(peerId));
552
545
  return Flock.fromInner(inner as ReturnType<typeof newFlock>);
553
546
  }
554
547
 
@@ -560,7 +553,7 @@ export class Flock {
560
553
  check_invariants_ffi(this.inner);
561
554
  }
562
555
 
563
- setPeerId(peerId: Uint8Array): void {
556
+ setPeerId(peerId: string): void {
564
557
  set_peer_id(this.inner, normalizePeerId(peerId));
565
558
  }
566
559
 
@@ -652,12 +645,15 @@ export class Flock {
652
645
  private exportJsonInternal(
653
646
  from?: VersionVector,
654
647
  pruneTombstonesBefore?: number,
648
+ peerId?: string,
655
649
  ): ExportBundle {
656
650
  const pruneBefore = normalizePruneBefore(pruneTombstonesBefore);
651
+ const normalizedPeerId = peerId !== undefined && isValidPeerId(peerId) ? peerId : undefined;
657
652
  return export_json_ffi(
658
653
  this.inner,
659
654
  encodeVersionVector(from),
660
655
  pruneBefore,
656
+ normalizedPeerId,
661
657
  ) as ExportBundle;
662
658
  }
663
659
 
@@ -665,6 +661,7 @@ export class Flock {
665
661
  const base = this.exportJsonInternal(
666
662
  options.from,
667
663
  options.pruneTombstonesBefore,
664
+ options.peerId,
668
665
  );
669
666
  const transform = options.hooks?.transform;
670
667
  if (!transform) {
@@ -754,12 +751,15 @@ export class Flock {
754
751
  return Number(get_max_physical_time_ffi(this.inner));
755
752
  }
756
753
 
757
- peerId(): Uint8Array {
758
- const hex = peer_id_ffi(this.inner);
759
- if (typeof hex !== "string") {
754
+ peerId(): string {
755
+ const id = peer_id_ffi(this.inner);
756
+ if (typeof id !== "string") {
760
757
  throw new TypeError("peerId ffi returned unexpected value");
761
758
  }
762
- return hexToBytes(hex);
759
+ if (!isValidPeerId(id)) {
760
+ throw new TypeError("peerId ffi returned an invalid string");
761
+ }
762
+ return id;
763
763
  }
764
764
 
765
765
  digest(): string {