loro-repo 0.5.3 → 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 +477 -95
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +47 -4
- package/dist/index.d.ts +47 -4
- package/dist/index.js +477 -95
- package/dist/index.js.map +1 -1
- package/dist/storage/filesystem.cjs +17 -0
- package/dist/storage/filesystem.cjs.map +1 -1
- package/dist/storage/filesystem.d.cts +2 -1
- package/dist/storage/filesystem.d.ts +2 -1
- package/dist/storage/filesystem.js +17 -0
- package/dist/storage/filesystem.js.map +1 -1
- package/dist/storage/indexeddb.cjs +4 -0
- package/dist/storage/indexeddb.cjs.map +1 -1
- package/dist/storage/indexeddb.d.cts +2 -1
- package/dist/storage/indexeddb.d.ts +2 -1
- package/dist/storage/indexeddb.js +4 -0
- 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 +116 -4
- package/dist/types.d.ts +116 -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;
|
|
@@ -177,6 +177,18 @@ var DocManager = class {
|
|
|
177
177
|
this.docs.delete(docId);
|
|
178
178
|
this.docPersistedVersions.delete(docId);
|
|
179
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
|
+
}
|
|
180
192
|
async flush() {
|
|
181
193
|
const promises = [];
|
|
182
194
|
for (const [docId, doc] of this.docs) promises.push((async () => {
|
|
@@ -504,13 +516,44 @@ var MetadataManager = class {
|
|
|
504
516
|
this.state = options.state;
|
|
505
517
|
}
|
|
506
518
|
getDocIds() {
|
|
507
|
-
|
|
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);
|
|
508
527
|
}
|
|
509
528
|
entries() {
|
|
510
529
|
return this.state.metadata.entries();
|
|
511
530
|
}
|
|
512
531
|
get(docId) {
|
|
513
|
-
|
|
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");
|
|
514
557
|
}
|
|
515
558
|
listDoc(query) {
|
|
516
559
|
if (query?.limit !== void 0 && query.limit <= 0) return [];
|
|
@@ -525,25 +568,49 @@ var MetadataManager = class {
|
|
|
525
568
|
kind: "exclusive",
|
|
526
569
|
key: ["m", endKey]
|
|
527
570
|
};
|
|
528
|
-
const rows = this.metaFlock.scan(scanOptions);
|
|
529
571
|
const seen = /* @__PURE__ */ new Set();
|
|
530
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);
|
|
531
587
|
for (const row of rows) {
|
|
532
588
|
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
533
589
|
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
534
590
|
const docId = row.key[1];
|
|
535
591
|
if (typeof docId !== "string") continue;
|
|
536
|
-
|
|
537
|
-
seen.add(docId);
|
|
538
|
-
const metadata = this.state.metadata.get(docId);
|
|
539
|
-
if (!metadata) continue;
|
|
540
|
-
if (!matchesQuery(docId, metadata, query)) continue;
|
|
541
|
-
entries.push({
|
|
542
|
-
docId,
|
|
543
|
-
meta: cloneJsonObject(metadata)
|
|
544
|
-
});
|
|
592
|
+
pushDoc(docId);
|
|
545
593
|
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
546
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
|
+
}
|
|
547
614
|
return entries;
|
|
548
615
|
}
|
|
549
616
|
async upsert(docId, patch) {
|
|
@@ -556,11 +623,10 @@ var MetadataManager = class {
|
|
|
556
623
|
const rawValue = patchObject[key];
|
|
557
624
|
if (rawValue === void 0) continue;
|
|
558
625
|
if (jsonEquals(base ? base[key] : void 0, rawValue)) continue;
|
|
559
|
-
const storageKey = key === "tombstone" ? "$tombstone" : key;
|
|
560
626
|
this.metaFlock.put([
|
|
561
627
|
"m",
|
|
562
628
|
docId,
|
|
563
|
-
|
|
629
|
+
key
|
|
564
630
|
], rawValue);
|
|
565
631
|
next[key] = rawValue;
|
|
566
632
|
outPatch[key] = rawValue;
|
|
@@ -579,10 +645,10 @@ var MetadataManager = class {
|
|
|
579
645
|
});
|
|
580
646
|
}
|
|
581
647
|
refreshFromFlock(docId, by) {
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
if (!
|
|
585
|
-
if (
|
|
648
|
+
const previousMeta = this.state.metadata.get(docId);
|
|
649
|
+
const nextMeta = this.readDocMetadataFromFlock(docId);
|
|
650
|
+
if (!nextMeta) {
|
|
651
|
+
if (previousMeta) {
|
|
586
652
|
this.state.metadata.delete(docId);
|
|
587
653
|
this.eventBus.emit({
|
|
588
654
|
kind: "doc-metadata",
|
|
@@ -593,9 +659,9 @@ var MetadataManager = class {
|
|
|
593
659
|
}
|
|
594
660
|
return;
|
|
595
661
|
}
|
|
596
|
-
this.state.metadata.set(docId,
|
|
597
|
-
const patch = diffJsonObjects(
|
|
598
|
-
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({
|
|
599
665
|
kind: "doc-metadata",
|
|
600
666
|
docId,
|
|
601
667
|
patch,
|
|
@@ -611,12 +677,16 @@ var MetadataManager = class {
|
|
|
611
677
|
const previous = prevMetadata.get(docId);
|
|
612
678
|
const current = nextMetadata.get(docId);
|
|
613
679
|
if (!current) {
|
|
614
|
-
if (previous)
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
+
}
|
|
620
690
|
continue;
|
|
621
691
|
}
|
|
622
692
|
const patch = diffJsonObjects(previous, current);
|
|
@@ -631,6 +701,22 @@ var MetadataManager = class {
|
|
|
631
701
|
clear() {
|
|
632
702
|
this.state.metadata.clear();
|
|
633
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
|
+
}
|
|
634
720
|
computeDocRangeKeys(query) {
|
|
635
721
|
if (!query) return {};
|
|
636
722
|
const prefix = query.prefix && query.prefix.length > 0 ? query.prefix : void 0;
|
|
@@ -656,7 +742,9 @@ var MetadataManager = class {
|
|
|
656
742
|
if (!rows.length) return void 0;
|
|
657
743
|
const docMeta = {};
|
|
658
744
|
let populated = false;
|
|
745
|
+
let sawRow = false;
|
|
659
746
|
for (const row of rows) {
|
|
747
|
+
sawRow = true;
|
|
660
748
|
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
661
749
|
if (row.key.length === 2) {
|
|
662
750
|
const obj = asJsonObject(row.value);
|
|
@@ -672,17 +760,33 @@ var MetadataManager = class {
|
|
|
672
760
|
}
|
|
673
761
|
const fieldKey = row.key[2];
|
|
674
762
|
if (typeof fieldKey !== "string") continue;
|
|
675
|
-
if (fieldKey === "$tombstone") {
|
|
676
|
-
docMeta.tombstone = Boolean(row.value);
|
|
677
|
-
populated = true;
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
763
|
const jsonValue = cloneJsonValue(row.value);
|
|
681
764
|
if (jsonValue === void 0) continue;
|
|
682
765
|
docMeta[fieldKey] = jsonValue;
|
|
683
766
|
populated = true;
|
|
684
767
|
}
|
|
685
|
-
|
|
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;
|
|
686
790
|
}
|
|
687
791
|
get metaFlock() {
|
|
688
792
|
return this.getMetaFlock();
|
|
@@ -921,6 +1025,11 @@ var AssetManager = class {
|
|
|
921
1025
|
by: "local"
|
|
922
1026
|
});
|
|
923
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
|
+
}
|
|
924
1033
|
async listAssets(docId) {
|
|
925
1034
|
const mapping = this.docAssets.get(docId);
|
|
926
1035
|
if (!mapping) return [];
|
|
@@ -1269,14 +1378,35 @@ var FlockHydrator = class {
|
|
|
1269
1378
|
this.docManager = options.docManager;
|
|
1270
1379
|
}
|
|
1271
1380
|
hydrateAll(by) {
|
|
1272
|
-
const
|
|
1273
|
-
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);
|
|
1274
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
|
+
})();
|
|
1275
1403
|
}
|
|
1276
|
-
applyEvents(events, by) {
|
|
1404
|
+
async applyEvents(events, by) {
|
|
1277
1405
|
if (!events.length) return;
|
|
1278
1406
|
const docMetadataIds = /* @__PURE__ */ new Set();
|
|
1407
|
+
const docTombstoneIds = /* @__PURE__ */ new Map();
|
|
1279
1408
|
const docAssetIds = /* @__PURE__ */ new Set();
|
|
1409
|
+
const docFrontierIds = /* @__PURE__ */ new Set();
|
|
1280
1410
|
const assetIds = /* @__PURE__ */ new Set();
|
|
1281
1411
|
for (const event of events) {
|
|
1282
1412
|
const key = event.key;
|
|
@@ -1285,6 +1415,12 @@ var FlockHydrator = class {
|
|
|
1285
1415
|
if (root === "m") {
|
|
1286
1416
|
const docId = key[1];
|
|
1287
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));
|
|
1288
1424
|
} else if (root === "a") {
|
|
1289
1425
|
const assetId = key[1];
|
|
1290
1426
|
if (typeof assetId === "string") assetIds.add(assetId);
|
|
@@ -1297,7 +1433,38 @@ var FlockHydrator = class {
|
|
|
1297
1433
|
}
|
|
1298
1434
|
for (const assetId of assetIds) this.assetManager.refreshAssetMetadataEntry(assetId, by);
|
|
1299
1435
|
for (const docId of docMetadataIds) this.metadataManager.refreshFromFlock(docId, by);
|
|
1436
|
+
for (const [docId, deletedAtMs] of docTombstoneIds) this.metadataManager.emitSoftDeleted(docId, deletedAtMs, by);
|
|
1300
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
|
+
}
|
|
1301
1468
|
}
|
|
1302
1469
|
readAllDocMetadata() {
|
|
1303
1470
|
const nextMetadata = /* @__PURE__ */ new Map();
|
|
@@ -1306,35 +1473,86 @@ var FlockHydrator = class {
|
|
|
1306
1473
|
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1307
1474
|
const docId = row.key[1];
|
|
1308
1475
|
if (typeof docId !== "string") continue;
|
|
1309
|
-
|
|
1310
|
-
if (!docMeta) {
|
|
1311
|
-
docMeta = {};
|
|
1312
|
-
nextMetadata.set(docId, docMeta);
|
|
1313
|
-
}
|
|
1476
|
+
if (row.value === void 0 && !this.metadataManager.get(docId)) continue;
|
|
1314
1477
|
if (row.key.length === 2) {
|
|
1315
1478
|
const obj = asJsonObject(row.value);
|
|
1316
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
|
+
}
|
|
1317
1485
|
for (const [field, value] of Object.entries(obj)) {
|
|
1318
1486
|
const cloned = cloneJsonValue(value);
|
|
1319
|
-
if (cloned !== void 0) docMeta[field] = cloned;
|
|
1487
|
+
if (cloned !== void 0) docMeta$1[field] = cloned;
|
|
1320
1488
|
}
|
|
1321
1489
|
continue;
|
|
1322
1490
|
}
|
|
1323
1491
|
const fieldKey = row.key[2];
|
|
1324
1492
|
if (typeof fieldKey !== "string") continue;
|
|
1325
|
-
if (fieldKey === "$tombstone") {
|
|
1326
|
-
docMeta.tombstone = Boolean(row.value);
|
|
1327
|
-
continue;
|
|
1328
|
-
}
|
|
1329
1493
|
const jsonValue = cloneJsonValue(row.value);
|
|
1330
1494
|
if (jsonValue === void 0) continue;
|
|
1495
|
+
let docMeta = nextMetadata.get(docId);
|
|
1496
|
+
if (!docMeta) {
|
|
1497
|
+
docMeta = {};
|
|
1498
|
+
nextMetadata.set(docId, docMeta);
|
|
1499
|
+
}
|
|
1331
1500
|
docMeta[fieldKey] = jsonValue;
|
|
1332
1501
|
}
|
|
1333
|
-
|
|
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;
|
|
1334
1518
|
}
|
|
1335
1519
|
get metaFlock() {
|
|
1336
1520
|
return this.getMetaFlock();
|
|
1337
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
|
+
}
|
|
1338
1556
|
};
|
|
1339
1557
|
|
|
1340
1558
|
//#endregion
|
|
@@ -1385,7 +1603,7 @@ var SyncRunner = class {
|
|
|
1385
1603
|
});
|
|
1386
1604
|
try {
|
|
1387
1605
|
if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
|
|
1388
|
-
if (recordedEvents.length > 0) this.flockHydrator.applyEvents(recordedEvents, "sync");
|
|
1606
|
+
if (recordedEvents.length > 0) await this.flockHydrator.applyEvents(recordedEvents, "sync");
|
|
1389
1607
|
else this.flockHydrator.hydrateAll("sync");
|
|
1390
1608
|
} finally {
|
|
1391
1609
|
unsubscribe();
|
|
@@ -1411,62 +1629,105 @@ var SyncRunner = class {
|
|
|
1411
1629
|
await this.ready();
|
|
1412
1630
|
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
1413
1631
|
if (!this.transport.isConnected()) await this.transport.connect();
|
|
1414
|
-
|
|
1632
|
+
const existing = this.metaRoomSubscription;
|
|
1633
|
+
if (existing) {
|
|
1634
|
+
existing.refCount += 1;
|
|
1635
|
+
return this.createMetaLease(existing);
|
|
1636
|
+
}
|
|
1415
1637
|
this.ensureMetaLiveMonitor();
|
|
1416
|
-
const
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
if (this.metaRoomSubscription === wrapped) this.metaRoomSubscription = void 0;
|
|
1421
|
-
if (this.unsubscribeMetaFlock) {
|
|
1422
|
-
this.unsubscribeMetaFlock();
|
|
1423
|
-
this.unsubscribeMetaFlock = void 0;
|
|
1424
|
-
}
|
|
1425
|
-
},
|
|
1426
|
-
firstSyncedWithRemote: subscription.firstSyncedWithRemote,
|
|
1427
|
-
get connected() {
|
|
1428
|
-
return subscription.connected;
|
|
1429
|
-
}
|
|
1638
|
+
const base = this.transport.joinMetaRoom(this.metaFlock, params);
|
|
1639
|
+
const record = {
|
|
1640
|
+
base,
|
|
1641
|
+
refCount: 1
|
|
1430
1642
|
};
|
|
1431
|
-
this.metaRoomSubscription =
|
|
1432
|
-
|
|
1643
|
+
this.metaRoomSubscription = record;
|
|
1644
|
+
base.firstSyncedWithRemote.then(async () => {
|
|
1433
1645
|
const by = this.eventBus.resolveEventBy("live");
|
|
1434
1646
|
this.flockHydrator.hydrateAll(by);
|
|
1435
1647
|
}).catch(logAsyncError("meta room first sync"));
|
|
1436
|
-
return
|
|
1648
|
+
return this.createMetaLease(record);
|
|
1437
1649
|
}
|
|
1438
1650
|
async joinDocRoom(docId, params) {
|
|
1439
1651
|
await this.ready();
|
|
1440
1652
|
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
1441
1653
|
if (!this.transport.isConnected()) await this.transport.connect();
|
|
1442
1654
|
const existing = this.docSubscriptions.get(docId);
|
|
1443
|
-
if (existing)
|
|
1655
|
+
if (existing) {
|
|
1656
|
+
existing.refCount += 1;
|
|
1657
|
+
return this.createDocLease(docId, existing);
|
|
1658
|
+
}
|
|
1444
1659
|
const doc = await this.docManager.ensureDoc(docId);
|
|
1445
|
-
const
|
|
1446
|
-
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 {
|
|
1447
1672
|
unsubscribe: () => {
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
+
}
|
|
1450
1686
|
},
|
|
1451
|
-
firstSyncedWithRemote:
|
|
1687
|
+
firstSyncedWithRemote: record.base.firstSyncedWithRemote,
|
|
1452
1688
|
get connected() {
|
|
1453
|
-
return
|
|
1454
|
-
}
|
|
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
|
|
1455
1719
|
};
|
|
1456
|
-
this.docSubscriptions.set(docId, wrapped);
|
|
1457
|
-
subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
1458
|
-
return wrapped;
|
|
1459
1720
|
}
|
|
1460
1721
|
async destroy() {
|
|
1461
1722
|
await this.docManager.close();
|
|
1462
|
-
this.metaRoomSubscription
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
this.docSubscriptions.clear();
|
|
1466
|
-
if (this.unsubscribeMetaFlock) {
|
|
1467
|
-
this.unsubscribeMetaFlock();
|
|
1468
|
-
this.unsubscribeMetaFlock = void 0;
|
|
1723
|
+
if (this.metaRoomSubscription) {
|
|
1724
|
+
this.metaRoomSubscription.base.unsubscribe();
|
|
1725
|
+
this.metaRoomSubscription = void 0;
|
|
1469
1726
|
}
|
|
1727
|
+
for (const record of this.docSubscriptions.values()) record.base.unsubscribe();
|
|
1728
|
+
this.docSubscriptions.clear();
|
|
1729
|
+
this.unsubscribeMetaFlock?.();
|
|
1730
|
+
this.unsubscribeMetaFlock = void 0;
|
|
1470
1731
|
this.eventBus.clear();
|
|
1471
1732
|
this.metadataManager.clear();
|
|
1472
1733
|
this.assetManager.clear();
|
|
@@ -1486,7 +1747,7 @@ var SyncRunner = class {
|
|
|
1486
1747
|
if (batch.source === "local") return;
|
|
1487
1748
|
const by = this.eventBus.resolveEventBy("live");
|
|
1488
1749
|
(async () => {
|
|
1489
|
-
this.flockHydrator.applyEvents(batch.events, by);
|
|
1750
|
+
await this.flockHydrator.applyEvents(batch.events, by);
|
|
1490
1751
|
})().catch(logAsyncError("meta live monitor sync"));
|
|
1491
1752
|
});
|
|
1492
1753
|
}
|
|
@@ -1592,14 +1853,10 @@ var MetaPersister = class {
|
|
|
1592
1853
|
this.forceFullOnNextFlush = false;
|
|
1593
1854
|
return;
|
|
1594
1855
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
});
|
|
1600
|
-
} catch (error) {
|
|
1601
|
-
throw error;
|
|
1602
|
-
}
|
|
1856
|
+
await this.storage.save({
|
|
1857
|
+
type: "meta",
|
|
1858
|
+
update: encoded
|
|
1859
|
+
});
|
|
1603
1860
|
this.lastPersistedVersion = currentVersion;
|
|
1604
1861
|
this.forceFullOnNextFlush = false;
|
|
1605
1862
|
}
|
|
@@ -1657,6 +1914,7 @@ var MetaPersister = class {
|
|
|
1657
1914
|
//#endregion
|
|
1658
1915
|
//#region src/index.ts
|
|
1659
1916
|
const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
|
|
1917
|
+
const DEFAULT_DELETED_DOC_KEEP_MS = 720 * 60 * 60 * 1e3;
|
|
1660
1918
|
var LoroRepo = class LoroRepo {
|
|
1661
1919
|
options;
|
|
1662
1920
|
_destroyed = false;
|
|
@@ -1672,6 +1930,8 @@ var LoroRepo = class LoroRepo {
|
|
|
1672
1930
|
state;
|
|
1673
1931
|
syncRunner;
|
|
1674
1932
|
metaPersister;
|
|
1933
|
+
deletedDocKeepMs;
|
|
1934
|
+
purgeWatchHandle;
|
|
1675
1935
|
constructor(options) {
|
|
1676
1936
|
this.options = options;
|
|
1677
1937
|
this.transport = options.transportAdapter;
|
|
@@ -1681,6 +1941,8 @@ var LoroRepo = class LoroRepo {
|
|
|
1681
1941
|
this.state = createRepoState();
|
|
1682
1942
|
const configuredDebounce = options.docFrontierDebounceMs;
|
|
1683
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;
|
|
1684
1946
|
this.docManager = new DocManager({
|
|
1685
1947
|
storage: this.storage,
|
|
1686
1948
|
docFrontierDebounceMs,
|
|
@@ -1723,6 +1985,10 @@ var LoroRepo = class LoroRepo {
|
|
|
1723
1985
|
this.metaFlock.merge(snapshot);
|
|
1724
1986
|
}
|
|
1725
1987
|
});
|
|
1988
|
+
this.purgeWatchHandle = this.eventBus.watch((event) => this.handlePurgeSignals(event), {
|
|
1989
|
+
kinds: ["doc-soft-deleted", "doc-metadata"],
|
|
1990
|
+
by: ["sync", "live"]
|
|
1991
|
+
});
|
|
1726
1992
|
}
|
|
1727
1993
|
static async create(options) {
|
|
1728
1994
|
const repo = new LoroRepo(options);
|
|
@@ -1740,6 +2006,18 @@ var LoroRepo = class LoroRepo {
|
|
|
1740
2006
|
await this.syncRunner.ready();
|
|
1741
2007
|
this.metaPersister.start(this.metaFlock.version());
|
|
1742
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]);
|
|
2020
|
+
}
|
|
1743
2021
|
/**
|
|
1744
2022
|
* Sync selected data via the transport adaptor
|
|
1745
2023
|
* @param options
|
|
@@ -1750,6 +2028,10 @@ var LoroRepo = class LoroRepo {
|
|
|
1750
2028
|
/**
|
|
1751
2029
|
* Start syncing the metadata (Flock) room. It will establish a realtime connection to the transport adaptor.
|
|
1752
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`.
|
|
1753
2035
|
* @param params
|
|
1754
2036
|
* @returns
|
|
1755
2037
|
*/
|
|
@@ -1760,6 +2042,10 @@ var LoroRepo = class LoroRepo {
|
|
|
1760
2042
|
get connected() {
|
|
1761
2043
|
return subscription.connected;
|
|
1762
2044
|
},
|
|
2045
|
+
get status() {
|
|
2046
|
+
return subscription.status;
|
|
2047
|
+
},
|
|
2048
|
+
onStatusChange: subscription.onStatusChange,
|
|
1763
2049
|
firstSyncedWithRemote: subscription.firstSyncedWithRemote.then(async () => {
|
|
1764
2050
|
await this.metaPersister.flushNow();
|
|
1765
2051
|
})
|
|
@@ -1770,12 +2056,36 @@ var LoroRepo = class LoroRepo {
|
|
|
1770
2056
|
* All changes on the doc will be synced to the transport, and all changes on the transport will be synced to the doc.
|
|
1771
2057
|
*
|
|
1772
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.
|
|
1773
2063
|
* @param docId
|
|
1774
2064
|
* @param params
|
|
1775
2065
|
* @returns
|
|
1776
2066
|
*/
|
|
1777
2067
|
async joinDocRoom(docId, params) {
|
|
1778
|
-
|
|
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
|
+
};
|
|
1779
2089
|
}
|
|
1780
2090
|
/**
|
|
1781
2091
|
* Opens a document that is automatically persisted to the configured storage adapter.
|
|
@@ -1807,6 +2117,61 @@ var LoroRepo = class LoroRepo {
|
|
|
1807
2117
|
async listDoc(query) {
|
|
1808
2118
|
return this.metadataManager.listDoc(query);
|
|
1809
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
|
+
}
|
|
1810
2175
|
getMeta() {
|
|
1811
2176
|
return this.metaFlock;
|
|
1812
2177
|
}
|
|
@@ -1864,12 +2229,29 @@ var LoroRepo = class LoroRepo {
|
|
|
1864
2229
|
async destroy() {
|
|
1865
2230
|
if (this._destroyed) return;
|
|
1866
2231
|
this._destroyed = true;
|
|
2232
|
+
this.purgeWatchHandle.unsubscribe();
|
|
1867
2233
|
await this.metaPersister.destroy();
|
|
1868
2234
|
await this.syncRunner.destroy();
|
|
1869
2235
|
this.assetTransport?.close?.();
|
|
1870
2236
|
this.storage?.close?.();
|
|
1871
2237
|
await this.transport?.close();
|
|
1872
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
|
+
}
|
|
1873
2255
|
};
|
|
1874
2256
|
|
|
1875
2257
|
//#endregion
|