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.cjs CHANGED
@@ -68,7 +68,6 @@ var DocManager = class {
68
68
  docFrontierDebounceMs;
69
69
  getMetaFlock;
70
70
  eventBus;
71
- persistMeta;
72
71
  docs = /* @__PURE__ */ new Map();
73
72
  docSubscriptions = /* @__PURE__ */ new Map();
74
73
  docFrontierUpdates = /* @__PURE__ */ new Map();
@@ -78,7 +77,6 @@ var DocManager = class {
78
77
  this.docFrontierDebounceMs = options.docFrontierDebounceMs;
79
78
  this.getMetaFlock = options.getMetaFlock;
80
79
  this.eventBus = options.eventBus;
81
- this.persistMeta = options.persistMeta;
82
80
  }
83
81
  async openPersistedDoc(docId) {
84
82
  return await this.ensureDoc(docId);
@@ -139,16 +137,13 @@ var DocManager = class {
139
137
  ], f.counter);
140
138
  mutated = true;
141
139
  }
142
- if (mutated) {
143
- for (const [peer, counter] of existingFrontiers) {
144
- const docCounterEnd = vv.get(peer);
145
- if (docCounterEnd != null && docCounterEnd > counter) metaFlock.delete([
146
- "f",
147
- docId,
148
- peer
149
- ]);
150
- }
151
- await this.persistMeta();
140
+ if (mutated) 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
+ ]);
152
147
  }
153
148
  const by = this.eventBus.resolveEventBy(defaultBy);
154
149
  this.eventBus.emit({
@@ -502,12 +497,10 @@ function matchesQuery(docId, _metadata, query) {
502
497
  var MetadataManager = class {
503
498
  getMetaFlock;
504
499
  eventBus;
505
- persistMeta;
506
500
  state;
507
501
  constructor(options) {
508
502
  this.getMetaFlock = options.getMetaFlock;
509
503
  this.eventBus = options.eventBus;
510
- this.persistMeta = options.persistMeta;
511
504
  this.state = options.state;
512
505
  }
513
506
  getDocIds() {
@@ -564,7 +557,6 @@ var MetadataManager = class {
564
557
  if (rawValue === void 0) continue;
565
558
  if (jsonEquals(base ? base[key] : void 0, rawValue)) continue;
566
559
  const storageKey = key === "tombstone" ? "$tombstone" : key;
567
- console.log("upserting", rawValue);
568
560
  this.metaFlock.put([
569
561
  "m",
570
562
  docId,
@@ -579,7 +571,6 @@ var MetadataManager = class {
579
571
  return;
580
572
  }
581
573
  this.state.metadata.set(docId, next);
582
- await this.persistMeta();
583
574
  this.eventBus.emit({
584
575
  kind: "doc-metadata",
585
576
  docId,
@@ -705,7 +696,6 @@ var AssetManager = class {
705
696
  assetTransport;
706
697
  getMetaFlock;
707
698
  eventBus;
708
- persistMeta;
709
699
  state;
710
700
  get docAssets() {
711
701
  return this.state.docAssets;
@@ -724,7 +714,6 @@ var AssetManager = class {
724
714
  this.assetTransport = options.assetTransport;
725
715
  this.getMetaFlock = options.getMetaFlock;
726
716
  this.eventBus = options.eventBus;
727
- this.persistMeta = options.persistMeta;
728
717
  this.state = options.state;
729
718
  }
730
719
  async uploadAsset(params) {
@@ -761,7 +750,6 @@ var AssetManager = class {
761
750
  if (metadataMutated) {
762
751
  existing.metadata = metadata$1;
763
752
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata$1));
764
- await this.persistMeta();
765
753
  this.eventBus.emit({
766
754
  kind: "asset-metadata",
767
755
  asset: this.createAssetDownload(assetId, metadata$1, bytes),
@@ -798,7 +786,6 @@ var AssetManager = class {
798
786
  this.markAssetAsOrphan(assetId, metadata);
799
787
  this.updateDocAssetMetadata(assetId, metadata);
800
788
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
801
- await this.persistMeta();
802
789
  this.eventBus.emit({
803
790
  kind: "asset-metadata",
804
791
  asset: this.createAssetDownload(assetId, metadata, storedBytes),
@@ -857,7 +844,6 @@ var AssetManager = class {
857
844
  existing.metadata = nextMetadata;
858
845
  metadata = nextMetadata;
859
846
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
860
- await this.persistMeta();
861
847
  this.eventBus.emit({
862
848
  kind: "asset-metadata",
863
849
  asset: this.createAssetDownload(assetId, metadata, bytes),
@@ -904,7 +890,6 @@ var AssetManager = class {
904
890
  docId,
905
891
  assetId
906
892
  ], true);
907
- await this.persistMeta();
908
893
  this.eventBus.emit({
909
894
  kind: "asset-link",
910
895
  docId,
@@ -929,7 +914,6 @@ var AssetManager = class {
929
914
  assetId
930
915
  ]);
931
916
  this.removeDocAssetReference(assetId, docId);
932
- await this.persistMeta();
933
917
  this.eventBus.emit({
934
918
  kind: "asset-unlink",
935
919
  docId,
@@ -1213,7 +1197,6 @@ var AssetManager = class {
1213
1197
  this.assets.set(assetId, { metadata });
1214
1198
  this.updateDocAssetMetadata(assetId, metadata);
1215
1199
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
1216
- await this.persistMeta();
1217
1200
  if (this.storage) await this.storage.save({
1218
1201
  type: "asset",
1219
1202
  assetId,
@@ -1369,7 +1352,6 @@ var SyncRunner = class {
1369
1352
  flockHydrator;
1370
1353
  getMetaFlock;
1371
1354
  replaceMetaFlock;
1372
- persistMeta;
1373
1355
  readyPromise;
1374
1356
  metaRoomSubscription;
1375
1357
  unsubscribeMetaFlock;
@@ -1384,7 +1366,6 @@ var SyncRunner = class {
1384
1366
  this.flockHydrator = options.flockHydrator;
1385
1367
  this.getMetaFlock = options.getMetaFlock;
1386
1368
  this.replaceMetaFlock = options.mergeFlock;
1387
- this.persistMeta = options.persistMeta;
1388
1369
  }
1389
1370
  async ready() {
1390
1371
  if (!this.readyPromise) this.readyPromise = this.initialize();
@@ -1406,7 +1387,6 @@ var SyncRunner = class {
1406
1387
  if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
1407
1388
  if (recordedEvents.length > 0) this.flockHydrator.applyEvents(recordedEvents, "sync");
1408
1389
  else this.flockHydrator.hydrateAll("sync");
1409
- await this.persistMeta();
1410
1390
  } finally {
1411
1391
  unsubscribe();
1412
1392
  this.eventBus.popEventBy();
@@ -1452,7 +1432,6 @@ var SyncRunner = class {
1452
1432
  subscription.firstSyncedWithRemote.then(async () => {
1453
1433
  const by = this.eventBus.resolveEventBy("live");
1454
1434
  this.flockHydrator.hydrateAll(by);
1455
- await this.persistMeta();
1456
1435
  }).catch(logAsyncError("meta room first sync"));
1457
1436
  return wrapped;
1458
1437
  }
@@ -1508,7 +1487,6 @@ var SyncRunner = class {
1508
1487
  const by = this.eventBus.resolveEventBy("live");
1509
1488
  (async () => {
1510
1489
  this.flockHydrator.applyEvents(batch.events, by);
1511
- await this.persistMeta();
1512
1490
  })().catch(logAsyncError("meta live monitor sync"));
1513
1491
  });
1514
1492
  }
@@ -1530,8 +1508,154 @@ function createRepoState() {
1530
1508
  }
1531
1509
 
1532
1510
  //#endregion
1533
- //#region src/index.ts
1511
+ //#region src/internal/meta-persister.ts
1534
1512
  const textEncoder = new TextEncoder();
1513
+ const DEFAULT_META_PERSIST_DEBOUNCE_MS = 5e3;
1514
+ var MetaPersister = class {
1515
+ getMetaFlock;
1516
+ storage;
1517
+ debounceMs;
1518
+ lastPersistedVersion;
1519
+ unsubscribe;
1520
+ flushPromise = Promise.resolve();
1521
+ flushTimer;
1522
+ forceFullOnNextFlush = false;
1523
+ destroyed = false;
1524
+ constructor(options) {
1525
+ this.getMetaFlock = options.getMetaFlock;
1526
+ this.storage = options.storage;
1527
+ const configuredDebounce = options.debounceMs;
1528
+ this.debounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_META_PERSIST_DEBOUNCE_MS;
1529
+ }
1530
+ start(initialVersion) {
1531
+ this.lastPersistedVersion = initialVersion;
1532
+ if (this.unsubscribe) return;
1533
+ this.unsubscribe = this.metaFlock.subscribe(() => {
1534
+ this.scheduleFlush();
1535
+ });
1536
+ }
1537
+ async destroy() {
1538
+ this.destroyed = true;
1539
+ if (this.flushTimer) {
1540
+ clearTimeout(this.flushTimer);
1541
+ this.flushTimer = void 0;
1542
+ }
1543
+ if (this.unsubscribe) {
1544
+ this.unsubscribe();
1545
+ this.unsubscribe = void 0;
1546
+ }
1547
+ await this.flushNow();
1548
+ }
1549
+ async flushNow(forceFull = false) {
1550
+ if (this.flushTimer) {
1551
+ clearTimeout(this.flushTimer);
1552
+ this.flushTimer = void 0;
1553
+ }
1554
+ await this.flush(forceFull);
1555
+ }
1556
+ scheduleFlush() {
1557
+ if (this.destroyed) return;
1558
+ if (this.debounceMs === 0) {
1559
+ this.flush();
1560
+ return;
1561
+ }
1562
+ if (this.flushTimer) clearTimeout(this.flushTimer);
1563
+ this.flushTimer = setTimeout(() => {
1564
+ this.flushTimer = void 0;
1565
+ this.flush();
1566
+ }, this.debounceMs);
1567
+ }
1568
+ async flush(forceFull = false) {
1569
+ if (forceFull) this.forceFullOnNextFlush = true;
1570
+ const run = this.flushPromise.catch(() => {}).then(() => this.flushInternal());
1571
+ this.flushPromise = run;
1572
+ await run;
1573
+ }
1574
+ async flushInternal() {
1575
+ const flock = this.metaFlock;
1576
+ const currentVersion = flock.version();
1577
+ if (this.lastPersistedVersion && this.versionsEqual(currentVersion, this.lastPersistedVersion)) {
1578
+ this.forceFullOnNextFlush = false;
1579
+ return;
1580
+ }
1581
+ const baseline = this.forceFullOnNextFlush ? void 0 : this.lastPersistedVersion;
1582
+ const rawBundle = baseline ? flock.exportJson(baseline) : flock.exportJson();
1583
+ const bundle = baseline ? this.stripUnchangedEntries(rawBundle, baseline) : rawBundle;
1584
+ if (Object.keys(bundle.entries).length === 0) {
1585
+ this.forceFullOnNextFlush = false;
1586
+ this.lastPersistedVersion = currentVersion;
1587
+ return;
1588
+ }
1589
+ const encoded = textEncoder.encode(JSON.stringify(bundle));
1590
+ if (!this.storage) {
1591
+ this.lastPersistedVersion = currentVersion;
1592
+ this.forceFullOnNextFlush = false;
1593
+ return;
1594
+ }
1595
+ try {
1596
+ await this.storage.save({
1597
+ type: "meta",
1598
+ update: encoded
1599
+ });
1600
+ } catch (error) {
1601
+ throw error;
1602
+ }
1603
+ this.lastPersistedVersion = currentVersion;
1604
+ this.forceFullOnNextFlush = false;
1605
+ }
1606
+ get metaFlock() {
1607
+ return this.getMetaFlock();
1608
+ }
1609
+ stripUnchangedEntries(bundle, baseline) {
1610
+ const entries = {};
1611
+ for (const [key, record] of Object.entries(bundle.entries)) {
1612
+ const clock = this.parseClock(record.c);
1613
+ if (!clock) {
1614
+ entries[key] = record;
1615
+ continue;
1616
+ }
1617
+ const baselineEntry = baseline[clock.peerIdHex];
1618
+ if (!baselineEntry) {
1619
+ entries[key] = record;
1620
+ continue;
1621
+ }
1622
+ if (clock.physicalTime > baselineEntry.physicalTime || clock.physicalTime === baselineEntry.physicalTime && clock.logicalCounter > baselineEntry.logicalCounter) entries[key] = record;
1623
+ }
1624
+ return {
1625
+ version: bundle.version,
1626
+ entries
1627
+ };
1628
+ }
1629
+ parseClock(raw) {
1630
+ if (typeof raw !== "string") return void 0;
1631
+ const [physicalTimeStr, logicalCounterStr, peerIdHex] = raw.split(",");
1632
+ if (!physicalTimeStr || !logicalCounterStr || !peerIdHex) return void 0;
1633
+ const physicalTime = Number(physicalTimeStr);
1634
+ const logicalCounter = Number(logicalCounterStr);
1635
+ if (!Number.isFinite(physicalTime) || !Number.isFinite(logicalCounter)) return void 0;
1636
+ return {
1637
+ physicalTime,
1638
+ logicalCounter,
1639
+ peerIdHex
1640
+ };
1641
+ }
1642
+ versionsEqual(a, b) {
1643
+ if (!a || !b) return false;
1644
+ const aKeys = Object.keys(a);
1645
+ const bKeys = Object.keys(b);
1646
+ if (aKeys.length !== bKeys.length) return false;
1647
+ for (const key of aKeys) {
1648
+ const aEntry = a[key];
1649
+ const bEntry = b[key];
1650
+ if (!aEntry || !bEntry) return false;
1651
+ if (aEntry.physicalTime !== bEntry.physicalTime || aEntry.logicalCounter !== bEntry.logicalCounter) return false;
1652
+ }
1653
+ return true;
1654
+ }
1655
+ };
1656
+
1657
+ //#endregion
1658
+ //#region src/index.ts
1535
1659
  const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
1536
1660
  var LoroRepo = class LoroRepo {
1537
1661
  options;
@@ -1547,6 +1671,7 @@ var LoroRepo = class LoroRepo {
1547
1671
  flockHydrator;
1548
1672
  state;
1549
1673
  syncRunner;
1674
+ metaPersister;
1550
1675
  constructor(options) {
1551
1676
  this.options = options;
1552
1677
  this.transport = options.transportAdapter;
@@ -1560,13 +1685,11 @@ var LoroRepo = class LoroRepo {
1560
1685
  storage: this.storage,
1561
1686
  docFrontierDebounceMs,
1562
1687
  getMetaFlock: () => this.metaFlock,
1563
- eventBus: this.eventBus,
1564
- persistMeta: () => this.persistMeta()
1688
+ eventBus: this.eventBus
1565
1689
  });
1566
1690
  this.metadataManager = new MetadataManager({
1567
1691
  getMetaFlock: () => this.metaFlock,
1568
1692
  eventBus: this.eventBus,
1569
- persistMeta: () => this.persistMeta(),
1570
1693
  state: this.state
1571
1694
  });
1572
1695
  this.assetManager = new AssetManager({
@@ -1574,9 +1697,13 @@ var LoroRepo = class LoroRepo {
1574
1697
  assetTransport: this.assetTransport,
1575
1698
  getMetaFlock: () => this.metaFlock,
1576
1699
  eventBus: this.eventBus,
1577
- persistMeta: () => this.persistMeta(),
1578
1700
  state: this.state
1579
1701
  });
1702
+ this.metaPersister = new MetaPersister({
1703
+ getMetaFlock: () => this.metaFlock,
1704
+ storage: this.storage,
1705
+ debounceMs: options.metaPersistDebounceMs
1706
+ });
1580
1707
  this.flockHydrator = new FlockHydrator({
1581
1708
  getMetaFlock: () => this.metaFlock,
1582
1709
  metadataManager: this.metadataManager,
@@ -1594,8 +1721,7 @@ var LoroRepo = class LoroRepo {
1594
1721
  getMetaFlock: () => this.metaFlock,
1595
1722
  mergeFlock: (snapshot) => {
1596
1723
  this.metaFlock.merge(snapshot);
1597
- },
1598
- persistMeta: () => this.persistMeta()
1724
+ }
1599
1725
  });
1600
1726
  }
1601
1727
  static async create(options) {
@@ -1612,6 +1738,7 @@ var LoroRepo = class LoroRepo {
1612
1738
  */
1613
1739
  async ready() {
1614
1740
  await this.syncRunner.ready();
1741
+ this.metaPersister.start(this.metaFlock.version());
1615
1742
  }
1616
1743
  /**
1617
1744
  * Sync selected data via the transport adaptor
@@ -1627,7 +1754,16 @@ var LoroRepo = class LoroRepo {
1627
1754
  * @returns
1628
1755
  */
1629
1756
  async joinMetaRoom(params) {
1630
- return this.syncRunner.joinMetaRoom(params);
1757
+ const subscription = await this.syncRunner.joinMetaRoom(params);
1758
+ return {
1759
+ unsubscribe: subscription.unsubscribe,
1760
+ get connected() {
1761
+ return subscription.connected;
1762
+ },
1763
+ firstSyncedWithRemote: subscription.firstSyncedWithRemote.then(async () => {
1764
+ await this.metaPersister.flushNow();
1765
+ })
1766
+ };
1631
1767
  }
1632
1768
  /**
1633
1769
  * Start syncing the given doc. It will establish a realtime connection to the transport adaptor.
@@ -1699,6 +1835,7 @@ var LoroRepo = class LoroRepo {
1699
1835
  }
1700
1836
  async flush() {
1701
1837
  await this.docManager.flush();
1838
+ await this.metaPersister.flushNow();
1702
1839
  }
1703
1840
  async uploadAsset(params) {
1704
1841
  return this.assetManager.uploadAsset(params);
@@ -1721,21 +1858,13 @@ var LoroRepo = class LoroRepo {
1721
1858
  async gcAssets(options = {}) {
1722
1859
  return this.assetManager.gcAssets(options);
1723
1860
  }
1724
- async persistMeta() {
1725
- if (!this.storage) return;
1726
- const bundle = this.metaFlock.exportJson();
1727
- const encoded = textEncoder.encode(JSON.stringify(bundle));
1728
- await this.storage.save({
1729
- type: "meta",
1730
- update: encoded
1731
- });
1732
- }
1733
1861
  get destroyed() {
1734
1862
  return this._destroyed;
1735
1863
  }
1736
1864
  async destroy() {
1737
1865
  if (this._destroyed) return;
1738
1866
  this._destroyed = true;
1867
+ await this.metaPersister.destroy();
1739
1868
  await this.syncRunner.destroy();
1740
1869
  this.assetTransport?.close?.();
1741
1870
  this.storage?.close?.();