loro-repo 0.5.2 → 0.5.3

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.d.ts CHANGED
@@ -17,6 +17,7 @@ declare class LoroRepo<Meta extends JsonObject = JsonObject> {
17
17
  private readonly flockHydrator;
18
18
  private readonly state;
19
19
  private readonly syncRunner;
20
+ private readonly metaPersister;
20
21
  private constructor();
21
22
  static create<Meta extends JsonObject = JsonObject>(options: LoroRepoOptions): Promise<LoroRepo<Meta>>;
22
23
  /**
@@ -85,7 +86,6 @@ declare class LoroRepo<Meta extends JsonObject = JsonObject> {
85
86
  listAssets(docId: string): Promise<RepoAssetMetadata[]>;
86
87
  ensureAsset(assetId: AssetId): Promise<AssetDownload>;
87
88
  gcAssets(options?: GarbageCollectionOptions): Promise<number>;
88
- private persistMeta;
89
89
  get destroyed(): boolean;
90
90
  destroy(): Promise<void>;
91
91
  }
package/dist/index.js CHANGED
@@ -65,7 +65,6 @@ var DocManager = class {
65
65
  docFrontierDebounceMs;
66
66
  getMetaFlock;
67
67
  eventBus;
68
- persistMeta;
69
68
  docs = /* @__PURE__ */ new Map();
70
69
  docSubscriptions = /* @__PURE__ */ new Map();
71
70
  docFrontierUpdates = /* @__PURE__ */ new Map();
@@ -75,7 +74,6 @@ var DocManager = class {
75
74
  this.docFrontierDebounceMs = options.docFrontierDebounceMs;
76
75
  this.getMetaFlock = options.getMetaFlock;
77
76
  this.eventBus = options.eventBus;
78
- this.persistMeta = options.persistMeta;
79
77
  }
80
78
  async openPersistedDoc(docId) {
81
79
  return await this.ensureDoc(docId);
@@ -136,16 +134,13 @@ var DocManager = class {
136
134
  ], f.counter);
137
135
  mutated = true;
138
136
  }
139
- if (mutated) {
140
- for (const [peer, counter] of existingFrontiers) {
141
- const docCounterEnd = vv.get(peer);
142
- if (docCounterEnd != null && docCounterEnd > counter) metaFlock.delete([
143
- "f",
144
- docId,
145
- peer
146
- ]);
147
- }
148
- await this.persistMeta();
137
+ if (mutated) for (const [peer, counter] of existingFrontiers) {
138
+ const docCounterEnd = vv.get(peer);
139
+ if (docCounterEnd != null && docCounterEnd > counter) metaFlock.delete([
140
+ "f",
141
+ docId,
142
+ peer
143
+ ]);
149
144
  }
150
145
  const by = this.eventBus.resolveEventBy(defaultBy);
151
146
  this.eventBus.emit({
@@ -499,12 +494,10 @@ function matchesQuery(docId, _metadata, query) {
499
494
  var MetadataManager = class {
500
495
  getMetaFlock;
501
496
  eventBus;
502
- persistMeta;
503
497
  state;
504
498
  constructor(options) {
505
499
  this.getMetaFlock = options.getMetaFlock;
506
500
  this.eventBus = options.eventBus;
507
- this.persistMeta = options.persistMeta;
508
501
  this.state = options.state;
509
502
  }
510
503
  getDocIds() {
@@ -561,7 +554,6 @@ var MetadataManager = class {
561
554
  if (rawValue === void 0) continue;
562
555
  if (jsonEquals(base ? base[key] : void 0, rawValue)) continue;
563
556
  const storageKey = key === "tombstone" ? "$tombstone" : key;
564
- console.log("upserting", rawValue);
565
557
  this.metaFlock.put([
566
558
  "m",
567
559
  docId,
@@ -576,7 +568,6 @@ var MetadataManager = class {
576
568
  return;
577
569
  }
578
570
  this.state.metadata.set(docId, next);
579
- await this.persistMeta();
580
571
  this.eventBus.emit({
581
572
  kind: "doc-metadata",
582
573
  docId,
@@ -702,7 +693,6 @@ var AssetManager = class {
702
693
  assetTransport;
703
694
  getMetaFlock;
704
695
  eventBus;
705
- persistMeta;
706
696
  state;
707
697
  get docAssets() {
708
698
  return this.state.docAssets;
@@ -721,7 +711,6 @@ var AssetManager = class {
721
711
  this.assetTransport = options.assetTransport;
722
712
  this.getMetaFlock = options.getMetaFlock;
723
713
  this.eventBus = options.eventBus;
724
- this.persistMeta = options.persistMeta;
725
714
  this.state = options.state;
726
715
  }
727
716
  async uploadAsset(params) {
@@ -758,7 +747,6 @@ var AssetManager = class {
758
747
  if (metadataMutated) {
759
748
  existing.metadata = metadata$1;
760
749
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata$1));
761
- await this.persistMeta();
762
750
  this.eventBus.emit({
763
751
  kind: "asset-metadata",
764
752
  asset: this.createAssetDownload(assetId, metadata$1, bytes),
@@ -795,7 +783,6 @@ var AssetManager = class {
795
783
  this.markAssetAsOrphan(assetId, metadata);
796
784
  this.updateDocAssetMetadata(assetId, metadata);
797
785
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
798
- await this.persistMeta();
799
786
  this.eventBus.emit({
800
787
  kind: "asset-metadata",
801
788
  asset: this.createAssetDownload(assetId, metadata, storedBytes),
@@ -854,7 +841,6 @@ var AssetManager = class {
854
841
  existing.metadata = nextMetadata;
855
842
  metadata = nextMetadata;
856
843
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
857
- await this.persistMeta();
858
844
  this.eventBus.emit({
859
845
  kind: "asset-metadata",
860
846
  asset: this.createAssetDownload(assetId, metadata, bytes),
@@ -901,7 +887,6 @@ var AssetManager = class {
901
887
  docId,
902
888
  assetId
903
889
  ], true);
904
- await this.persistMeta();
905
890
  this.eventBus.emit({
906
891
  kind: "asset-link",
907
892
  docId,
@@ -926,7 +911,6 @@ var AssetManager = class {
926
911
  assetId
927
912
  ]);
928
913
  this.removeDocAssetReference(assetId, docId);
929
- await this.persistMeta();
930
914
  this.eventBus.emit({
931
915
  kind: "asset-unlink",
932
916
  docId,
@@ -1210,7 +1194,6 @@ var AssetManager = class {
1210
1194
  this.assets.set(assetId, { metadata });
1211
1195
  this.updateDocAssetMetadata(assetId, metadata);
1212
1196
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
1213
- await this.persistMeta();
1214
1197
  if (this.storage) await this.storage.save({
1215
1198
  type: "asset",
1216
1199
  assetId,
@@ -1366,7 +1349,6 @@ var SyncRunner = class {
1366
1349
  flockHydrator;
1367
1350
  getMetaFlock;
1368
1351
  replaceMetaFlock;
1369
- persistMeta;
1370
1352
  readyPromise;
1371
1353
  metaRoomSubscription;
1372
1354
  unsubscribeMetaFlock;
@@ -1381,7 +1363,6 @@ var SyncRunner = class {
1381
1363
  this.flockHydrator = options.flockHydrator;
1382
1364
  this.getMetaFlock = options.getMetaFlock;
1383
1365
  this.replaceMetaFlock = options.mergeFlock;
1384
- this.persistMeta = options.persistMeta;
1385
1366
  }
1386
1367
  async ready() {
1387
1368
  if (!this.readyPromise) this.readyPromise = this.initialize();
@@ -1403,7 +1384,6 @@ var SyncRunner = class {
1403
1384
  if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
1404
1385
  if (recordedEvents.length > 0) this.flockHydrator.applyEvents(recordedEvents, "sync");
1405
1386
  else this.flockHydrator.hydrateAll("sync");
1406
- await this.persistMeta();
1407
1387
  } finally {
1408
1388
  unsubscribe();
1409
1389
  this.eventBus.popEventBy();
@@ -1449,7 +1429,6 @@ var SyncRunner = class {
1449
1429
  subscription.firstSyncedWithRemote.then(async () => {
1450
1430
  const by = this.eventBus.resolveEventBy("live");
1451
1431
  this.flockHydrator.hydrateAll(by);
1452
- await this.persistMeta();
1453
1432
  }).catch(logAsyncError("meta room first sync"));
1454
1433
  return wrapped;
1455
1434
  }
@@ -1505,7 +1484,6 @@ var SyncRunner = class {
1505
1484
  const by = this.eventBus.resolveEventBy("live");
1506
1485
  (async () => {
1507
1486
  this.flockHydrator.applyEvents(batch.events, by);
1508
- await this.persistMeta();
1509
1487
  })().catch(logAsyncError("meta live monitor sync"));
1510
1488
  });
1511
1489
  }
@@ -1527,8 +1505,154 @@ function createRepoState() {
1527
1505
  }
1528
1506
 
1529
1507
  //#endregion
1530
- //#region src/index.ts
1508
+ //#region src/internal/meta-persister.ts
1531
1509
  const textEncoder = new TextEncoder();
1510
+ const DEFAULT_META_PERSIST_DEBOUNCE_MS = 5e3;
1511
+ var MetaPersister = class {
1512
+ getMetaFlock;
1513
+ storage;
1514
+ debounceMs;
1515
+ lastPersistedVersion;
1516
+ unsubscribe;
1517
+ flushPromise = Promise.resolve();
1518
+ flushTimer;
1519
+ forceFullOnNextFlush = false;
1520
+ destroyed = false;
1521
+ constructor(options) {
1522
+ this.getMetaFlock = options.getMetaFlock;
1523
+ this.storage = options.storage;
1524
+ const configuredDebounce = options.debounceMs;
1525
+ this.debounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_META_PERSIST_DEBOUNCE_MS;
1526
+ }
1527
+ start(initialVersion) {
1528
+ this.lastPersistedVersion = initialVersion;
1529
+ if (this.unsubscribe) return;
1530
+ this.unsubscribe = this.metaFlock.subscribe(() => {
1531
+ this.scheduleFlush();
1532
+ });
1533
+ }
1534
+ async destroy() {
1535
+ this.destroyed = true;
1536
+ if (this.flushTimer) {
1537
+ clearTimeout(this.flushTimer);
1538
+ this.flushTimer = void 0;
1539
+ }
1540
+ if (this.unsubscribe) {
1541
+ this.unsubscribe();
1542
+ this.unsubscribe = void 0;
1543
+ }
1544
+ await this.flushNow();
1545
+ }
1546
+ async flushNow(forceFull = false) {
1547
+ if (this.flushTimer) {
1548
+ clearTimeout(this.flushTimer);
1549
+ this.flushTimer = void 0;
1550
+ }
1551
+ await this.flush(forceFull);
1552
+ }
1553
+ scheduleFlush() {
1554
+ if (this.destroyed) return;
1555
+ if (this.debounceMs === 0) {
1556
+ this.flush();
1557
+ return;
1558
+ }
1559
+ if (this.flushTimer) clearTimeout(this.flushTimer);
1560
+ this.flushTimer = setTimeout(() => {
1561
+ this.flushTimer = void 0;
1562
+ this.flush();
1563
+ }, this.debounceMs);
1564
+ }
1565
+ async flush(forceFull = false) {
1566
+ if (forceFull) this.forceFullOnNextFlush = true;
1567
+ const run = this.flushPromise.catch(() => {}).then(() => this.flushInternal());
1568
+ this.flushPromise = run;
1569
+ await run;
1570
+ }
1571
+ async flushInternal() {
1572
+ const flock = this.metaFlock;
1573
+ const currentVersion = flock.version();
1574
+ if (this.lastPersistedVersion && this.versionsEqual(currentVersion, this.lastPersistedVersion)) {
1575
+ this.forceFullOnNextFlush = false;
1576
+ return;
1577
+ }
1578
+ const baseline = this.forceFullOnNextFlush ? void 0 : this.lastPersistedVersion;
1579
+ const rawBundle = baseline ? flock.exportJson(baseline) : flock.exportJson();
1580
+ const bundle = baseline ? this.stripUnchangedEntries(rawBundle, baseline) : rawBundle;
1581
+ if (Object.keys(bundle.entries).length === 0) {
1582
+ this.forceFullOnNextFlush = false;
1583
+ this.lastPersistedVersion = currentVersion;
1584
+ return;
1585
+ }
1586
+ const encoded = textEncoder.encode(JSON.stringify(bundle));
1587
+ if (!this.storage) {
1588
+ this.lastPersistedVersion = currentVersion;
1589
+ this.forceFullOnNextFlush = false;
1590
+ return;
1591
+ }
1592
+ try {
1593
+ await this.storage.save({
1594
+ type: "meta",
1595
+ update: encoded
1596
+ });
1597
+ } catch (error) {
1598
+ throw error;
1599
+ }
1600
+ this.lastPersistedVersion = currentVersion;
1601
+ this.forceFullOnNextFlush = false;
1602
+ }
1603
+ get metaFlock() {
1604
+ return this.getMetaFlock();
1605
+ }
1606
+ stripUnchangedEntries(bundle, baseline) {
1607
+ const entries = {};
1608
+ for (const [key, record] of Object.entries(bundle.entries)) {
1609
+ const clock = this.parseClock(record.c);
1610
+ if (!clock) {
1611
+ entries[key] = record;
1612
+ continue;
1613
+ }
1614
+ const baselineEntry = baseline[clock.peerIdHex];
1615
+ if (!baselineEntry) {
1616
+ entries[key] = record;
1617
+ continue;
1618
+ }
1619
+ if (clock.physicalTime > baselineEntry.physicalTime || clock.physicalTime === baselineEntry.physicalTime && clock.logicalCounter > baselineEntry.logicalCounter) entries[key] = record;
1620
+ }
1621
+ return {
1622
+ version: bundle.version,
1623
+ entries
1624
+ };
1625
+ }
1626
+ parseClock(raw) {
1627
+ if (typeof raw !== "string") return void 0;
1628
+ const [physicalTimeStr, logicalCounterStr, peerIdHex] = raw.split(",");
1629
+ if (!physicalTimeStr || !logicalCounterStr || !peerIdHex) return void 0;
1630
+ const physicalTime = Number(physicalTimeStr);
1631
+ const logicalCounter = Number(logicalCounterStr);
1632
+ if (!Number.isFinite(physicalTime) || !Number.isFinite(logicalCounter)) return void 0;
1633
+ return {
1634
+ physicalTime,
1635
+ logicalCounter,
1636
+ peerIdHex
1637
+ };
1638
+ }
1639
+ versionsEqual(a, b) {
1640
+ if (!a || !b) return false;
1641
+ const aKeys = Object.keys(a);
1642
+ const bKeys = Object.keys(b);
1643
+ if (aKeys.length !== bKeys.length) return false;
1644
+ for (const key of aKeys) {
1645
+ const aEntry = a[key];
1646
+ const bEntry = b[key];
1647
+ if (!aEntry || !bEntry) return false;
1648
+ if (aEntry.physicalTime !== bEntry.physicalTime || aEntry.logicalCounter !== bEntry.logicalCounter) return false;
1649
+ }
1650
+ return true;
1651
+ }
1652
+ };
1653
+
1654
+ //#endregion
1655
+ //#region src/index.ts
1532
1656
  const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
1533
1657
  var LoroRepo = class LoroRepo {
1534
1658
  options;
@@ -1544,6 +1668,7 @@ var LoroRepo = class LoroRepo {
1544
1668
  flockHydrator;
1545
1669
  state;
1546
1670
  syncRunner;
1671
+ metaPersister;
1547
1672
  constructor(options) {
1548
1673
  this.options = options;
1549
1674
  this.transport = options.transportAdapter;
@@ -1557,13 +1682,11 @@ var LoroRepo = class LoroRepo {
1557
1682
  storage: this.storage,
1558
1683
  docFrontierDebounceMs,
1559
1684
  getMetaFlock: () => this.metaFlock,
1560
- eventBus: this.eventBus,
1561
- persistMeta: () => this.persistMeta()
1685
+ eventBus: this.eventBus
1562
1686
  });
1563
1687
  this.metadataManager = new MetadataManager({
1564
1688
  getMetaFlock: () => this.metaFlock,
1565
1689
  eventBus: this.eventBus,
1566
- persistMeta: () => this.persistMeta(),
1567
1690
  state: this.state
1568
1691
  });
1569
1692
  this.assetManager = new AssetManager({
@@ -1571,9 +1694,13 @@ var LoroRepo = class LoroRepo {
1571
1694
  assetTransport: this.assetTransport,
1572
1695
  getMetaFlock: () => this.metaFlock,
1573
1696
  eventBus: this.eventBus,
1574
- persistMeta: () => this.persistMeta(),
1575
1697
  state: this.state
1576
1698
  });
1699
+ this.metaPersister = new MetaPersister({
1700
+ getMetaFlock: () => this.metaFlock,
1701
+ storage: this.storage,
1702
+ debounceMs: options.metaPersistDebounceMs
1703
+ });
1577
1704
  this.flockHydrator = new FlockHydrator({
1578
1705
  getMetaFlock: () => this.metaFlock,
1579
1706
  metadataManager: this.metadataManager,
@@ -1591,8 +1718,7 @@ var LoroRepo = class LoroRepo {
1591
1718
  getMetaFlock: () => this.metaFlock,
1592
1719
  mergeFlock: (snapshot) => {
1593
1720
  this.metaFlock.merge(snapshot);
1594
- },
1595
- persistMeta: () => this.persistMeta()
1721
+ }
1596
1722
  });
1597
1723
  }
1598
1724
  static async create(options) {
@@ -1609,6 +1735,7 @@ var LoroRepo = class LoroRepo {
1609
1735
  */
1610
1736
  async ready() {
1611
1737
  await this.syncRunner.ready();
1738
+ this.metaPersister.start(this.metaFlock.version());
1612
1739
  }
1613
1740
  /**
1614
1741
  * Sync selected data via the transport adaptor
@@ -1624,7 +1751,16 @@ var LoroRepo = class LoroRepo {
1624
1751
  * @returns
1625
1752
  */
1626
1753
  async joinMetaRoom(params) {
1627
- return this.syncRunner.joinMetaRoom(params);
1754
+ const subscription = await this.syncRunner.joinMetaRoom(params);
1755
+ return {
1756
+ unsubscribe: subscription.unsubscribe,
1757
+ get connected() {
1758
+ return subscription.connected;
1759
+ },
1760
+ firstSyncedWithRemote: subscription.firstSyncedWithRemote.then(async () => {
1761
+ await this.metaPersister.flushNow();
1762
+ })
1763
+ };
1628
1764
  }
1629
1765
  /**
1630
1766
  * Start syncing the given doc. It will establish a realtime connection to the transport adaptor.
@@ -1696,6 +1832,7 @@ var LoroRepo = class LoroRepo {
1696
1832
  }
1697
1833
  async flush() {
1698
1834
  await this.docManager.flush();
1835
+ await this.metaPersister.flushNow();
1699
1836
  }
1700
1837
  async uploadAsset(params) {
1701
1838
  return this.assetManager.uploadAsset(params);
@@ -1718,21 +1855,13 @@ var LoroRepo = class LoroRepo {
1718
1855
  async gcAssets(options = {}) {
1719
1856
  return this.assetManager.gcAssets(options);
1720
1857
  }
1721
- async persistMeta() {
1722
- if (!this.storage) return;
1723
- const bundle = this.metaFlock.exportJson();
1724
- const encoded = textEncoder.encode(JSON.stringify(bundle));
1725
- await this.storage.save({
1726
- type: "meta",
1727
- update: encoded
1728
- });
1729
- }
1730
1858
  get destroyed() {
1731
1859
  return this._destroyed;
1732
1860
  }
1733
1861
  async destroy() {
1734
1862
  if (this._destroyed) return;
1735
1863
  this._destroyed = true;
1864
+ await this.metaPersister.destroy();
1736
1865
  await this.syncRunner.destroy();
1737
1866
  this.assetTransport?.close?.();
1738
1867
  this.storage?.close?.();