loro-repo 0.5.2 → 0.6.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/README.md +40 -1
- package/dist/index.cjs +644 -133
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -5
- package/dist/index.d.ts +48 -5
- package/dist/index.js +644 -133
- package/dist/index.js.map +1 -1
- package/dist/storage/filesystem.cjs +60 -10
- package/dist/storage/filesystem.cjs.map +1 -1
- package/dist/storage/filesystem.d.cts +8 -2
- package/dist/storage/filesystem.d.ts +8 -2
- package/dist/storage/filesystem.js +60 -10
- package/dist/storage/filesystem.js.map +1 -1
- package/dist/storage/indexeddb.cjs +51 -9
- package/dist/storage/indexeddb.cjs.map +1 -1
- package/dist/storage/indexeddb.d.cts +7 -1
- package/dist/storage/indexeddb.d.ts +7 -1
- package/dist/storage/indexeddb.js +51 -9
- package/dist/storage/indexeddb.js.map +1 -1
- package/dist/transport/broadcast-channel.cjs +131 -1
- package/dist/transport/broadcast-channel.cjs.map +1 -1
- package/dist/transport/broadcast-channel.d.cts +20 -3
- package/dist/transport/broadcast-channel.d.ts +20 -3
- package/dist/transport/broadcast-channel.js +130 -1
- package/dist/transport/broadcast-channel.js.map +1 -1
- package/dist/transport/websocket.cjs +348 -24
- package/dist/transport/websocket.cjs.map +1 -1
- package/dist/transport/websocket.d.cts +47 -5
- package/dist/transport/websocket.d.ts +47 -5
- package/dist/transport/websocket.js +349 -24
- package/dist/transport/websocket.js.map +1 -1
- package/dist/types.d.cts +121 -4
- package/dist/types.d.ts +121 -4
- package/package.json +7 -7
package/dist/index.cjs
CHANGED
|
@@ -40,7 +40,7 @@ var RepoEventBus = class {
|
|
|
40
40
|
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
41
41
|
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
42
42
|
const docId = (() => {
|
|
43
|
-
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
43
|
+
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers" || event.kind === "doc-soft-deleted" || event.kind === "doc-purging") return event.docId;
|
|
44
44
|
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
45
45
|
})();
|
|
46
46
|
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
@@ -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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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({
|
|
@@ -182,6 +177,18 @@ var DocManager = class {
|
|
|
182
177
|
this.docs.delete(docId);
|
|
183
178
|
this.docPersistedVersions.delete(docId);
|
|
184
179
|
}
|
|
180
|
+
async dropDoc(docId) {
|
|
181
|
+
const pending = this.docFrontierUpdates.get(docId);
|
|
182
|
+
if (pending) {
|
|
183
|
+
clearTimeout(pending.timeout);
|
|
184
|
+
this.docFrontierUpdates.delete(docId);
|
|
185
|
+
}
|
|
186
|
+
this.docSubscriptions.get(docId)?.();
|
|
187
|
+
this.docSubscriptions.delete(docId);
|
|
188
|
+
this.docs.delete(docId);
|
|
189
|
+
this.docPersistedVersions.delete(docId);
|
|
190
|
+
if (this.storage?.deleteDoc) await this.storage.deleteDoc(docId);
|
|
191
|
+
}
|
|
185
192
|
async flush() {
|
|
186
193
|
const promises = [];
|
|
187
194
|
for (const [docId, doc] of this.docs) promises.push((async () => {
|
|
@@ -502,22 +509,51 @@ function matchesQuery(docId, _metadata, query) {
|
|
|
502
509
|
var MetadataManager = class {
|
|
503
510
|
getMetaFlock;
|
|
504
511
|
eventBus;
|
|
505
|
-
persistMeta;
|
|
506
512
|
state;
|
|
507
513
|
constructor(options) {
|
|
508
514
|
this.getMetaFlock = options.getMetaFlock;
|
|
509
515
|
this.eventBus = options.eventBus;
|
|
510
|
-
this.persistMeta = options.persistMeta;
|
|
511
516
|
this.state = options.state;
|
|
512
517
|
}
|
|
513
518
|
getDocIds() {
|
|
514
|
-
|
|
519
|
+
const ids = new Set(this.state.metadata.keys());
|
|
520
|
+
const tombstoneRows = this.metaFlock.scan({ prefix: ["ts"] });
|
|
521
|
+
for (const row of tombstoneRows) {
|
|
522
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
523
|
+
const docId = row.key[1];
|
|
524
|
+
if (typeof docId === "string") ids.add(docId);
|
|
525
|
+
}
|
|
526
|
+
return Array.from(ids);
|
|
515
527
|
}
|
|
516
528
|
entries() {
|
|
517
529
|
return this.state.metadata.entries();
|
|
518
530
|
}
|
|
519
531
|
get(docId) {
|
|
520
|
-
|
|
532
|
+
const metadata = this.state.metadata.get(docId);
|
|
533
|
+
const deletedAtMs = this.readDeletedAtFromFlock(docId);
|
|
534
|
+
if (deletedAtMs === void 0 && this.isDocKeyspaceEmpty(docId) || !metadata || deletedAtMs === void 0 && this.isEmpty(metadata)) {
|
|
535
|
+
if (deletedAtMs === void 0) return void 0;
|
|
536
|
+
return {
|
|
537
|
+
meta: {},
|
|
538
|
+
deletedAtMs
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
meta: cloneJsonObject(metadata),
|
|
543
|
+
deletedAtMs
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
getDeletedAtMs(docId) {
|
|
547
|
+
return this.readDeletedAtFromFlock(docId);
|
|
548
|
+
}
|
|
549
|
+
markDeleted(docId, deletedAtMs) {
|
|
550
|
+
this.metaFlock.put(["ts", docId], deletedAtMs);
|
|
551
|
+
this.emitSoftDeleted(docId, deletedAtMs, "local");
|
|
552
|
+
}
|
|
553
|
+
clearDeleted(docId) {
|
|
554
|
+
const existing = this.readDeletedAtFromFlock(docId);
|
|
555
|
+
this.metaFlock.delete(["ts", docId]);
|
|
556
|
+
if (existing !== void 0) this.emitSoftDeleted(docId, void 0, "local");
|
|
521
557
|
}
|
|
522
558
|
listDoc(query) {
|
|
523
559
|
if (query?.limit !== void 0 && query.limit <= 0) return [];
|
|
@@ -532,25 +568,49 @@ var MetadataManager = class {
|
|
|
532
568
|
kind: "exclusive",
|
|
533
569
|
key: ["m", endKey]
|
|
534
570
|
};
|
|
535
|
-
const rows = this.metaFlock.scan(scanOptions);
|
|
536
571
|
const seen = /* @__PURE__ */ new Set();
|
|
537
572
|
const entries = [];
|
|
573
|
+
const pushDoc = (docId) => {
|
|
574
|
+
if (seen.has(docId)) return;
|
|
575
|
+
const metadata = this.state.metadata.get(docId);
|
|
576
|
+
const deletedAtMs = this.readDeletedAtFromFlock(docId);
|
|
577
|
+
if (deletedAtMs === void 0 && this.isDocKeyspaceEmpty(docId) || this.isEmpty(metadata) && deletedAtMs === void 0) return;
|
|
578
|
+
if (!matchesQuery(docId, metadata ?? {}, query)) return;
|
|
579
|
+
seen.add(docId);
|
|
580
|
+
entries.push({
|
|
581
|
+
docId,
|
|
582
|
+
deletedAtMs,
|
|
583
|
+
meta: cloneJsonObject(metadata ?? {})
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
const rows = this.metaFlock.scan(scanOptions);
|
|
538
587
|
for (const row of rows) {
|
|
539
588
|
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
540
589
|
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
541
590
|
const docId = row.key[1];
|
|
542
591
|
if (typeof docId !== "string") continue;
|
|
543
|
-
|
|
544
|
-
seen.add(docId);
|
|
545
|
-
const metadata = this.state.metadata.get(docId);
|
|
546
|
-
if (!metadata) continue;
|
|
547
|
-
if (!matchesQuery(docId, metadata, query)) continue;
|
|
548
|
-
entries.push({
|
|
549
|
-
docId,
|
|
550
|
-
meta: cloneJsonObject(metadata)
|
|
551
|
-
});
|
|
592
|
+
pushDoc(docId);
|
|
552
593
|
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
553
594
|
}
|
|
595
|
+
if (query?.limit === void 0 || entries.length < query.limit) {
|
|
596
|
+
const tsScan = { prefix: ["ts"] };
|
|
597
|
+
if (startKey) tsScan.start = {
|
|
598
|
+
kind: "inclusive",
|
|
599
|
+
key: ["ts", startKey]
|
|
600
|
+
};
|
|
601
|
+
if (endKey) tsScan.end = {
|
|
602
|
+
kind: "exclusive",
|
|
603
|
+
key: ["ts", endKey]
|
|
604
|
+
};
|
|
605
|
+
const tsRows = this.metaFlock.scan(tsScan);
|
|
606
|
+
for (const row of tsRows) {
|
|
607
|
+
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
608
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
609
|
+
const docId = row.key[1];
|
|
610
|
+
if (typeof docId !== "string") continue;
|
|
611
|
+
pushDoc(docId);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
554
614
|
return entries;
|
|
555
615
|
}
|
|
556
616
|
async upsert(docId, patch) {
|
|
@@ -563,12 +623,10 @@ var MetadataManager = class {
|
|
|
563
623
|
const rawValue = patchObject[key];
|
|
564
624
|
if (rawValue === void 0) continue;
|
|
565
625
|
if (jsonEquals(base ? base[key] : void 0, rawValue)) continue;
|
|
566
|
-
const storageKey = key === "tombstone" ? "$tombstone" : key;
|
|
567
|
-
console.log("upserting", rawValue);
|
|
568
626
|
this.metaFlock.put([
|
|
569
627
|
"m",
|
|
570
628
|
docId,
|
|
571
|
-
|
|
629
|
+
key
|
|
572
630
|
], rawValue);
|
|
573
631
|
next[key] = rawValue;
|
|
574
632
|
outPatch[key] = rawValue;
|
|
@@ -579,7 +637,6 @@ var MetadataManager = class {
|
|
|
579
637
|
return;
|
|
580
638
|
}
|
|
581
639
|
this.state.metadata.set(docId, next);
|
|
582
|
-
await this.persistMeta();
|
|
583
640
|
this.eventBus.emit({
|
|
584
641
|
kind: "doc-metadata",
|
|
585
642
|
docId,
|
|
@@ -588,10 +645,10 @@ var MetadataManager = class {
|
|
|
588
645
|
});
|
|
589
646
|
}
|
|
590
647
|
refreshFromFlock(docId, by) {
|
|
591
|
-
const
|
|
592
|
-
const
|
|
593
|
-
if (!
|
|
594
|
-
if (
|
|
648
|
+
const previousMeta = this.state.metadata.get(docId);
|
|
649
|
+
const nextMeta = this.readDocMetadataFromFlock(docId);
|
|
650
|
+
if (!nextMeta) {
|
|
651
|
+
if (previousMeta) {
|
|
595
652
|
this.state.metadata.delete(docId);
|
|
596
653
|
this.eventBus.emit({
|
|
597
654
|
kind: "doc-metadata",
|
|
@@ -602,9 +659,9 @@ var MetadataManager = class {
|
|
|
602
659
|
}
|
|
603
660
|
return;
|
|
604
661
|
}
|
|
605
|
-
this.state.metadata.set(docId,
|
|
606
|
-
const patch = diffJsonObjects(
|
|
607
|
-
if (!
|
|
662
|
+
this.state.metadata.set(docId, nextMeta);
|
|
663
|
+
const patch = diffJsonObjects(previousMeta, nextMeta);
|
|
664
|
+
if (!previousMeta || Object.keys(patch).length > 0) this.eventBus.emit({
|
|
608
665
|
kind: "doc-metadata",
|
|
609
666
|
docId,
|
|
610
667
|
patch,
|
|
@@ -620,12 +677,16 @@ var MetadataManager = class {
|
|
|
620
677
|
const previous = prevMetadata.get(docId);
|
|
621
678
|
const current = nextMetadata.get(docId);
|
|
622
679
|
if (!current) {
|
|
623
|
-
if (previous)
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
680
|
+
if (previous) {
|
|
681
|
+
const patch$1 = {};
|
|
682
|
+
for (const key of Object.keys(previous)) patch$1[key] = null;
|
|
683
|
+
this.eventBus.emit({
|
|
684
|
+
kind: "doc-metadata",
|
|
685
|
+
docId,
|
|
686
|
+
patch: patch$1,
|
|
687
|
+
by
|
|
688
|
+
});
|
|
689
|
+
}
|
|
629
690
|
continue;
|
|
630
691
|
}
|
|
631
692
|
const patch = diffJsonObjects(previous, current);
|
|
@@ -640,6 +701,22 @@ var MetadataManager = class {
|
|
|
640
701
|
clear() {
|
|
641
702
|
this.state.metadata.clear();
|
|
642
703
|
}
|
|
704
|
+
purgeMetadata(docId, by) {
|
|
705
|
+
if (this.state.metadata.delete(docId)) this.eventBus.emit({
|
|
706
|
+
kind: "doc-metadata",
|
|
707
|
+
docId,
|
|
708
|
+
patch: {},
|
|
709
|
+
by
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
emitSoftDeleted(docId, deletedAtMs, by) {
|
|
713
|
+
this.eventBus.emit({
|
|
714
|
+
kind: "doc-soft-deleted",
|
|
715
|
+
docId,
|
|
716
|
+
deletedAtMs,
|
|
717
|
+
by
|
|
718
|
+
});
|
|
719
|
+
}
|
|
643
720
|
computeDocRangeKeys(query) {
|
|
644
721
|
if (!query) return {};
|
|
645
722
|
const prefix = query.prefix && query.prefix.length > 0 ? query.prefix : void 0;
|
|
@@ -665,7 +742,9 @@ var MetadataManager = class {
|
|
|
665
742
|
if (!rows.length) return void 0;
|
|
666
743
|
const docMeta = {};
|
|
667
744
|
let populated = false;
|
|
745
|
+
let sawRow = false;
|
|
668
746
|
for (const row of rows) {
|
|
747
|
+
sawRow = true;
|
|
669
748
|
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
670
749
|
if (row.key.length === 2) {
|
|
671
750
|
const obj = asJsonObject(row.value);
|
|
@@ -681,17 +760,33 @@ var MetadataManager = class {
|
|
|
681
760
|
}
|
|
682
761
|
const fieldKey = row.key[2];
|
|
683
762
|
if (typeof fieldKey !== "string") continue;
|
|
684
|
-
if (fieldKey === "$tombstone") {
|
|
685
|
-
docMeta.tombstone = Boolean(row.value);
|
|
686
|
-
populated = true;
|
|
687
|
-
continue;
|
|
688
|
-
}
|
|
689
763
|
const jsonValue = cloneJsonValue(row.value);
|
|
690
764
|
if (jsonValue === void 0) continue;
|
|
691
765
|
docMeta[fieldKey] = jsonValue;
|
|
692
766
|
populated = true;
|
|
693
767
|
}
|
|
694
|
-
|
|
768
|
+
if (populated) return docMeta;
|
|
769
|
+
if (sawRow) return {};
|
|
770
|
+
}
|
|
771
|
+
readDeletedAtFromFlock(docId) {
|
|
772
|
+
const raw = this.metaFlock.get(["ts", docId]);
|
|
773
|
+
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
774
|
+
}
|
|
775
|
+
isEmpty(metadata) {
|
|
776
|
+
return !metadata || Object.keys(metadata).length === 0;
|
|
777
|
+
}
|
|
778
|
+
isDocKeyspaceEmpty(docId) {
|
|
779
|
+
if (this.state.metadata.has(docId)) return false;
|
|
780
|
+
const hasTruthyKey = (prefix) => {
|
|
781
|
+
const rows = this.metaFlock.scan({ prefix });
|
|
782
|
+
for (const row of rows) if (row.value !== void 0) return true;
|
|
783
|
+
return false;
|
|
784
|
+
};
|
|
785
|
+
if (this.metaFlock.get(["ts", docId]) !== void 0) return false;
|
|
786
|
+
if (hasTruthyKey(["m", docId])) return false;
|
|
787
|
+
if (hasTruthyKey(["f", docId])) return false;
|
|
788
|
+
if (hasTruthyKey(["ld", docId])) return false;
|
|
789
|
+
return true;
|
|
695
790
|
}
|
|
696
791
|
get metaFlock() {
|
|
697
792
|
return this.getMetaFlock();
|
|
@@ -705,7 +800,6 @@ var AssetManager = class {
|
|
|
705
800
|
assetTransport;
|
|
706
801
|
getMetaFlock;
|
|
707
802
|
eventBus;
|
|
708
|
-
persistMeta;
|
|
709
803
|
state;
|
|
710
804
|
get docAssets() {
|
|
711
805
|
return this.state.docAssets;
|
|
@@ -724,7 +818,6 @@ var AssetManager = class {
|
|
|
724
818
|
this.assetTransport = options.assetTransport;
|
|
725
819
|
this.getMetaFlock = options.getMetaFlock;
|
|
726
820
|
this.eventBus = options.eventBus;
|
|
727
|
-
this.persistMeta = options.persistMeta;
|
|
728
821
|
this.state = options.state;
|
|
729
822
|
}
|
|
730
823
|
async uploadAsset(params) {
|
|
@@ -761,7 +854,6 @@ var AssetManager = class {
|
|
|
761
854
|
if (metadataMutated) {
|
|
762
855
|
existing.metadata = metadata$1;
|
|
763
856
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata$1));
|
|
764
|
-
await this.persistMeta();
|
|
765
857
|
this.eventBus.emit({
|
|
766
858
|
kind: "asset-metadata",
|
|
767
859
|
asset: this.createAssetDownload(assetId, metadata$1, bytes),
|
|
@@ -798,7 +890,6 @@ var AssetManager = class {
|
|
|
798
890
|
this.markAssetAsOrphan(assetId, metadata);
|
|
799
891
|
this.updateDocAssetMetadata(assetId, metadata);
|
|
800
892
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
801
|
-
await this.persistMeta();
|
|
802
893
|
this.eventBus.emit({
|
|
803
894
|
kind: "asset-metadata",
|
|
804
895
|
asset: this.createAssetDownload(assetId, metadata, storedBytes),
|
|
@@ -857,7 +948,6 @@ var AssetManager = class {
|
|
|
857
948
|
existing.metadata = nextMetadata;
|
|
858
949
|
metadata = nextMetadata;
|
|
859
950
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
860
|
-
await this.persistMeta();
|
|
861
951
|
this.eventBus.emit({
|
|
862
952
|
kind: "asset-metadata",
|
|
863
953
|
asset: this.createAssetDownload(assetId, metadata, bytes),
|
|
@@ -904,7 +994,6 @@ var AssetManager = class {
|
|
|
904
994
|
docId,
|
|
905
995
|
assetId
|
|
906
996
|
], true);
|
|
907
|
-
await this.persistMeta();
|
|
908
997
|
this.eventBus.emit({
|
|
909
998
|
kind: "asset-link",
|
|
910
999
|
docId,
|
|
@@ -929,7 +1018,6 @@ var AssetManager = class {
|
|
|
929
1018
|
assetId
|
|
930
1019
|
]);
|
|
931
1020
|
this.removeDocAssetReference(assetId, docId);
|
|
932
|
-
await this.persistMeta();
|
|
933
1021
|
this.eventBus.emit({
|
|
934
1022
|
kind: "asset-unlink",
|
|
935
1023
|
docId,
|
|
@@ -937,6 +1025,11 @@ var AssetManager = class {
|
|
|
937
1025
|
by: "local"
|
|
938
1026
|
});
|
|
939
1027
|
}
|
|
1028
|
+
purgeDocLinks(docId, by) {
|
|
1029
|
+
const keys = Array.from(this.metaFlock.scan({ prefix: ["ld", docId] }), (row) => row.key);
|
|
1030
|
+
for (const key of keys) this.metaFlock.delete(key);
|
|
1031
|
+
this.refreshDocAssetsEntry(docId, by);
|
|
1032
|
+
}
|
|
940
1033
|
async listAssets(docId) {
|
|
941
1034
|
const mapping = this.docAssets.get(docId);
|
|
942
1035
|
if (!mapping) return [];
|
|
@@ -1213,7 +1306,6 @@ var AssetManager = class {
|
|
|
1213
1306
|
this.assets.set(assetId, { metadata });
|
|
1214
1307
|
this.updateDocAssetMetadata(assetId, metadata);
|
|
1215
1308
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
1216
|
-
await this.persistMeta();
|
|
1217
1309
|
if (this.storage) await this.storage.save({
|
|
1218
1310
|
type: "asset",
|
|
1219
1311
|
assetId,
|
|
@@ -1286,14 +1378,35 @@ var FlockHydrator = class {
|
|
|
1286
1378
|
this.docManager = options.docManager;
|
|
1287
1379
|
}
|
|
1288
1380
|
hydrateAll(by) {
|
|
1289
|
-
const
|
|
1290
|
-
this.metadataManager.replaceAll(
|
|
1381
|
+
const { metadata, tombstones } = this.readAllDocMetadata();
|
|
1382
|
+
this.metadataManager.replaceAll(metadata, by);
|
|
1383
|
+
for (const [docId, deletedAtMs] of tombstones) this.metadataManager.emitSoftDeleted(docId, deletedAtMs, by);
|
|
1291
1384
|
this.assetManager.hydrateFromFlock(by);
|
|
1385
|
+
const docIds = new Set([
|
|
1386
|
+
...metadata.keys(),
|
|
1387
|
+
...tombstones.keys(),
|
|
1388
|
+
...this.collectDocIds(["f"]),
|
|
1389
|
+
...this.collectDocIds(["ld"])
|
|
1390
|
+
]);
|
|
1391
|
+
for (const docId of docIds) if (this.isDocKeyspaceEmpty(docId)) (async () => {
|
|
1392
|
+
try {
|
|
1393
|
+
this.clearDocKeyspace(docId);
|
|
1394
|
+
this.metadataManager.purgeMetadata(docId, by);
|
|
1395
|
+
await this.docManager.dropDoc(docId);
|
|
1396
|
+
} catch (error) {
|
|
1397
|
+
console.error("Failed to drop purged doc during hydrateAll", {
|
|
1398
|
+
docId,
|
|
1399
|
+
error
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
})();
|
|
1292
1403
|
}
|
|
1293
|
-
applyEvents(events, by) {
|
|
1404
|
+
async applyEvents(events, by) {
|
|
1294
1405
|
if (!events.length) return;
|
|
1295
1406
|
const docMetadataIds = /* @__PURE__ */ new Set();
|
|
1407
|
+
const docTombstoneIds = /* @__PURE__ */ new Map();
|
|
1296
1408
|
const docAssetIds = /* @__PURE__ */ new Set();
|
|
1409
|
+
const docFrontierIds = /* @__PURE__ */ new Set();
|
|
1297
1410
|
const assetIds = /* @__PURE__ */ new Set();
|
|
1298
1411
|
for (const event of events) {
|
|
1299
1412
|
const key = event.key;
|
|
@@ -1302,6 +1415,12 @@ var FlockHydrator = class {
|
|
|
1302
1415
|
if (root === "m") {
|
|
1303
1416
|
const docId = key[1];
|
|
1304
1417
|
if (typeof docId === "string") docMetadataIds.add(docId);
|
|
1418
|
+
} else if (root === "f") {
|
|
1419
|
+
const docId = key[1];
|
|
1420
|
+
if (typeof docId === "string") docFrontierIds.add(docId);
|
|
1421
|
+
} else if (root === "ts") {
|
|
1422
|
+
const docId = key[1];
|
|
1423
|
+
if (typeof docId === "string") docTombstoneIds.set(docId, this.toDeletedAt(event.value));
|
|
1305
1424
|
} else if (root === "a") {
|
|
1306
1425
|
const assetId = key[1];
|
|
1307
1426
|
if (typeof assetId === "string") assetIds.add(assetId);
|
|
@@ -1314,7 +1433,38 @@ var FlockHydrator = class {
|
|
|
1314
1433
|
}
|
|
1315
1434
|
for (const assetId of assetIds) this.assetManager.refreshAssetMetadataEntry(assetId, by);
|
|
1316
1435
|
for (const docId of docMetadataIds) this.metadataManager.refreshFromFlock(docId, by);
|
|
1436
|
+
for (const [docId, deletedAtMs] of docTombstoneIds) this.metadataManager.emitSoftDeleted(docId, deletedAtMs, by);
|
|
1317
1437
|
for (const docId of docAssetIds) this.assetManager.refreshDocAssetsEntry(docId, by);
|
|
1438
|
+
const docIdsToCheck = new Set([
|
|
1439
|
+
...docMetadataIds,
|
|
1440
|
+
...docTombstoneIds.keys(),
|
|
1441
|
+
...docAssetIds,
|
|
1442
|
+
...docFrontierIds
|
|
1443
|
+
]);
|
|
1444
|
+
for (const docId of docIdsToCheck) if (this.isDocKeyspaceEmpty(docId)) try {
|
|
1445
|
+
this.clearDocKeyspace(docId);
|
|
1446
|
+
this.metadataManager.purgeMetadata(docId, by);
|
|
1447
|
+
await this.docManager.dropDoc(docId);
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
console.error("Failed to drop purged doc during hydration", {
|
|
1450
|
+
docId,
|
|
1451
|
+
error
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
for (const docId of docIdsToCheck) {
|
|
1455
|
+
const snapshot = this.metadataManager.get(docId);
|
|
1456
|
+
const tombstone = this.metaFlock.get(["ts", docId]);
|
|
1457
|
+
if (!snapshot && tombstone === void 0) try {
|
|
1458
|
+
this.clearDocKeyspace(docId);
|
|
1459
|
+
this.metadataManager.purgeMetadata(docId, by);
|
|
1460
|
+
await this.docManager.dropDoc(docId);
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
console.error("Failed to drop purged doc after refresh", {
|
|
1463
|
+
docId,
|
|
1464
|
+
error
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1318
1468
|
}
|
|
1319
1469
|
readAllDocMetadata() {
|
|
1320
1470
|
const nextMetadata = /* @__PURE__ */ new Map();
|
|
@@ -1323,35 +1473,86 @@ var FlockHydrator = class {
|
|
|
1323
1473
|
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1324
1474
|
const docId = row.key[1];
|
|
1325
1475
|
if (typeof docId !== "string") continue;
|
|
1326
|
-
|
|
1327
|
-
if (!docMeta) {
|
|
1328
|
-
docMeta = {};
|
|
1329
|
-
nextMetadata.set(docId, docMeta);
|
|
1330
|
-
}
|
|
1476
|
+
if (row.value === void 0 && !this.metadataManager.get(docId)) continue;
|
|
1331
1477
|
if (row.key.length === 2) {
|
|
1332
1478
|
const obj = asJsonObject(row.value);
|
|
1333
1479
|
if (!obj) continue;
|
|
1480
|
+
let docMeta$1 = nextMetadata.get(docId);
|
|
1481
|
+
if (!docMeta$1) {
|
|
1482
|
+
docMeta$1 = {};
|
|
1483
|
+
nextMetadata.set(docId, docMeta$1);
|
|
1484
|
+
}
|
|
1334
1485
|
for (const [field, value] of Object.entries(obj)) {
|
|
1335
1486
|
const cloned = cloneJsonValue(value);
|
|
1336
|
-
if (cloned !== void 0) docMeta[field] = cloned;
|
|
1487
|
+
if (cloned !== void 0) docMeta$1[field] = cloned;
|
|
1337
1488
|
}
|
|
1338
1489
|
continue;
|
|
1339
1490
|
}
|
|
1340
1491
|
const fieldKey = row.key[2];
|
|
1341
1492
|
if (typeof fieldKey !== "string") continue;
|
|
1342
|
-
if (fieldKey === "$tombstone") {
|
|
1343
|
-
docMeta.tombstone = Boolean(row.value);
|
|
1344
|
-
continue;
|
|
1345
|
-
}
|
|
1346
1493
|
const jsonValue = cloneJsonValue(row.value);
|
|
1347
1494
|
if (jsonValue === void 0) continue;
|
|
1495
|
+
let docMeta = nextMetadata.get(docId);
|
|
1496
|
+
if (!docMeta) {
|
|
1497
|
+
docMeta = {};
|
|
1498
|
+
nextMetadata.set(docId, docMeta);
|
|
1499
|
+
}
|
|
1348
1500
|
docMeta[fieldKey] = jsonValue;
|
|
1349
1501
|
}
|
|
1350
|
-
|
|
1502
|
+
const tombstones = /* @__PURE__ */ new Map();
|
|
1503
|
+
const tombstoneRows = this.metaFlock.scan({ prefix: ["ts"] });
|
|
1504
|
+
for (const row of tombstoneRows) {
|
|
1505
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1506
|
+
const docId = row.key[1];
|
|
1507
|
+
if (typeof docId !== "string") continue;
|
|
1508
|
+
const tombstone = cloneJsonValue(row.value);
|
|
1509
|
+
if (typeof tombstone === "number" && Number.isFinite(tombstone)) tombstones.set(docId, tombstone);
|
|
1510
|
+
}
|
|
1511
|
+
return {
|
|
1512
|
+
metadata: nextMetadata,
|
|
1513
|
+
tombstones
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
toDeletedAt(value) {
|
|
1517
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1351
1518
|
}
|
|
1352
1519
|
get metaFlock() {
|
|
1353
1520
|
return this.getMetaFlock();
|
|
1354
1521
|
}
|
|
1522
|
+
isDocKeyspaceEmpty(docId) {
|
|
1523
|
+
if (this.metaFlock.get(["ts", docId]) !== void 0) return false;
|
|
1524
|
+
if (this.hasTruthyKey(["m", docId])) return false;
|
|
1525
|
+
if (this.hasTruthyKey(["f", docId])) return false;
|
|
1526
|
+
if (this.hasTruthyKey(["ld", docId])) return false;
|
|
1527
|
+
return true;
|
|
1528
|
+
}
|
|
1529
|
+
hasTruthyKey(prefix) {
|
|
1530
|
+
const rows = this.metaFlock.scan({ prefix });
|
|
1531
|
+
for (const row of rows) if (row.value !== void 0) return true;
|
|
1532
|
+
return false;
|
|
1533
|
+
}
|
|
1534
|
+
collectDocIds(prefix) {
|
|
1535
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1536
|
+
const rows = this.metaFlock.scan({ prefix });
|
|
1537
|
+
for (const row of rows) {
|
|
1538
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1539
|
+
const docId = row.key[1];
|
|
1540
|
+
if (typeof docId === "string") ids.add(docId);
|
|
1541
|
+
}
|
|
1542
|
+
return ids;
|
|
1543
|
+
}
|
|
1544
|
+
clearDocKeyspace(docId) {
|
|
1545
|
+
const prefixes = [
|
|
1546
|
+
["m", docId],
|
|
1547
|
+
["f", docId],
|
|
1548
|
+
["ld", docId]
|
|
1549
|
+
];
|
|
1550
|
+
for (const prefix of prefixes) {
|
|
1551
|
+
const rows = this.metaFlock.scan({ prefix });
|
|
1552
|
+
for (const row of rows) this.metaFlock.delete(row.key);
|
|
1553
|
+
}
|
|
1554
|
+
this.metaFlock.delete(["ts", docId]);
|
|
1555
|
+
}
|
|
1355
1556
|
};
|
|
1356
1557
|
|
|
1357
1558
|
//#endregion
|
|
@@ -1369,7 +1570,6 @@ var SyncRunner = class {
|
|
|
1369
1570
|
flockHydrator;
|
|
1370
1571
|
getMetaFlock;
|
|
1371
1572
|
replaceMetaFlock;
|
|
1372
|
-
persistMeta;
|
|
1373
1573
|
readyPromise;
|
|
1374
1574
|
metaRoomSubscription;
|
|
1375
1575
|
unsubscribeMetaFlock;
|
|
@@ -1384,7 +1584,6 @@ var SyncRunner = class {
|
|
|
1384
1584
|
this.flockHydrator = options.flockHydrator;
|
|
1385
1585
|
this.getMetaFlock = options.getMetaFlock;
|
|
1386
1586
|
this.replaceMetaFlock = options.mergeFlock;
|
|
1387
|
-
this.persistMeta = options.persistMeta;
|
|
1388
1587
|
}
|
|
1389
1588
|
async ready() {
|
|
1390
1589
|
if (!this.readyPromise) this.readyPromise = this.initialize();
|
|
@@ -1404,9 +1603,8 @@ var SyncRunner = class {
|
|
|
1404
1603
|
});
|
|
1405
1604
|
try {
|
|
1406
1605
|
if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
|
|
1407
|
-
if (recordedEvents.length > 0) this.flockHydrator.applyEvents(recordedEvents, "sync");
|
|
1606
|
+
if (recordedEvents.length > 0) await this.flockHydrator.applyEvents(recordedEvents, "sync");
|
|
1408
1607
|
else this.flockHydrator.hydrateAll("sync");
|
|
1409
|
-
await this.persistMeta();
|
|
1410
1608
|
} finally {
|
|
1411
1609
|
unsubscribe();
|
|
1412
1610
|
this.eventBus.popEventBy();
|
|
@@ -1431,63 +1629,105 @@ var SyncRunner = class {
|
|
|
1431
1629
|
await this.ready();
|
|
1432
1630
|
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
1433
1631
|
if (!this.transport.isConnected()) await this.transport.connect();
|
|
1434
|
-
|
|
1632
|
+
const existing = this.metaRoomSubscription;
|
|
1633
|
+
if (existing) {
|
|
1634
|
+
existing.refCount += 1;
|
|
1635
|
+
return this.createMetaLease(existing);
|
|
1636
|
+
}
|
|
1435
1637
|
this.ensureMetaLiveMonitor();
|
|
1436
|
-
const
|
|
1437
|
-
const
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
if (this.metaRoomSubscription === wrapped) this.metaRoomSubscription = void 0;
|
|
1441
|
-
if (this.unsubscribeMetaFlock) {
|
|
1442
|
-
this.unsubscribeMetaFlock();
|
|
1443
|
-
this.unsubscribeMetaFlock = void 0;
|
|
1444
|
-
}
|
|
1445
|
-
},
|
|
1446
|
-
firstSyncedWithRemote: subscription.firstSyncedWithRemote,
|
|
1447
|
-
get connected() {
|
|
1448
|
-
return subscription.connected;
|
|
1449
|
-
}
|
|
1638
|
+
const base = this.transport.joinMetaRoom(this.metaFlock, params);
|
|
1639
|
+
const record = {
|
|
1640
|
+
base,
|
|
1641
|
+
refCount: 1
|
|
1450
1642
|
};
|
|
1451
|
-
this.metaRoomSubscription =
|
|
1452
|
-
|
|
1643
|
+
this.metaRoomSubscription = record;
|
|
1644
|
+
base.firstSyncedWithRemote.then(async () => {
|
|
1453
1645
|
const by = this.eventBus.resolveEventBy("live");
|
|
1454
1646
|
this.flockHydrator.hydrateAll(by);
|
|
1455
|
-
await this.persistMeta();
|
|
1456
1647
|
}).catch(logAsyncError("meta room first sync"));
|
|
1457
|
-
return
|
|
1648
|
+
return this.createMetaLease(record);
|
|
1458
1649
|
}
|
|
1459
1650
|
async joinDocRoom(docId, params) {
|
|
1460
1651
|
await this.ready();
|
|
1461
1652
|
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
1462
1653
|
if (!this.transport.isConnected()) await this.transport.connect();
|
|
1463
1654
|
const existing = this.docSubscriptions.get(docId);
|
|
1464
|
-
if (existing)
|
|
1655
|
+
if (existing) {
|
|
1656
|
+
existing.refCount += 1;
|
|
1657
|
+
return this.createDocLease(docId, existing);
|
|
1658
|
+
}
|
|
1465
1659
|
const doc = await this.docManager.ensureDoc(docId);
|
|
1466
|
-
const
|
|
1467
|
-
const
|
|
1660
|
+
const base = this.transport.joinDocRoom(docId, doc, params);
|
|
1661
|
+
const record = {
|
|
1662
|
+
base,
|
|
1663
|
+
refCount: 1
|
|
1664
|
+
};
|
|
1665
|
+
this.docSubscriptions.set(docId, record);
|
|
1666
|
+
base.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
1667
|
+
return this.createDocLease(docId, record);
|
|
1668
|
+
}
|
|
1669
|
+
createMetaLease(record) {
|
|
1670
|
+
let released = false;
|
|
1671
|
+
return {
|
|
1468
1672
|
unsubscribe: () => {
|
|
1469
|
-
|
|
1470
|
-
|
|
1673
|
+
if (released) return;
|
|
1674
|
+
released = true;
|
|
1675
|
+
const current = this.metaRoomSubscription;
|
|
1676
|
+
if (!current || current !== record) return;
|
|
1677
|
+
current.refCount = Math.max(0, current.refCount - 1);
|
|
1678
|
+
if (current.refCount === 0) {
|
|
1679
|
+
current.base.unsubscribe();
|
|
1680
|
+
this.metaRoomSubscription = void 0;
|
|
1681
|
+
if (this.unsubscribeMetaFlock) {
|
|
1682
|
+
this.unsubscribeMetaFlock();
|
|
1683
|
+
this.unsubscribeMetaFlock = void 0;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1471
1686
|
},
|
|
1472
|
-
firstSyncedWithRemote:
|
|
1687
|
+
firstSyncedWithRemote: record.base.firstSyncedWithRemote,
|
|
1473
1688
|
get connected() {
|
|
1474
|
-
return
|
|
1475
|
-
}
|
|
1689
|
+
return record.base.connected;
|
|
1690
|
+
},
|
|
1691
|
+
get status() {
|
|
1692
|
+
return record.base.status;
|
|
1693
|
+
},
|
|
1694
|
+
onStatusChange: record.base.onStatusChange
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
createDocLease(docId, record) {
|
|
1698
|
+
let released = false;
|
|
1699
|
+
return {
|
|
1700
|
+
unsubscribe: () => {
|
|
1701
|
+
if (released) return;
|
|
1702
|
+
released = true;
|
|
1703
|
+
const current = this.docSubscriptions.get(docId);
|
|
1704
|
+
if (!current || current !== record) return;
|
|
1705
|
+
current.refCount = Math.max(0, current.refCount - 1);
|
|
1706
|
+
if (current.refCount === 0) {
|
|
1707
|
+
current.base.unsubscribe();
|
|
1708
|
+
if (this.docSubscriptions.get(docId) === current) this.docSubscriptions.delete(docId);
|
|
1709
|
+
}
|
|
1710
|
+
},
|
|
1711
|
+
firstSyncedWithRemote: record.base.firstSyncedWithRemote,
|
|
1712
|
+
get connected() {
|
|
1713
|
+
return record.base.connected;
|
|
1714
|
+
},
|
|
1715
|
+
get status() {
|
|
1716
|
+
return record.base.status;
|
|
1717
|
+
},
|
|
1718
|
+
onStatusChange: record.base.onStatusChange
|
|
1476
1719
|
};
|
|
1477
|
-
this.docSubscriptions.set(docId, wrapped);
|
|
1478
|
-
subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
1479
|
-
return wrapped;
|
|
1480
1720
|
}
|
|
1481
1721
|
async destroy() {
|
|
1482
1722
|
await this.docManager.close();
|
|
1483
|
-
this.metaRoomSubscription
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
this.docSubscriptions.clear();
|
|
1487
|
-
if (this.unsubscribeMetaFlock) {
|
|
1488
|
-
this.unsubscribeMetaFlock();
|
|
1489
|
-
this.unsubscribeMetaFlock = void 0;
|
|
1723
|
+
if (this.metaRoomSubscription) {
|
|
1724
|
+
this.metaRoomSubscription.base.unsubscribe();
|
|
1725
|
+
this.metaRoomSubscription = void 0;
|
|
1490
1726
|
}
|
|
1727
|
+
for (const record of this.docSubscriptions.values()) record.base.unsubscribe();
|
|
1728
|
+
this.docSubscriptions.clear();
|
|
1729
|
+
this.unsubscribeMetaFlock?.();
|
|
1730
|
+
this.unsubscribeMetaFlock = void 0;
|
|
1491
1731
|
this.eventBus.clear();
|
|
1492
1732
|
this.metadataManager.clear();
|
|
1493
1733
|
this.assetManager.clear();
|
|
@@ -1507,8 +1747,7 @@ var SyncRunner = class {
|
|
|
1507
1747
|
if (batch.source === "local") return;
|
|
1508
1748
|
const by = this.eventBus.resolveEventBy("live");
|
|
1509
1749
|
(async () => {
|
|
1510
|
-
this.flockHydrator.applyEvents(batch.events, by);
|
|
1511
|
-
await this.persistMeta();
|
|
1750
|
+
await this.flockHydrator.applyEvents(batch.events, by);
|
|
1512
1751
|
})().catch(logAsyncError("meta live monitor sync"));
|
|
1513
1752
|
});
|
|
1514
1753
|
}
|
|
@@ -1530,9 +1769,152 @@ function createRepoState() {
|
|
|
1530
1769
|
}
|
|
1531
1770
|
|
|
1532
1771
|
//#endregion
|
|
1533
|
-
//#region src/
|
|
1772
|
+
//#region src/internal/meta-persister.ts
|
|
1534
1773
|
const textEncoder = new TextEncoder();
|
|
1774
|
+
const DEFAULT_META_PERSIST_DEBOUNCE_MS = 5e3;
|
|
1775
|
+
var MetaPersister = class {
|
|
1776
|
+
getMetaFlock;
|
|
1777
|
+
storage;
|
|
1778
|
+
debounceMs;
|
|
1779
|
+
lastPersistedVersion;
|
|
1780
|
+
unsubscribe;
|
|
1781
|
+
flushPromise = Promise.resolve();
|
|
1782
|
+
flushTimer;
|
|
1783
|
+
forceFullOnNextFlush = false;
|
|
1784
|
+
destroyed = false;
|
|
1785
|
+
constructor(options) {
|
|
1786
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
1787
|
+
this.storage = options.storage;
|
|
1788
|
+
const configuredDebounce = options.debounceMs;
|
|
1789
|
+
this.debounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_META_PERSIST_DEBOUNCE_MS;
|
|
1790
|
+
}
|
|
1791
|
+
start(initialVersion) {
|
|
1792
|
+
this.lastPersistedVersion = initialVersion;
|
|
1793
|
+
if (this.unsubscribe) return;
|
|
1794
|
+
this.unsubscribe = this.metaFlock.subscribe(() => {
|
|
1795
|
+
this.scheduleFlush();
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
async destroy() {
|
|
1799
|
+
this.destroyed = true;
|
|
1800
|
+
if (this.flushTimer) {
|
|
1801
|
+
clearTimeout(this.flushTimer);
|
|
1802
|
+
this.flushTimer = void 0;
|
|
1803
|
+
}
|
|
1804
|
+
if (this.unsubscribe) {
|
|
1805
|
+
this.unsubscribe();
|
|
1806
|
+
this.unsubscribe = void 0;
|
|
1807
|
+
}
|
|
1808
|
+
await this.flushNow();
|
|
1809
|
+
}
|
|
1810
|
+
async flushNow(forceFull = false) {
|
|
1811
|
+
if (this.flushTimer) {
|
|
1812
|
+
clearTimeout(this.flushTimer);
|
|
1813
|
+
this.flushTimer = void 0;
|
|
1814
|
+
}
|
|
1815
|
+
await this.flush(forceFull);
|
|
1816
|
+
}
|
|
1817
|
+
scheduleFlush() {
|
|
1818
|
+
if (this.destroyed) return;
|
|
1819
|
+
if (this.debounceMs === 0) {
|
|
1820
|
+
this.flush();
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
if (this.flushTimer) clearTimeout(this.flushTimer);
|
|
1824
|
+
this.flushTimer = setTimeout(() => {
|
|
1825
|
+
this.flushTimer = void 0;
|
|
1826
|
+
this.flush();
|
|
1827
|
+
}, this.debounceMs);
|
|
1828
|
+
}
|
|
1829
|
+
async flush(forceFull = false) {
|
|
1830
|
+
if (forceFull) this.forceFullOnNextFlush = true;
|
|
1831
|
+
const run = this.flushPromise.catch(() => {}).then(() => this.flushInternal());
|
|
1832
|
+
this.flushPromise = run;
|
|
1833
|
+
await run;
|
|
1834
|
+
}
|
|
1835
|
+
async flushInternal() {
|
|
1836
|
+
const flock = this.metaFlock;
|
|
1837
|
+
const currentVersion = flock.version();
|
|
1838
|
+
if (this.lastPersistedVersion && this.versionsEqual(currentVersion, this.lastPersistedVersion)) {
|
|
1839
|
+
this.forceFullOnNextFlush = false;
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
const baseline = this.forceFullOnNextFlush ? void 0 : this.lastPersistedVersion;
|
|
1843
|
+
const rawBundle = baseline ? flock.exportJson(baseline) : flock.exportJson();
|
|
1844
|
+
const bundle = baseline ? this.stripUnchangedEntries(rawBundle, baseline) : rawBundle;
|
|
1845
|
+
if (Object.keys(bundle.entries).length === 0) {
|
|
1846
|
+
this.forceFullOnNextFlush = false;
|
|
1847
|
+
this.lastPersistedVersion = currentVersion;
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
const encoded = textEncoder.encode(JSON.stringify(bundle));
|
|
1851
|
+
if (!this.storage) {
|
|
1852
|
+
this.lastPersistedVersion = currentVersion;
|
|
1853
|
+
this.forceFullOnNextFlush = false;
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
await this.storage.save({
|
|
1857
|
+
type: "meta",
|
|
1858
|
+
update: encoded
|
|
1859
|
+
});
|
|
1860
|
+
this.lastPersistedVersion = currentVersion;
|
|
1861
|
+
this.forceFullOnNextFlush = false;
|
|
1862
|
+
}
|
|
1863
|
+
get metaFlock() {
|
|
1864
|
+
return this.getMetaFlock();
|
|
1865
|
+
}
|
|
1866
|
+
stripUnchangedEntries(bundle, baseline) {
|
|
1867
|
+
const entries = {};
|
|
1868
|
+
for (const [key, record] of Object.entries(bundle.entries)) {
|
|
1869
|
+
const clock = this.parseClock(record.c);
|
|
1870
|
+
if (!clock) {
|
|
1871
|
+
entries[key] = record;
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
const baselineEntry = baseline[clock.peerIdHex];
|
|
1875
|
+
if (!baselineEntry) {
|
|
1876
|
+
entries[key] = record;
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
if (clock.physicalTime > baselineEntry.physicalTime || clock.physicalTime === baselineEntry.physicalTime && clock.logicalCounter > baselineEntry.logicalCounter) entries[key] = record;
|
|
1880
|
+
}
|
|
1881
|
+
return {
|
|
1882
|
+
version: bundle.version,
|
|
1883
|
+
entries
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
parseClock(raw) {
|
|
1887
|
+
if (typeof raw !== "string") return void 0;
|
|
1888
|
+
const [physicalTimeStr, logicalCounterStr, peerIdHex] = raw.split(",");
|
|
1889
|
+
if (!physicalTimeStr || !logicalCounterStr || !peerIdHex) return void 0;
|
|
1890
|
+
const physicalTime = Number(physicalTimeStr);
|
|
1891
|
+
const logicalCounter = Number(logicalCounterStr);
|
|
1892
|
+
if (!Number.isFinite(physicalTime) || !Number.isFinite(logicalCounter)) return void 0;
|
|
1893
|
+
return {
|
|
1894
|
+
physicalTime,
|
|
1895
|
+
logicalCounter,
|
|
1896
|
+
peerIdHex
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
versionsEqual(a, b) {
|
|
1900
|
+
if (!a || !b) return false;
|
|
1901
|
+
const aKeys = Object.keys(a);
|
|
1902
|
+
const bKeys = Object.keys(b);
|
|
1903
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
1904
|
+
for (const key of aKeys) {
|
|
1905
|
+
const aEntry = a[key];
|
|
1906
|
+
const bEntry = b[key];
|
|
1907
|
+
if (!aEntry || !bEntry) return false;
|
|
1908
|
+
if (aEntry.physicalTime !== bEntry.physicalTime || aEntry.logicalCounter !== bEntry.logicalCounter) return false;
|
|
1909
|
+
}
|
|
1910
|
+
return true;
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
|
|
1914
|
+
//#endregion
|
|
1915
|
+
//#region src/index.ts
|
|
1535
1916
|
const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
|
|
1917
|
+
const DEFAULT_DELETED_DOC_KEEP_MS = 720 * 60 * 60 * 1e3;
|
|
1536
1918
|
var LoroRepo = class LoroRepo {
|
|
1537
1919
|
options;
|
|
1538
1920
|
_destroyed = false;
|
|
@@ -1547,6 +1929,9 @@ var LoroRepo = class LoroRepo {
|
|
|
1547
1929
|
flockHydrator;
|
|
1548
1930
|
state;
|
|
1549
1931
|
syncRunner;
|
|
1932
|
+
metaPersister;
|
|
1933
|
+
deletedDocKeepMs;
|
|
1934
|
+
purgeWatchHandle;
|
|
1550
1935
|
constructor(options) {
|
|
1551
1936
|
this.options = options;
|
|
1552
1937
|
this.transport = options.transportAdapter;
|
|
@@ -1556,17 +1941,17 @@ var LoroRepo = class LoroRepo {
|
|
|
1556
1941
|
this.state = createRepoState();
|
|
1557
1942
|
const configuredDebounce = options.docFrontierDebounceMs;
|
|
1558
1943
|
const docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
|
|
1944
|
+
const configuredDeletedKeepMs = options.deletedDocKeepMs;
|
|
1945
|
+
this.deletedDocKeepMs = typeof configuredDeletedKeepMs === "number" && Number.isFinite(configuredDeletedKeepMs) && configuredDeletedKeepMs >= 0 ? configuredDeletedKeepMs : DEFAULT_DELETED_DOC_KEEP_MS;
|
|
1559
1946
|
this.docManager = new DocManager({
|
|
1560
1947
|
storage: this.storage,
|
|
1561
1948
|
docFrontierDebounceMs,
|
|
1562
1949
|
getMetaFlock: () => this.metaFlock,
|
|
1563
|
-
eventBus: this.eventBus
|
|
1564
|
-
persistMeta: () => this.persistMeta()
|
|
1950
|
+
eventBus: this.eventBus
|
|
1565
1951
|
});
|
|
1566
1952
|
this.metadataManager = new MetadataManager({
|
|
1567
1953
|
getMetaFlock: () => this.metaFlock,
|
|
1568
1954
|
eventBus: this.eventBus,
|
|
1569
|
-
persistMeta: () => this.persistMeta(),
|
|
1570
1955
|
state: this.state
|
|
1571
1956
|
});
|
|
1572
1957
|
this.assetManager = new AssetManager({
|
|
@@ -1574,9 +1959,13 @@ var LoroRepo = class LoroRepo {
|
|
|
1574
1959
|
assetTransport: this.assetTransport,
|
|
1575
1960
|
getMetaFlock: () => this.metaFlock,
|
|
1576
1961
|
eventBus: this.eventBus,
|
|
1577
|
-
persistMeta: () => this.persistMeta(),
|
|
1578
1962
|
state: this.state
|
|
1579
1963
|
});
|
|
1964
|
+
this.metaPersister = new MetaPersister({
|
|
1965
|
+
getMetaFlock: () => this.metaFlock,
|
|
1966
|
+
storage: this.storage,
|
|
1967
|
+
debounceMs: options.metaPersistDebounceMs
|
|
1968
|
+
});
|
|
1580
1969
|
this.flockHydrator = new FlockHydrator({
|
|
1581
1970
|
getMetaFlock: () => this.metaFlock,
|
|
1582
1971
|
metadataManager: this.metadataManager,
|
|
@@ -1594,8 +1983,11 @@ var LoroRepo = class LoroRepo {
|
|
|
1594
1983
|
getMetaFlock: () => this.metaFlock,
|
|
1595
1984
|
mergeFlock: (snapshot) => {
|
|
1596
1985
|
this.metaFlock.merge(snapshot);
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1986
|
+
}
|
|
1987
|
+
});
|
|
1988
|
+
this.purgeWatchHandle = this.eventBus.watch((event) => this.handlePurgeSignals(event), {
|
|
1989
|
+
kinds: ["doc-soft-deleted", "doc-metadata"],
|
|
1990
|
+
by: ["sync", "live"]
|
|
1599
1991
|
});
|
|
1600
1992
|
}
|
|
1601
1993
|
static async create(options) {
|
|
@@ -1612,6 +2004,19 @@ var LoroRepo = class LoroRepo {
|
|
|
1612
2004
|
*/
|
|
1613
2005
|
async ready() {
|
|
1614
2006
|
await this.syncRunner.ready();
|
|
2007
|
+
this.metaPersister.start(this.metaFlock.version());
|
|
2008
|
+
}
|
|
2009
|
+
computeDocPurgeAfter(docId, minKeepMs) {
|
|
2010
|
+
const deletedAt = this.metadataManager.getDeletedAtMs(docId);
|
|
2011
|
+
if (deletedAt === void 0) return void 0;
|
|
2012
|
+
return deletedAt + minKeepMs;
|
|
2013
|
+
}
|
|
2014
|
+
purgeDocKeyspace(docId) {
|
|
2015
|
+
const metadataKeys = Array.from(this.metaFlock.scan({ prefix: ["m", docId] }), (row) => row.key);
|
|
2016
|
+
for (const key of metadataKeys) this.metaFlock.delete(key);
|
|
2017
|
+
const frontierKeys = Array.from(this.metaFlock.scan({ prefix: ["f", docId] }), (row) => row.key);
|
|
2018
|
+
for (const key of frontierKeys) this.metaFlock.delete(key);
|
|
2019
|
+
this.metaFlock.delete(["ts", docId]);
|
|
1615
2020
|
}
|
|
1616
2021
|
/**
|
|
1617
2022
|
* Sync selected data via the transport adaptor
|
|
@@ -1623,23 +2028,64 @@ var LoroRepo = class LoroRepo {
|
|
|
1623
2028
|
/**
|
|
1624
2029
|
* Start syncing the metadata (Flock) room. It will establish a realtime connection to the transport adaptor.
|
|
1625
2030
|
* All changes on the room will be synced to the Flock, and all changes on the Flock will be synced to the room.
|
|
2031
|
+
*
|
|
2032
|
+
* - Idempotent: repeated calls reuse the same underlying room session; no extra join request is sent for the same repo.
|
|
2033
|
+
* - Reference-counted leave: every call to `joinMetaRoom` returns a subscription that increments an internal counter. The room is
|
|
2034
|
+
* actually left only after all returned subscriptions have called `unsubscribe`.
|
|
1626
2035
|
* @param params
|
|
1627
2036
|
* @returns
|
|
1628
2037
|
*/
|
|
1629
2038
|
async joinMetaRoom(params) {
|
|
1630
|
-
|
|
2039
|
+
const subscription = await this.syncRunner.joinMetaRoom(params);
|
|
2040
|
+
return {
|
|
2041
|
+
unsubscribe: subscription.unsubscribe,
|
|
2042
|
+
get connected() {
|
|
2043
|
+
return subscription.connected;
|
|
2044
|
+
},
|
|
2045
|
+
get status() {
|
|
2046
|
+
return subscription.status;
|
|
2047
|
+
},
|
|
2048
|
+
onStatusChange: subscription.onStatusChange,
|
|
2049
|
+
firstSyncedWithRemote: subscription.firstSyncedWithRemote.then(async () => {
|
|
2050
|
+
await this.metaPersister.flushNow();
|
|
2051
|
+
})
|
|
2052
|
+
};
|
|
1631
2053
|
}
|
|
1632
2054
|
/**
|
|
1633
2055
|
* Start syncing the given doc. It will establish a realtime connection to the transport adaptor.
|
|
1634
2056
|
* All changes on the doc will be synced to the transport, and all changes on the transport will be synced to the doc.
|
|
1635
2057
|
*
|
|
1636
2058
|
* All the changes on the room will be reflected on the same doc you get from `repo.openCollaborativeDoc(docId)`
|
|
2059
|
+
*
|
|
2060
|
+
* - Idempotent: multiple joins for the same `docId` reuse the existing session; no duplicate transport joins are issued.
|
|
2061
|
+
* - Reference-counted leave: each returned subscription bumps an internal counter and only the final `unsubscribe()` will
|
|
2062
|
+
* actually leave the room. Earlier unsubscribes simply decrement the counter.
|
|
1637
2063
|
* @param docId
|
|
1638
2064
|
* @param params
|
|
1639
2065
|
* @returns
|
|
1640
2066
|
*/
|
|
1641
2067
|
async joinDocRoom(docId, params) {
|
|
1642
|
-
|
|
2068
|
+
const subscription = await this.syncRunner.joinDocRoom(docId, params);
|
|
2069
|
+
return {
|
|
2070
|
+
...subscription,
|
|
2071
|
+
onStatusChange: subscription.onStatusChange,
|
|
2072
|
+
status: subscription.status
|
|
2073
|
+
};
|
|
2074
|
+
}
|
|
2075
|
+
/**
|
|
2076
|
+
* Joins an ephemeral CRDT room. This is useful for presence-like state that should not be persisted.
|
|
2077
|
+
* The returned store can be used immediately; the first sync promise resolves once the initial handshake completes.
|
|
2078
|
+
*/
|
|
2079
|
+
async joinEphemeralRoom(roomId) {
|
|
2080
|
+
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
2081
|
+
await this.syncRunner.ready();
|
|
2082
|
+
if (!this.transport.isConnected()) await this.transport.connect();
|
|
2083
|
+
const subscription = this.transport.joinEphemeralRoom(roomId);
|
|
2084
|
+
return {
|
|
2085
|
+
...subscription,
|
|
2086
|
+
onStatusChange: subscription.onStatusChange,
|
|
2087
|
+
status: subscription.status
|
|
2088
|
+
};
|
|
1643
2089
|
}
|
|
1644
2090
|
/**
|
|
1645
2091
|
* Opens a document that is automatically persisted to the configured storage adapter.
|
|
@@ -1671,6 +2117,61 @@ var LoroRepo = class LoroRepo {
|
|
|
1671
2117
|
async listDoc(query) {
|
|
1672
2118
|
return this.metadataManager.listDoc(query);
|
|
1673
2119
|
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Mark a document deleted by writing a `ts/*` tombstone entry (timestamp).
|
|
2122
|
+
* The body and metadata remain until purged; callers use the tombstone to
|
|
2123
|
+
* render deleted state or trigger retention workflows. For immediate removal,
|
|
2124
|
+
* call `purgeDoc` instead.
|
|
2125
|
+
*/
|
|
2126
|
+
async deleteDoc(docId, options = {}) {
|
|
2127
|
+
if (this.metadataManager.getDeletedAtMs(docId) !== void 0 && !options.force) return;
|
|
2128
|
+
const deletedAt = options.deletedAt ?? Date.now();
|
|
2129
|
+
this.metadataManager.markDeleted(docId, deletedAt);
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Undo a soft delete by removing the tombstone entry. Metadata and document
|
|
2133
|
+
* state remain untouched.
|
|
2134
|
+
*/
|
|
2135
|
+
async restoreDoc(docId) {
|
|
2136
|
+
if (this.metadataManager.getDeletedAtMs(docId) === void 0) return;
|
|
2137
|
+
this.metadataManager.clearDeleted(docId);
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Hard-delete a document immediately. Removes doc snapshots/updates via the
|
|
2141
|
+
* storage adapter (if supported), clears metadata/frontiers/link keys from
|
|
2142
|
+
* Flock, and unlinks assets (they become orphaned for asset GC).
|
|
2143
|
+
*/
|
|
2144
|
+
async purgeDoc(docId) {
|
|
2145
|
+
const deletedAtMs = this.metadataManager.getDeletedAtMs(docId);
|
|
2146
|
+
this.eventBus.emit({
|
|
2147
|
+
kind: "doc-purging",
|
|
2148
|
+
docId,
|
|
2149
|
+
deletedAtMs,
|
|
2150
|
+
by: "local"
|
|
2151
|
+
});
|
|
2152
|
+
await this.docManager.dropDoc(docId);
|
|
2153
|
+
this.assetManager.purgeDocLinks(docId, "local");
|
|
2154
|
+
this.purgeDocKeyspace(docId);
|
|
2155
|
+
this.metadataManager.emitSoftDeleted(docId, void 0, "local");
|
|
2156
|
+
this.metadataManager.refreshFromFlock(docId, "local");
|
|
2157
|
+
}
|
|
2158
|
+
/**
|
|
2159
|
+
* Sweep tombstoned documents whose retention window expired. Uses
|
|
2160
|
+
* `deletedDocKeepMs` by default; pass `minKeepMs`/`now` for overrides.
|
|
2161
|
+
*/
|
|
2162
|
+
async gcDeletedDocs(options = {}) {
|
|
2163
|
+
const now = options.now ?? Date.now();
|
|
2164
|
+
const minKeepMs = options.minKeepMs ?? this.deletedDocKeepMs;
|
|
2165
|
+
const docIds = this.metadataManager.getDocIds();
|
|
2166
|
+
let purged = 0;
|
|
2167
|
+
for (const docId of docIds) {
|
|
2168
|
+
const purgeAfter = this.computeDocPurgeAfter(docId, minKeepMs);
|
|
2169
|
+
if (purgeAfter === void 0 || now < purgeAfter) continue;
|
|
2170
|
+
await this.purgeDoc(docId);
|
|
2171
|
+
purged += 1;
|
|
2172
|
+
}
|
|
2173
|
+
return purged;
|
|
2174
|
+
}
|
|
1674
2175
|
getMeta() {
|
|
1675
2176
|
return this.metaFlock;
|
|
1676
2177
|
}
|
|
@@ -1699,6 +2200,7 @@ var LoroRepo = class LoroRepo {
|
|
|
1699
2200
|
}
|
|
1700
2201
|
async flush() {
|
|
1701
2202
|
await this.docManager.flush();
|
|
2203
|
+
await this.metaPersister.flushNow();
|
|
1702
2204
|
}
|
|
1703
2205
|
async uploadAsset(params) {
|
|
1704
2206
|
return this.assetManager.uploadAsset(params);
|
|
@@ -1721,26 +2223,35 @@ var LoroRepo = class LoroRepo {
|
|
|
1721
2223
|
async gcAssets(options = {}) {
|
|
1722
2224
|
return this.assetManager.gcAssets(options);
|
|
1723
2225
|
}
|
|
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
2226
|
get destroyed() {
|
|
1734
2227
|
return this._destroyed;
|
|
1735
2228
|
}
|
|
1736
2229
|
async destroy() {
|
|
1737
2230
|
if (this._destroyed) return;
|
|
1738
2231
|
this._destroyed = true;
|
|
2232
|
+
this.purgeWatchHandle.unsubscribe();
|
|
2233
|
+
await this.metaPersister.destroy();
|
|
1739
2234
|
await this.syncRunner.destroy();
|
|
1740
2235
|
this.assetTransport?.close?.();
|
|
1741
2236
|
this.storage?.close?.();
|
|
1742
2237
|
await this.transport?.close();
|
|
1743
2238
|
}
|
|
2239
|
+
handlePurgeSignals(event) {
|
|
2240
|
+
const docId = (() => {
|
|
2241
|
+
if (event.kind === "doc-soft-deleted") return event.docId;
|
|
2242
|
+
if (event.kind === "doc-metadata") return event.docId;
|
|
2243
|
+
})();
|
|
2244
|
+
if (!docId) return;
|
|
2245
|
+
const metadataCleared = event.kind === "doc-metadata" && Object.keys(event.patch).length === 0;
|
|
2246
|
+
const tombstoneCleared = event.kind === "doc-soft-deleted" && event.deletedAtMs === void 0;
|
|
2247
|
+
if (!(metadataCleared || tombstoneCleared && this.metadataManager.get(docId) === void 0)) return;
|
|
2248
|
+
this.docManager.dropDoc(docId).catch((error) => {
|
|
2249
|
+
console.error("Failed to drop purged doc", {
|
|
2250
|
+
docId,
|
|
2251
|
+
error
|
|
2252
|
+
});
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
1744
2255
|
};
|
|
1745
2256
|
|
|
1746
2257
|
//#endregion
|