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