loro-repo 0.5.3 → 0.7.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 +42 -1
- package/dist/index.cjs +511 -97
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +56 -6
- package/dist/index.d.ts +56 -6
- package/dist/index.js +511 -97
- 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
|
|
@@ -1367,6 +1585,11 @@ var SyncRunner = class {
|
|
|
1367
1585
|
this.getMetaFlock = options.getMetaFlock;
|
|
1368
1586
|
this.replaceMetaFlock = options.mergeFlock;
|
|
1369
1587
|
}
|
|
1588
|
+
setTransport(transport) {
|
|
1589
|
+
if (this.transport === transport) return;
|
|
1590
|
+
this.leaveRooms();
|
|
1591
|
+
this.transport = transport;
|
|
1592
|
+
}
|
|
1370
1593
|
async ready() {
|
|
1371
1594
|
if (!this.readyPromise) this.readyPromise = this.initialize();
|
|
1372
1595
|
await this.readyPromise;
|
|
@@ -1385,7 +1608,7 @@ var SyncRunner = class {
|
|
|
1385
1608
|
});
|
|
1386
1609
|
try {
|
|
1387
1610
|
if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
|
|
1388
|
-
if (recordedEvents.length > 0) this.flockHydrator.applyEvents(recordedEvents, "sync");
|
|
1611
|
+
if (recordedEvents.length > 0) await this.flockHydrator.applyEvents(recordedEvents, "sync");
|
|
1389
1612
|
else this.flockHydrator.hydrateAll("sync");
|
|
1390
1613
|
} finally {
|
|
1391
1614
|
unsubscribe();
|
|
@@ -1411,62 +1634,105 @@ var SyncRunner = class {
|
|
|
1411
1634
|
await this.ready();
|
|
1412
1635
|
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
1413
1636
|
if (!this.transport.isConnected()) await this.transport.connect();
|
|
1414
|
-
|
|
1637
|
+
const existing = this.metaRoomSubscription;
|
|
1638
|
+
if (existing) {
|
|
1639
|
+
existing.refCount += 1;
|
|
1640
|
+
return this.createMetaLease(existing);
|
|
1641
|
+
}
|
|
1415
1642
|
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
|
-
}
|
|
1643
|
+
const base = this.transport.joinMetaRoom(this.metaFlock, params);
|
|
1644
|
+
const record = {
|
|
1645
|
+
base,
|
|
1646
|
+
refCount: 1
|
|
1430
1647
|
};
|
|
1431
|
-
this.metaRoomSubscription =
|
|
1432
|
-
|
|
1648
|
+
this.metaRoomSubscription = record;
|
|
1649
|
+
base.firstSyncedWithRemote.then(async () => {
|
|
1433
1650
|
const by = this.eventBus.resolveEventBy("live");
|
|
1434
1651
|
this.flockHydrator.hydrateAll(by);
|
|
1435
1652
|
}).catch(logAsyncError("meta room first sync"));
|
|
1436
|
-
return
|
|
1653
|
+
return this.createMetaLease(record);
|
|
1437
1654
|
}
|
|
1438
1655
|
async joinDocRoom(docId, params) {
|
|
1439
1656
|
await this.ready();
|
|
1440
1657
|
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
1441
1658
|
if (!this.transport.isConnected()) await this.transport.connect();
|
|
1442
1659
|
const existing = this.docSubscriptions.get(docId);
|
|
1443
|
-
if (existing)
|
|
1660
|
+
if (existing) {
|
|
1661
|
+
existing.refCount += 1;
|
|
1662
|
+
return this.createDocLease(docId, existing);
|
|
1663
|
+
}
|
|
1444
1664
|
const doc = await this.docManager.ensureDoc(docId);
|
|
1445
|
-
const
|
|
1446
|
-
const
|
|
1665
|
+
const base = this.transport.joinDocRoom(docId, doc, params);
|
|
1666
|
+
const record = {
|
|
1667
|
+
base,
|
|
1668
|
+
refCount: 1
|
|
1669
|
+
};
|
|
1670
|
+
this.docSubscriptions.set(docId, record);
|
|
1671
|
+
base.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
1672
|
+
return this.createDocLease(docId, record);
|
|
1673
|
+
}
|
|
1674
|
+
createMetaLease(record) {
|
|
1675
|
+
let released = false;
|
|
1676
|
+
return {
|
|
1447
1677
|
unsubscribe: () => {
|
|
1448
|
-
|
|
1449
|
-
|
|
1678
|
+
if (released) return;
|
|
1679
|
+
released = true;
|
|
1680
|
+
const current = this.metaRoomSubscription;
|
|
1681
|
+
if (!current || current !== record) return;
|
|
1682
|
+
current.refCount = Math.max(0, current.refCount - 1);
|
|
1683
|
+
if (current.refCount === 0) {
|
|
1684
|
+
current.base.unsubscribe();
|
|
1685
|
+
this.metaRoomSubscription = void 0;
|
|
1686
|
+
if (this.unsubscribeMetaFlock) {
|
|
1687
|
+
this.unsubscribeMetaFlock();
|
|
1688
|
+
this.unsubscribeMetaFlock = void 0;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1450
1691
|
},
|
|
1451
|
-
firstSyncedWithRemote:
|
|
1692
|
+
firstSyncedWithRemote: record.base.firstSyncedWithRemote,
|
|
1452
1693
|
get connected() {
|
|
1453
|
-
return
|
|
1454
|
-
}
|
|
1694
|
+
return record.base.connected;
|
|
1695
|
+
},
|
|
1696
|
+
get status() {
|
|
1697
|
+
return record.base.status;
|
|
1698
|
+
},
|
|
1699
|
+
onStatusChange: record.base.onStatusChange
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
createDocLease(docId, record) {
|
|
1703
|
+
let released = false;
|
|
1704
|
+
return {
|
|
1705
|
+
unsubscribe: () => {
|
|
1706
|
+
if (released) return;
|
|
1707
|
+
released = true;
|
|
1708
|
+
const current = this.docSubscriptions.get(docId);
|
|
1709
|
+
if (!current || current !== record) return;
|
|
1710
|
+
current.refCount = Math.max(0, current.refCount - 1);
|
|
1711
|
+
if (current.refCount === 0) {
|
|
1712
|
+
current.base.unsubscribe();
|
|
1713
|
+
if (this.docSubscriptions.get(docId) === current) this.docSubscriptions.delete(docId);
|
|
1714
|
+
}
|
|
1715
|
+
},
|
|
1716
|
+
firstSyncedWithRemote: record.base.firstSyncedWithRemote,
|
|
1717
|
+
get connected() {
|
|
1718
|
+
return record.base.connected;
|
|
1719
|
+
},
|
|
1720
|
+
get status() {
|
|
1721
|
+
return record.base.status;
|
|
1722
|
+
},
|
|
1723
|
+
onStatusChange: record.base.onStatusChange
|
|
1455
1724
|
};
|
|
1456
|
-
this.docSubscriptions.set(docId, wrapped);
|
|
1457
|
-
subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
1458
|
-
return wrapped;
|
|
1459
1725
|
}
|
|
1460
1726
|
async destroy() {
|
|
1461
1727
|
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;
|
|
1728
|
+
if (this.metaRoomSubscription) {
|
|
1729
|
+
this.metaRoomSubscription.base.unsubscribe();
|
|
1730
|
+
this.metaRoomSubscription = void 0;
|
|
1469
1731
|
}
|
|
1732
|
+
for (const record of this.docSubscriptions.values()) record.base.unsubscribe();
|
|
1733
|
+
this.docSubscriptions.clear();
|
|
1734
|
+
this.unsubscribeMetaFlock?.();
|
|
1735
|
+
this.unsubscribeMetaFlock = void 0;
|
|
1470
1736
|
this.eventBus.clear();
|
|
1471
1737
|
this.metadataManager.clear();
|
|
1472
1738
|
this.assetManager.clear();
|
|
@@ -1486,13 +1752,23 @@ var SyncRunner = class {
|
|
|
1486
1752
|
if (batch.source === "local") return;
|
|
1487
1753
|
const by = this.eventBus.resolveEventBy("live");
|
|
1488
1754
|
(async () => {
|
|
1489
|
-
this.flockHydrator.applyEvents(batch.events, by);
|
|
1755
|
+
await this.flockHydrator.applyEvents(batch.events, by);
|
|
1490
1756
|
})().catch(logAsyncError("meta live monitor sync"));
|
|
1491
1757
|
});
|
|
1492
1758
|
}
|
|
1493
1759
|
get metaFlock() {
|
|
1494
1760
|
return this.getMetaFlock();
|
|
1495
1761
|
}
|
|
1762
|
+
leaveRooms() {
|
|
1763
|
+
if (this.metaRoomSubscription) {
|
|
1764
|
+
this.metaRoomSubscription.base.unsubscribe();
|
|
1765
|
+
this.metaRoomSubscription = void 0;
|
|
1766
|
+
}
|
|
1767
|
+
for (const record of this.docSubscriptions.values()) record.base.unsubscribe();
|
|
1768
|
+
this.docSubscriptions.clear();
|
|
1769
|
+
this.unsubscribeMetaFlock?.();
|
|
1770
|
+
this.unsubscribeMetaFlock = void 0;
|
|
1771
|
+
}
|
|
1496
1772
|
};
|
|
1497
1773
|
|
|
1498
1774
|
//#endregion
|
|
@@ -1592,14 +1868,10 @@ var MetaPersister = class {
|
|
|
1592
1868
|
this.forceFullOnNextFlush = false;
|
|
1593
1869
|
return;
|
|
1594
1870
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
});
|
|
1600
|
-
} catch (error) {
|
|
1601
|
-
throw error;
|
|
1602
|
-
}
|
|
1871
|
+
await this.storage.save({
|
|
1872
|
+
type: "meta",
|
|
1873
|
+
update: encoded
|
|
1874
|
+
});
|
|
1603
1875
|
this.lastPersistedVersion = currentVersion;
|
|
1604
1876
|
this.forceFullOnNextFlush = false;
|
|
1605
1877
|
}
|
|
@@ -1657,8 +1929,8 @@ var MetaPersister = class {
|
|
|
1657
1929
|
//#endregion
|
|
1658
1930
|
//#region src/index.ts
|
|
1659
1931
|
const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
|
|
1932
|
+
const DEFAULT_DELETED_DOC_KEEP_MS = 720 * 60 * 60 * 1e3;
|
|
1660
1933
|
var LoroRepo = class LoroRepo {
|
|
1661
|
-
options;
|
|
1662
1934
|
_destroyed = false;
|
|
1663
1935
|
transport;
|
|
1664
1936
|
storage;
|
|
@@ -1672,8 +1944,9 @@ var LoroRepo = class LoroRepo {
|
|
|
1672
1944
|
state;
|
|
1673
1945
|
syncRunner;
|
|
1674
1946
|
metaPersister;
|
|
1947
|
+
deletedDocKeepMs;
|
|
1948
|
+
purgeWatchHandle;
|
|
1675
1949
|
constructor(options) {
|
|
1676
|
-
this.options = options;
|
|
1677
1950
|
this.transport = options.transportAdapter;
|
|
1678
1951
|
this.storage = options.storageAdapter;
|
|
1679
1952
|
this.assetTransport = options.assetTransportAdapter;
|
|
@@ -1681,6 +1954,8 @@ var LoroRepo = class LoroRepo {
|
|
|
1681
1954
|
this.state = createRepoState();
|
|
1682
1955
|
const configuredDebounce = options.docFrontierDebounceMs;
|
|
1683
1956
|
const docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
|
|
1957
|
+
const configuredDeletedKeepMs = options.deletedDocKeepMs;
|
|
1958
|
+
this.deletedDocKeepMs = typeof configuredDeletedKeepMs === "number" && Number.isFinite(configuredDeletedKeepMs) && configuredDeletedKeepMs >= 0 ? configuredDeletedKeepMs : DEFAULT_DELETED_DOC_KEEP_MS;
|
|
1684
1959
|
this.docManager = new DocManager({
|
|
1685
1960
|
storage: this.storage,
|
|
1686
1961
|
docFrontierDebounceMs,
|
|
@@ -1723,6 +1998,10 @@ var LoroRepo = class LoroRepo {
|
|
|
1723
1998
|
this.metaFlock.merge(snapshot);
|
|
1724
1999
|
}
|
|
1725
2000
|
});
|
|
2001
|
+
this.purgeWatchHandle = this.eventBus.watch((event) => this.handlePurgeSignals(event), {
|
|
2002
|
+
kinds: ["doc-soft-deleted", "doc-metadata"],
|
|
2003
|
+
by: ["sync", "live"]
|
|
2004
|
+
});
|
|
1726
2005
|
}
|
|
1727
2006
|
static async create(options) {
|
|
1728
2007
|
const repo = new LoroRepo(options);
|
|
@@ -1740,6 +2019,18 @@ var LoroRepo = class LoroRepo {
|
|
|
1740
2019
|
await this.syncRunner.ready();
|
|
1741
2020
|
this.metaPersister.start(this.metaFlock.version());
|
|
1742
2021
|
}
|
|
2022
|
+
computeDocPurgeAfter(docId, minKeepMs) {
|
|
2023
|
+
const deletedAt = this.metadataManager.getDeletedAtMs(docId);
|
|
2024
|
+
if (deletedAt === void 0) return void 0;
|
|
2025
|
+
return deletedAt + minKeepMs;
|
|
2026
|
+
}
|
|
2027
|
+
purgeDocKeyspace(docId) {
|
|
2028
|
+
const metadataKeys = Array.from(this.metaFlock.scan({ prefix: ["m", docId] }), (row) => row.key);
|
|
2029
|
+
for (const key of metadataKeys) this.metaFlock.delete(key);
|
|
2030
|
+
const frontierKeys = Array.from(this.metaFlock.scan({ prefix: ["f", docId] }), (row) => row.key);
|
|
2031
|
+
for (const key of frontierKeys) this.metaFlock.delete(key);
|
|
2032
|
+
this.metaFlock.delete(["ts", docId]);
|
|
2033
|
+
}
|
|
1743
2034
|
/**
|
|
1744
2035
|
* Sync selected data via the transport adaptor
|
|
1745
2036
|
* @param options
|
|
@@ -1748,8 +2039,31 @@ var LoroRepo = class LoroRepo {
|
|
|
1748
2039
|
await this.syncRunner.sync(options);
|
|
1749
2040
|
}
|
|
1750
2041
|
/**
|
|
2042
|
+
* Sets (or replaces) the transport adapter used for syncing and realtime rooms.
|
|
2043
|
+
*
|
|
2044
|
+
* Swapping transports will leave any joined meta/doc rooms managed by the repo.
|
|
2045
|
+
*/
|
|
2046
|
+
async setTransportAdapter(transport) {
|
|
2047
|
+
if (this._destroyed) throw new Error("Repo has been destroyed");
|
|
2048
|
+
if (this.transport === transport) return;
|
|
2049
|
+
const previous = this.transport;
|
|
2050
|
+
this.transport = transport;
|
|
2051
|
+
this.syncRunner.setTransport(transport);
|
|
2052
|
+
await previous?.close();
|
|
2053
|
+
}
|
|
2054
|
+
hasTransport() {
|
|
2055
|
+
return Boolean(this.transport);
|
|
2056
|
+
}
|
|
2057
|
+
hasStorage() {
|
|
2058
|
+
return Boolean(this.storage);
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
1751
2061
|
* Start syncing the metadata (Flock) room. It will establish a realtime connection to the transport adaptor.
|
|
1752
2062
|
* All changes on the room will be synced to the Flock, and all changes on the Flock will be synced to the room.
|
|
2063
|
+
*
|
|
2064
|
+
* - Idempotent: repeated calls reuse the same underlying room session; no extra join request is sent for the same repo.
|
|
2065
|
+
* - Reference-counted leave: every call to `joinMetaRoom` returns a subscription that increments an internal counter. The room is
|
|
2066
|
+
* actually left only after all returned subscriptions have called `unsubscribe`.
|
|
1753
2067
|
* @param params
|
|
1754
2068
|
* @returns
|
|
1755
2069
|
*/
|
|
@@ -1760,6 +2074,10 @@ var LoroRepo = class LoroRepo {
|
|
|
1760
2074
|
get connected() {
|
|
1761
2075
|
return subscription.connected;
|
|
1762
2076
|
},
|
|
2077
|
+
get status() {
|
|
2078
|
+
return subscription.status;
|
|
2079
|
+
},
|
|
2080
|
+
onStatusChange: subscription.onStatusChange,
|
|
1763
2081
|
firstSyncedWithRemote: subscription.firstSyncedWithRemote.then(async () => {
|
|
1764
2082
|
await this.metaPersister.flushNow();
|
|
1765
2083
|
})
|
|
@@ -1770,12 +2088,36 @@ var LoroRepo = class LoroRepo {
|
|
|
1770
2088
|
* All changes on the doc will be synced to the transport, and all changes on the transport will be synced to the doc.
|
|
1771
2089
|
*
|
|
1772
2090
|
* All the changes on the room will be reflected on the same doc you get from `repo.openCollaborativeDoc(docId)`
|
|
2091
|
+
*
|
|
2092
|
+
* - Idempotent: multiple joins for the same `docId` reuse the existing session; no duplicate transport joins are issued.
|
|
2093
|
+
* - Reference-counted leave: each returned subscription bumps an internal counter and only the final `unsubscribe()` will
|
|
2094
|
+
* actually leave the room. Earlier unsubscribes simply decrement the counter.
|
|
1773
2095
|
* @param docId
|
|
1774
2096
|
* @param params
|
|
1775
2097
|
* @returns
|
|
1776
2098
|
*/
|
|
1777
2099
|
async joinDocRoom(docId, params) {
|
|
1778
|
-
|
|
2100
|
+
const subscription = await this.syncRunner.joinDocRoom(docId, params);
|
|
2101
|
+
return {
|
|
2102
|
+
...subscription,
|
|
2103
|
+
onStatusChange: subscription.onStatusChange,
|
|
2104
|
+
status: subscription.status
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
/**
|
|
2108
|
+
* Joins an ephemeral CRDT room. This is useful for presence-like state that should not be persisted.
|
|
2109
|
+
* The returned store can be used immediately; the first sync promise resolves once the initial handshake completes.
|
|
2110
|
+
*/
|
|
2111
|
+
async joinEphemeralRoom(roomId) {
|
|
2112
|
+
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
2113
|
+
await this.syncRunner.ready();
|
|
2114
|
+
if (!this.transport.isConnected()) await this.transport.connect();
|
|
2115
|
+
const subscription = this.transport.joinEphemeralRoom(roomId);
|
|
2116
|
+
return {
|
|
2117
|
+
...subscription,
|
|
2118
|
+
onStatusChange: subscription.onStatusChange,
|
|
2119
|
+
status: subscription.status
|
|
2120
|
+
};
|
|
1779
2121
|
}
|
|
1780
2122
|
/**
|
|
1781
2123
|
* Opens a document that is automatically persisted to the configured storage adapter.
|
|
@@ -1807,6 +2149,61 @@ var LoroRepo = class LoroRepo {
|
|
|
1807
2149
|
async listDoc(query) {
|
|
1808
2150
|
return this.metadataManager.listDoc(query);
|
|
1809
2151
|
}
|
|
2152
|
+
/**
|
|
2153
|
+
* Mark a document deleted by writing a `ts/*` tombstone entry (timestamp).
|
|
2154
|
+
* The body and metadata remain until purged; callers use the tombstone to
|
|
2155
|
+
* render deleted state or trigger retention workflows. For immediate removal,
|
|
2156
|
+
* call `purgeDoc` instead.
|
|
2157
|
+
*/
|
|
2158
|
+
async deleteDoc(docId, options = {}) {
|
|
2159
|
+
if (this.metadataManager.getDeletedAtMs(docId) !== void 0 && !options.force) return;
|
|
2160
|
+
const deletedAt = options.deletedAt ?? Date.now();
|
|
2161
|
+
this.metadataManager.markDeleted(docId, deletedAt);
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Undo a soft delete by removing the tombstone entry. Metadata and document
|
|
2165
|
+
* state remain untouched.
|
|
2166
|
+
*/
|
|
2167
|
+
async restoreDoc(docId) {
|
|
2168
|
+
if (this.metadataManager.getDeletedAtMs(docId) === void 0) return;
|
|
2169
|
+
this.metadataManager.clearDeleted(docId);
|
|
2170
|
+
}
|
|
2171
|
+
/**
|
|
2172
|
+
* Hard-delete a document immediately. Removes doc snapshots/updates via the
|
|
2173
|
+
* storage adapter (if supported), clears metadata/frontiers/link keys from
|
|
2174
|
+
* Flock, and unlinks assets (they become orphaned for asset GC).
|
|
2175
|
+
*/
|
|
2176
|
+
async purgeDoc(docId) {
|
|
2177
|
+
const deletedAtMs = this.metadataManager.getDeletedAtMs(docId);
|
|
2178
|
+
this.eventBus.emit({
|
|
2179
|
+
kind: "doc-purging",
|
|
2180
|
+
docId,
|
|
2181
|
+
deletedAtMs,
|
|
2182
|
+
by: "local"
|
|
2183
|
+
});
|
|
2184
|
+
await this.docManager.dropDoc(docId);
|
|
2185
|
+
this.assetManager.purgeDocLinks(docId, "local");
|
|
2186
|
+
this.purgeDocKeyspace(docId);
|
|
2187
|
+
this.metadataManager.emitSoftDeleted(docId, void 0, "local");
|
|
2188
|
+
this.metadataManager.refreshFromFlock(docId, "local");
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Sweep tombstoned documents whose retention window expired. Uses
|
|
2192
|
+
* `deletedDocKeepMs` by default; pass `minKeepMs`/`now` for overrides.
|
|
2193
|
+
*/
|
|
2194
|
+
async gcDeletedDocs(options = {}) {
|
|
2195
|
+
const now = options.now ?? Date.now();
|
|
2196
|
+
const minKeepMs = options.minKeepMs ?? this.deletedDocKeepMs;
|
|
2197
|
+
const docIds = this.metadataManager.getDocIds();
|
|
2198
|
+
let purged = 0;
|
|
2199
|
+
for (const docId of docIds) {
|
|
2200
|
+
const purgeAfter = this.computeDocPurgeAfter(docId, minKeepMs);
|
|
2201
|
+
if (purgeAfter === void 0 || now < purgeAfter) continue;
|
|
2202
|
+
await this.purgeDoc(docId);
|
|
2203
|
+
purged += 1;
|
|
2204
|
+
}
|
|
2205
|
+
return purged;
|
|
2206
|
+
}
|
|
1810
2207
|
getMeta() {
|
|
1811
2208
|
return this.metaFlock;
|
|
1812
2209
|
}
|
|
@@ -1864,12 +2261,29 @@ var LoroRepo = class LoroRepo {
|
|
|
1864
2261
|
async destroy() {
|
|
1865
2262
|
if (this._destroyed) return;
|
|
1866
2263
|
this._destroyed = true;
|
|
2264
|
+
this.purgeWatchHandle.unsubscribe();
|
|
1867
2265
|
await this.metaPersister.destroy();
|
|
1868
2266
|
await this.syncRunner.destroy();
|
|
1869
2267
|
this.assetTransport?.close?.();
|
|
1870
2268
|
this.storage?.close?.();
|
|
1871
2269
|
await this.transport?.close();
|
|
1872
2270
|
}
|
|
2271
|
+
handlePurgeSignals(event) {
|
|
2272
|
+
const docId = (() => {
|
|
2273
|
+
if (event.kind === "doc-soft-deleted") return event.docId;
|
|
2274
|
+
if (event.kind === "doc-metadata") return event.docId;
|
|
2275
|
+
})();
|
|
2276
|
+
if (!docId) return;
|
|
2277
|
+
const metadataCleared = event.kind === "doc-metadata" && Object.keys(event.patch).length === 0;
|
|
2278
|
+
const tombstoneCleared = event.kind === "doc-soft-deleted" && event.deletedAtMs === void 0;
|
|
2279
|
+
if (!(metadataCleared || tombstoneCleared && this.metadataManager.get(docId) === void 0)) return;
|
|
2280
|
+
this.docManager.dropDoc(docId).catch((error) => {
|
|
2281
|
+
console.error("Failed to drop purged doc", {
|
|
2282
|
+
docId,
|
|
2283
|
+
error
|
|
2284
|
+
});
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
1873
2287
|
};
|
|
1874
2288
|
|
|
1875
2289
|
//#endregion
|