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