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.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;
|
|
@@ -174,6 +174,18 @@ var DocManager = class {
|
|
|
174
174
|
this.docs.delete(docId);
|
|
175
175
|
this.docPersistedVersions.delete(docId);
|
|
176
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
|
+
}
|
|
177
189
|
async flush() {
|
|
178
190
|
const promises = [];
|
|
179
191
|
for (const [docId, doc] of this.docs) promises.push((async () => {
|
|
@@ -501,13 +513,44 @@ var MetadataManager = class {
|
|
|
501
513
|
this.state = options.state;
|
|
502
514
|
}
|
|
503
515
|
getDocIds() {
|
|
504
|
-
|
|
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);
|
|
505
524
|
}
|
|
506
525
|
entries() {
|
|
507
526
|
return this.state.metadata.entries();
|
|
508
527
|
}
|
|
509
528
|
get(docId) {
|
|
510
|
-
|
|
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");
|
|
511
554
|
}
|
|
512
555
|
listDoc(query) {
|
|
513
556
|
if (query?.limit !== void 0 && query.limit <= 0) return [];
|
|
@@ -522,25 +565,49 @@ var MetadataManager = class {
|
|
|
522
565
|
kind: "exclusive",
|
|
523
566
|
key: ["m", endKey]
|
|
524
567
|
};
|
|
525
|
-
const rows = this.metaFlock.scan(scanOptions);
|
|
526
568
|
const seen = /* @__PURE__ */ new Set();
|
|
527
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);
|
|
528
584
|
for (const row of rows) {
|
|
529
585
|
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
530
586
|
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
531
587
|
const docId = row.key[1];
|
|
532
588
|
if (typeof docId !== "string") continue;
|
|
533
|
-
|
|
534
|
-
seen.add(docId);
|
|
535
|
-
const metadata = this.state.metadata.get(docId);
|
|
536
|
-
if (!metadata) continue;
|
|
537
|
-
if (!matchesQuery(docId, metadata, query)) continue;
|
|
538
|
-
entries.push({
|
|
539
|
-
docId,
|
|
540
|
-
meta: cloneJsonObject(metadata)
|
|
541
|
-
});
|
|
589
|
+
pushDoc(docId);
|
|
542
590
|
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
543
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
|
+
}
|
|
544
611
|
return entries;
|
|
545
612
|
}
|
|
546
613
|
async upsert(docId, patch) {
|
|
@@ -553,11 +620,10 @@ var MetadataManager = class {
|
|
|
553
620
|
const rawValue = patchObject[key];
|
|
554
621
|
if (rawValue === void 0) continue;
|
|
555
622
|
if (jsonEquals(base ? base[key] : void 0, rawValue)) continue;
|
|
556
|
-
const storageKey = key === "tombstone" ? "$tombstone" : key;
|
|
557
623
|
this.metaFlock.put([
|
|
558
624
|
"m",
|
|
559
625
|
docId,
|
|
560
|
-
|
|
626
|
+
key
|
|
561
627
|
], rawValue);
|
|
562
628
|
next[key] = rawValue;
|
|
563
629
|
outPatch[key] = rawValue;
|
|
@@ -576,10 +642,10 @@ var MetadataManager = class {
|
|
|
576
642
|
});
|
|
577
643
|
}
|
|
578
644
|
refreshFromFlock(docId, by) {
|
|
579
|
-
const
|
|
580
|
-
const
|
|
581
|
-
if (!
|
|
582
|
-
if (
|
|
645
|
+
const previousMeta = this.state.metadata.get(docId);
|
|
646
|
+
const nextMeta = this.readDocMetadataFromFlock(docId);
|
|
647
|
+
if (!nextMeta) {
|
|
648
|
+
if (previousMeta) {
|
|
583
649
|
this.state.metadata.delete(docId);
|
|
584
650
|
this.eventBus.emit({
|
|
585
651
|
kind: "doc-metadata",
|
|
@@ -590,9 +656,9 @@ var MetadataManager = class {
|
|
|
590
656
|
}
|
|
591
657
|
return;
|
|
592
658
|
}
|
|
593
|
-
this.state.metadata.set(docId,
|
|
594
|
-
const patch = diffJsonObjects(
|
|
595
|
-
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({
|
|
596
662
|
kind: "doc-metadata",
|
|
597
663
|
docId,
|
|
598
664
|
patch,
|
|
@@ -608,12 +674,16 @@ var MetadataManager = class {
|
|
|
608
674
|
const previous = prevMetadata.get(docId);
|
|
609
675
|
const current = nextMetadata.get(docId);
|
|
610
676
|
if (!current) {
|
|
611
|
-
if (previous)
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
+
}
|
|
617
687
|
continue;
|
|
618
688
|
}
|
|
619
689
|
const patch = diffJsonObjects(previous, current);
|
|
@@ -628,6 +698,22 @@ var MetadataManager = class {
|
|
|
628
698
|
clear() {
|
|
629
699
|
this.state.metadata.clear();
|
|
630
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
|
+
}
|
|
631
717
|
computeDocRangeKeys(query) {
|
|
632
718
|
if (!query) return {};
|
|
633
719
|
const prefix = query.prefix && query.prefix.length > 0 ? query.prefix : void 0;
|
|
@@ -653,7 +739,9 @@ var MetadataManager = class {
|
|
|
653
739
|
if (!rows.length) return void 0;
|
|
654
740
|
const docMeta = {};
|
|
655
741
|
let populated = false;
|
|
742
|
+
let sawRow = false;
|
|
656
743
|
for (const row of rows) {
|
|
744
|
+
sawRow = true;
|
|
657
745
|
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
658
746
|
if (row.key.length === 2) {
|
|
659
747
|
const obj = asJsonObject(row.value);
|
|
@@ -669,17 +757,33 @@ var MetadataManager = class {
|
|
|
669
757
|
}
|
|
670
758
|
const fieldKey = row.key[2];
|
|
671
759
|
if (typeof fieldKey !== "string") continue;
|
|
672
|
-
if (fieldKey === "$tombstone") {
|
|
673
|
-
docMeta.tombstone = Boolean(row.value);
|
|
674
|
-
populated = true;
|
|
675
|
-
continue;
|
|
676
|
-
}
|
|
677
760
|
const jsonValue = cloneJsonValue(row.value);
|
|
678
761
|
if (jsonValue === void 0) continue;
|
|
679
762
|
docMeta[fieldKey] = jsonValue;
|
|
680
763
|
populated = true;
|
|
681
764
|
}
|
|
682
|
-
|
|
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;
|
|
683
787
|
}
|
|
684
788
|
get metaFlock() {
|
|
685
789
|
return this.getMetaFlock();
|
|
@@ -918,6 +1022,11 @@ var AssetManager = class {
|
|
|
918
1022
|
by: "local"
|
|
919
1023
|
});
|
|
920
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
|
+
}
|
|
921
1030
|
async listAssets(docId) {
|
|
922
1031
|
const mapping = this.docAssets.get(docId);
|
|
923
1032
|
if (!mapping) return [];
|
|
@@ -1266,14 +1375,35 @@ var FlockHydrator = class {
|
|
|
1266
1375
|
this.docManager = options.docManager;
|
|
1267
1376
|
}
|
|
1268
1377
|
hydrateAll(by) {
|
|
1269
|
-
const
|
|
1270
|
-
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);
|
|
1271
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
|
+
})();
|
|
1272
1400
|
}
|
|
1273
|
-
applyEvents(events, by) {
|
|
1401
|
+
async applyEvents(events, by) {
|
|
1274
1402
|
if (!events.length) return;
|
|
1275
1403
|
const docMetadataIds = /* @__PURE__ */ new Set();
|
|
1404
|
+
const docTombstoneIds = /* @__PURE__ */ new Map();
|
|
1276
1405
|
const docAssetIds = /* @__PURE__ */ new Set();
|
|
1406
|
+
const docFrontierIds = /* @__PURE__ */ new Set();
|
|
1277
1407
|
const assetIds = /* @__PURE__ */ new Set();
|
|
1278
1408
|
for (const event of events) {
|
|
1279
1409
|
const key = event.key;
|
|
@@ -1282,6 +1412,12 @@ var FlockHydrator = class {
|
|
|
1282
1412
|
if (root === "m") {
|
|
1283
1413
|
const docId = key[1];
|
|
1284
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));
|
|
1285
1421
|
} else if (root === "a") {
|
|
1286
1422
|
const assetId = key[1];
|
|
1287
1423
|
if (typeof assetId === "string") assetIds.add(assetId);
|
|
@@ -1294,7 +1430,38 @@ var FlockHydrator = class {
|
|
|
1294
1430
|
}
|
|
1295
1431
|
for (const assetId of assetIds) this.assetManager.refreshAssetMetadataEntry(assetId, by);
|
|
1296
1432
|
for (const docId of docMetadataIds) this.metadataManager.refreshFromFlock(docId, by);
|
|
1433
|
+
for (const [docId, deletedAtMs] of docTombstoneIds) this.metadataManager.emitSoftDeleted(docId, deletedAtMs, by);
|
|
1297
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
|
+
}
|
|
1298
1465
|
}
|
|
1299
1466
|
readAllDocMetadata() {
|
|
1300
1467
|
const nextMetadata = /* @__PURE__ */ new Map();
|
|
@@ -1303,35 +1470,86 @@ var FlockHydrator = class {
|
|
|
1303
1470
|
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1304
1471
|
const docId = row.key[1];
|
|
1305
1472
|
if (typeof docId !== "string") continue;
|
|
1306
|
-
|
|
1307
|
-
if (!docMeta) {
|
|
1308
|
-
docMeta = {};
|
|
1309
|
-
nextMetadata.set(docId, docMeta);
|
|
1310
|
-
}
|
|
1473
|
+
if (row.value === void 0 && !this.metadataManager.get(docId)) continue;
|
|
1311
1474
|
if (row.key.length === 2) {
|
|
1312
1475
|
const obj = asJsonObject(row.value);
|
|
1313
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
|
+
}
|
|
1314
1482
|
for (const [field, value] of Object.entries(obj)) {
|
|
1315
1483
|
const cloned = cloneJsonValue(value);
|
|
1316
|
-
if (cloned !== void 0) docMeta[field] = cloned;
|
|
1484
|
+
if (cloned !== void 0) docMeta$1[field] = cloned;
|
|
1317
1485
|
}
|
|
1318
1486
|
continue;
|
|
1319
1487
|
}
|
|
1320
1488
|
const fieldKey = row.key[2];
|
|
1321
1489
|
if (typeof fieldKey !== "string") continue;
|
|
1322
|
-
if (fieldKey === "$tombstone") {
|
|
1323
|
-
docMeta.tombstone = Boolean(row.value);
|
|
1324
|
-
continue;
|
|
1325
|
-
}
|
|
1326
1490
|
const jsonValue = cloneJsonValue(row.value);
|
|
1327
1491
|
if (jsonValue === void 0) continue;
|
|
1492
|
+
let docMeta = nextMetadata.get(docId);
|
|
1493
|
+
if (!docMeta) {
|
|
1494
|
+
docMeta = {};
|
|
1495
|
+
nextMetadata.set(docId, docMeta);
|
|
1496
|
+
}
|
|
1328
1497
|
docMeta[fieldKey] = jsonValue;
|
|
1329
1498
|
}
|
|
1330
|
-
|
|
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;
|
|
1331
1515
|
}
|
|
1332
1516
|
get metaFlock() {
|
|
1333
1517
|
return this.getMetaFlock();
|
|
1334
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
|
+
}
|
|
1335
1553
|
};
|
|
1336
1554
|
|
|
1337
1555
|
//#endregion
|
|
@@ -1382,7 +1600,7 @@ var SyncRunner = class {
|
|
|
1382
1600
|
});
|
|
1383
1601
|
try {
|
|
1384
1602
|
if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
|
|
1385
|
-
if (recordedEvents.length > 0) this.flockHydrator.applyEvents(recordedEvents, "sync");
|
|
1603
|
+
if (recordedEvents.length > 0) await this.flockHydrator.applyEvents(recordedEvents, "sync");
|
|
1386
1604
|
else this.flockHydrator.hydrateAll("sync");
|
|
1387
1605
|
} finally {
|
|
1388
1606
|
unsubscribe();
|
|
@@ -1408,62 +1626,105 @@ var SyncRunner = class {
|
|
|
1408
1626
|
await this.ready();
|
|
1409
1627
|
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
1410
1628
|
if (!this.transport.isConnected()) await this.transport.connect();
|
|
1411
|
-
|
|
1629
|
+
const existing = this.metaRoomSubscription;
|
|
1630
|
+
if (existing) {
|
|
1631
|
+
existing.refCount += 1;
|
|
1632
|
+
return this.createMetaLease(existing);
|
|
1633
|
+
}
|
|
1412
1634
|
this.ensureMetaLiveMonitor();
|
|
1413
|
-
const
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
if (this.metaRoomSubscription === wrapped) this.metaRoomSubscription = void 0;
|
|
1418
|
-
if (this.unsubscribeMetaFlock) {
|
|
1419
|
-
this.unsubscribeMetaFlock();
|
|
1420
|
-
this.unsubscribeMetaFlock = void 0;
|
|
1421
|
-
}
|
|
1422
|
-
},
|
|
1423
|
-
firstSyncedWithRemote: subscription.firstSyncedWithRemote,
|
|
1424
|
-
get connected() {
|
|
1425
|
-
return subscription.connected;
|
|
1426
|
-
}
|
|
1635
|
+
const base = this.transport.joinMetaRoom(this.metaFlock, params);
|
|
1636
|
+
const record = {
|
|
1637
|
+
base,
|
|
1638
|
+
refCount: 1
|
|
1427
1639
|
};
|
|
1428
|
-
this.metaRoomSubscription =
|
|
1429
|
-
|
|
1640
|
+
this.metaRoomSubscription = record;
|
|
1641
|
+
base.firstSyncedWithRemote.then(async () => {
|
|
1430
1642
|
const by = this.eventBus.resolveEventBy("live");
|
|
1431
1643
|
this.flockHydrator.hydrateAll(by);
|
|
1432
1644
|
}).catch(logAsyncError("meta room first sync"));
|
|
1433
|
-
return
|
|
1645
|
+
return this.createMetaLease(record);
|
|
1434
1646
|
}
|
|
1435
1647
|
async joinDocRoom(docId, params) {
|
|
1436
1648
|
await this.ready();
|
|
1437
1649
|
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
1438
1650
|
if (!this.transport.isConnected()) await this.transport.connect();
|
|
1439
1651
|
const existing = this.docSubscriptions.get(docId);
|
|
1440
|
-
if (existing)
|
|
1652
|
+
if (existing) {
|
|
1653
|
+
existing.refCount += 1;
|
|
1654
|
+
return this.createDocLease(docId, existing);
|
|
1655
|
+
}
|
|
1441
1656
|
const doc = await this.docManager.ensureDoc(docId);
|
|
1442
|
-
const
|
|
1443
|
-
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 {
|
|
1444
1669
|
unsubscribe: () => {
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
+
}
|
|
1447
1683
|
},
|
|
1448
|
-
firstSyncedWithRemote:
|
|
1684
|
+
firstSyncedWithRemote: record.base.firstSyncedWithRemote,
|
|
1449
1685
|
get connected() {
|
|
1450
|
-
return
|
|
1451
|
-
}
|
|
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
|
|
1452
1716
|
};
|
|
1453
|
-
this.docSubscriptions.set(docId, wrapped);
|
|
1454
|
-
subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
1455
|
-
return wrapped;
|
|
1456
1717
|
}
|
|
1457
1718
|
async destroy() {
|
|
1458
1719
|
await this.docManager.close();
|
|
1459
|
-
this.metaRoomSubscription
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
this.docSubscriptions.clear();
|
|
1463
|
-
if (this.unsubscribeMetaFlock) {
|
|
1464
|
-
this.unsubscribeMetaFlock();
|
|
1465
|
-
this.unsubscribeMetaFlock = void 0;
|
|
1720
|
+
if (this.metaRoomSubscription) {
|
|
1721
|
+
this.metaRoomSubscription.base.unsubscribe();
|
|
1722
|
+
this.metaRoomSubscription = void 0;
|
|
1466
1723
|
}
|
|
1724
|
+
for (const record of this.docSubscriptions.values()) record.base.unsubscribe();
|
|
1725
|
+
this.docSubscriptions.clear();
|
|
1726
|
+
this.unsubscribeMetaFlock?.();
|
|
1727
|
+
this.unsubscribeMetaFlock = void 0;
|
|
1467
1728
|
this.eventBus.clear();
|
|
1468
1729
|
this.metadataManager.clear();
|
|
1469
1730
|
this.assetManager.clear();
|
|
@@ -1483,7 +1744,7 @@ var SyncRunner = class {
|
|
|
1483
1744
|
if (batch.source === "local") return;
|
|
1484
1745
|
const by = this.eventBus.resolveEventBy("live");
|
|
1485
1746
|
(async () => {
|
|
1486
|
-
this.flockHydrator.applyEvents(batch.events, by);
|
|
1747
|
+
await this.flockHydrator.applyEvents(batch.events, by);
|
|
1487
1748
|
})().catch(logAsyncError("meta live monitor sync"));
|
|
1488
1749
|
});
|
|
1489
1750
|
}
|
|
@@ -1589,14 +1850,10 @@ var MetaPersister = class {
|
|
|
1589
1850
|
this.forceFullOnNextFlush = false;
|
|
1590
1851
|
return;
|
|
1591
1852
|
}
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
});
|
|
1597
|
-
} catch (error) {
|
|
1598
|
-
throw error;
|
|
1599
|
-
}
|
|
1853
|
+
await this.storage.save({
|
|
1854
|
+
type: "meta",
|
|
1855
|
+
update: encoded
|
|
1856
|
+
});
|
|
1600
1857
|
this.lastPersistedVersion = currentVersion;
|
|
1601
1858
|
this.forceFullOnNextFlush = false;
|
|
1602
1859
|
}
|
|
@@ -1654,6 +1911,7 @@ var MetaPersister = class {
|
|
|
1654
1911
|
//#endregion
|
|
1655
1912
|
//#region src/index.ts
|
|
1656
1913
|
const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
|
|
1914
|
+
const DEFAULT_DELETED_DOC_KEEP_MS = 720 * 60 * 60 * 1e3;
|
|
1657
1915
|
var LoroRepo = class LoroRepo {
|
|
1658
1916
|
options;
|
|
1659
1917
|
_destroyed = false;
|
|
@@ -1669,6 +1927,8 @@ var LoroRepo = class LoroRepo {
|
|
|
1669
1927
|
state;
|
|
1670
1928
|
syncRunner;
|
|
1671
1929
|
metaPersister;
|
|
1930
|
+
deletedDocKeepMs;
|
|
1931
|
+
purgeWatchHandle;
|
|
1672
1932
|
constructor(options) {
|
|
1673
1933
|
this.options = options;
|
|
1674
1934
|
this.transport = options.transportAdapter;
|
|
@@ -1678,6 +1938,8 @@ var LoroRepo = class LoroRepo {
|
|
|
1678
1938
|
this.state = createRepoState();
|
|
1679
1939
|
const configuredDebounce = options.docFrontierDebounceMs;
|
|
1680
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;
|
|
1681
1943
|
this.docManager = new DocManager({
|
|
1682
1944
|
storage: this.storage,
|
|
1683
1945
|
docFrontierDebounceMs,
|
|
@@ -1720,6 +1982,10 @@ var LoroRepo = class LoroRepo {
|
|
|
1720
1982
|
this.metaFlock.merge(snapshot);
|
|
1721
1983
|
}
|
|
1722
1984
|
});
|
|
1985
|
+
this.purgeWatchHandle = this.eventBus.watch((event) => this.handlePurgeSignals(event), {
|
|
1986
|
+
kinds: ["doc-soft-deleted", "doc-metadata"],
|
|
1987
|
+
by: ["sync", "live"]
|
|
1988
|
+
});
|
|
1723
1989
|
}
|
|
1724
1990
|
static async create(options) {
|
|
1725
1991
|
const repo = new LoroRepo(options);
|
|
@@ -1737,6 +2003,18 @@ var LoroRepo = class LoroRepo {
|
|
|
1737
2003
|
await this.syncRunner.ready();
|
|
1738
2004
|
this.metaPersister.start(this.metaFlock.version());
|
|
1739
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]);
|
|
2017
|
+
}
|
|
1740
2018
|
/**
|
|
1741
2019
|
* Sync selected data via the transport adaptor
|
|
1742
2020
|
* @param options
|
|
@@ -1747,6 +2025,10 @@ var LoroRepo = class LoroRepo {
|
|
|
1747
2025
|
/**
|
|
1748
2026
|
* Start syncing the metadata (Flock) room. It will establish a realtime connection to the transport adaptor.
|
|
1749
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`.
|
|
1750
2032
|
* @param params
|
|
1751
2033
|
* @returns
|
|
1752
2034
|
*/
|
|
@@ -1757,6 +2039,10 @@ var LoroRepo = class LoroRepo {
|
|
|
1757
2039
|
get connected() {
|
|
1758
2040
|
return subscription.connected;
|
|
1759
2041
|
},
|
|
2042
|
+
get status() {
|
|
2043
|
+
return subscription.status;
|
|
2044
|
+
},
|
|
2045
|
+
onStatusChange: subscription.onStatusChange,
|
|
1760
2046
|
firstSyncedWithRemote: subscription.firstSyncedWithRemote.then(async () => {
|
|
1761
2047
|
await this.metaPersister.flushNow();
|
|
1762
2048
|
})
|
|
@@ -1767,12 +2053,36 @@ var LoroRepo = class LoroRepo {
|
|
|
1767
2053
|
* All changes on the doc will be synced to the transport, and all changes on the transport will be synced to the doc.
|
|
1768
2054
|
*
|
|
1769
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.
|
|
1770
2060
|
* @param docId
|
|
1771
2061
|
* @param params
|
|
1772
2062
|
* @returns
|
|
1773
2063
|
*/
|
|
1774
2064
|
async joinDocRoom(docId, params) {
|
|
1775
|
-
|
|
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
|
+
};
|
|
1776
2086
|
}
|
|
1777
2087
|
/**
|
|
1778
2088
|
* Opens a document that is automatically persisted to the configured storage adapter.
|
|
@@ -1804,6 +2114,61 @@ var LoroRepo = class LoroRepo {
|
|
|
1804
2114
|
async listDoc(query) {
|
|
1805
2115
|
return this.metadataManager.listDoc(query);
|
|
1806
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
|
+
}
|
|
1807
2172
|
getMeta() {
|
|
1808
2173
|
return this.metaFlock;
|
|
1809
2174
|
}
|
|
@@ -1861,12 +2226,29 @@ var LoroRepo = class LoroRepo {
|
|
|
1861
2226
|
async destroy() {
|
|
1862
2227
|
if (this._destroyed) return;
|
|
1863
2228
|
this._destroyed = true;
|
|
2229
|
+
this.purgeWatchHandle.unsubscribe();
|
|
1864
2230
|
await this.metaPersister.destroy();
|
|
1865
2231
|
await this.syncRunner.destroy();
|
|
1866
2232
|
this.assetTransport?.close?.();
|
|
1867
2233
|
this.storage?.close?.();
|
|
1868
2234
|
await this.transport?.close();
|
|
1869
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
|
+
}
|
|
1870
2252
|
};
|
|
1871
2253
|
|
|
1872
2254
|
//#endregion
|