loro-repo 0.0.0 → 0.2.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 +3 -0
- package/dist/index.cjs +1293 -807
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -63
- package/dist/index.d.ts +60 -64
- package/dist/index.js +1272 -811
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { Flock } from "@loro-dev/flock";
|
|
2
2
|
import { LoroDoc } from "loro-crdt";
|
|
3
|
-
import { FlockAdaptor,
|
|
3
|
+
import { FlockAdaptor, LoroAdaptor } from "loro-adaptors";
|
|
4
4
|
import { CrdtType, bytesToHex } from "loro-protocol";
|
|
5
5
|
import { LoroWebsocketClient } from "loro-websocket";
|
|
6
|
+
import { promises } from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
6
9
|
|
|
7
10
|
//#region src/loro-adaptor.ts
|
|
8
11
|
function createRepoFlockAdaptorFromDoc(flock, config = {}) {
|
|
@@ -195,7 +198,7 @@ var WebSocketTransportAdapter = class {
|
|
|
195
198
|
const auth = params.auth ?? this.options.docAuth?.(docId);
|
|
196
199
|
if (existing && existing.doc === doc && existing.roomId === roomId) return existing;
|
|
197
200
|
if (existing) await this.leaveDocSession(docId).catch(() => {});
|
|
198
|
-
const adaptor =
|
|
201
|
+
const adaptor = new LoroAdaptor(doc);
|
|
199
202
|
const room = await client.join({
|
|
200
203
|
roomId,
|
|
201
204
|
crdtAdaptor: adaptor,
|
|
@@ -483,7 +486,7 @@ const DEFAULT_META_STORE = "meta";
|
|
|
483
486
|
const DEFAULT_ASSET_STORE = "assets";
|
|
484
487
|
const DEFAULT_DOC_UPDATE_STORE = "doc-updates";
|
|
485
488
|
const DEFAULT_META_KEY = "snapshot";
|
|
486
|
-
const textDecoder = new TextDecoder();
|
|
489
|
+
const textDecoder$1 = new TextDecoder();
|
|
487
490
|
function describeUnknown(cause) {
|
|
488
491
|
if (typeof cause === "string") return cause;
|
|
489
492
|
if (typeof cause === "number" || typeof cause === "boolean") return String(cause);
|
|
@@ -584,7 +587,7 @@ var IndexedDBStorageAdaptor = class {
|
|
|
584
587
|
const bytes = await this.getBinary(this.metaStore, this.metaKey);
|
|
585
588
|
if (!bytes) return void 0;
|
|
586
589
|
try {
|
|
587
|
-
const json = textDecoder.decode(bytes);
|
|
590
|
+
const json = textDecoder$1.decode(bytes);
|
|
588
591
|
const bundle = JSON.parse(json);
|
|
589
592
|
const flock = new Flock();
|
|
590
593
|
flock.importJson(bundle);
|
|
@@ -728,15 +731,224 @@ var IndexedDBStorageAdaptor = class {
|
|
|
728
731
|
};
|
|
729
732
|
|
|
730
733
|
//#endregion
|
|
731
|
-
//#region src/
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
734
|
+
//#region src/storage/filesystem.ts
|
|
735
|
+
const textDecoder = new TextDecoder();
|
|
736
|
+
var FileSystemStorageAdaptor = class {
|
|
737
|
+
baseDir;
|
|
738
|
+
docsDir;
|
|
739
|
+
assetsDir;
|
|
740
|
+
metaPath;
|
|
741
|
+
initPromise;
|
|
742
|
+
updateCounter = 0;
|
|
743
|
+
constructor(options = {}) {
|
|
744
|
+
this.baseDir = path.resolve(options.baseDir ?? path.join(process.cwd(), ".loro-repo"));
|
|
745
|
+
this.docsDir = path.join(this.baseDir, options.docsDirName ?? "docs");
|
|
746
|
+
this.assetsDir = path.join(this.baseDir, options.assetsDirName ?? "assets");
|
|
747
|
+
this.metaPath = path.join(this.baseDir, options.metaFileName ?? "meta.json");
|
|
748
|
+
this.initPromise = this.ensureLayout();
|
|
749
|
+
}
|
|
750
|
+
async save(payload) {
|
|
751
|
+
await this.initPromise;
|
|
752
|
+
switch (payload.type) {
|
|
753
|
+
case "doc-snapshot":
|
|
754
|
+
await this.writeDocSnapshot(payload.docId, payload.snapshot);
|
|
755
|
+
return;
|
|
756
|
+
case "doc-update":
|
|
757
|
+
await this.enqueueDocUpdate(payload.docId, payload.update);
|
|
758
|
+
return;
|
|
759
|
+
case "asset":
|
|
760
|
+
await this.writeAsset(payload.assetId, payload.data);
|
|
761
|
+
return;
|
|
762
|
+
case "meta":
|
|
763
|
+
await writeFileAtomic(this.metaPath, payload.update);
|
|
764
|
+
return;
|
|
765
|
+
default: throw new Error(`Unsupported payload type: ${payload.type}`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async deleteAsset(assetId) {
|
|
769
|
+
await this.initPromise;
|
|
770
|
+
await removeIfExists(this.assetPath(assetId));
|
|
771
|
+
}
|
|
772
|
+
async loadDoc(docId) {
|
|
773
|
+
await this.initPromise;
|
|
774
|
+
const snapshotBytes = await readFileIfExists(this.docSnapshotPath(docId));
|
|
775
|
+
const updateDir = this.docUpdatesDir(docId);
|
|
776
|
+
const updateFiles = await listFiles(updateDir);
|
|
777
|
+
if (!snapshotBytes && updateFiles.length === 0) return;
|
|
778
|
+
const doc = snapshotBytes ? LoroDoc.fromSnapshot(snapshotBytes) : new LoroDoc();
|
|
779
|
+
if (updateFiles.length === 0) return doc;
|
|
780
|
+
const updatePaths = updateFiles.map((file) => path.join(updateDir, file));
|
|
781
|
+
for (const updatePath of updatePaths) {
|
|
782
|
+
const update = await readFileIfExists(updatePath);
|
|
783
|
+
if (!update) continue;
|
|
784
|
+
doc.import(update);
|
|
785
|
+
}
|
|
786
|
+
await Promise.all(updatePaths.map((filePath) => removeIfExists(filePath)));
|
|
787
|
+
const consolidated = doc.export({ mode: "snapshot" });
|
|
788
|
+
await this.writeDocSnapshot(docId, consolidated);
|
|
789
|
+
return doc;
|
|
790
|
+
}
|
|
791
|
+
async loadMeta() {
|
|
792
|
+
await this.initPromise;
|
|
793
|
+
const bytes = await readFileIfExists(this.metaPath);
|
|
794
|
+
if (!bytes) return void 0;
|
|
795
|
+
try {
|
|
796
|
+
const bundle = JSON.parse(textDecoder.decode(bytes));
|
|
797
|
+
const flock = new Flock();
|
|
798
|
+
flock.importJson(bundle);
|
|
799
|
+
return flock;
|
|
800
|
+
} catch (error) {
|
|
801
|
+
throw new Error("Failed to hydrate metadata snapshot", { cause: error });
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
async loadAsset(assetId) {
|
|
805
|
+
await this.initPromise;
|
|
806
|
+
return readFileIfExists(this.assetPath(assetId));
|
|
807
|
+
}
|
|
808
|
+
async ensureLayout() {
|
|
809
|
+
await Promise.all([
|
|
810
|
+
ensureDir(this.baseDir),
|
|
811
|
+
ensureDir(this.docsDir),
|
|
812
|
+
ensureDir(this.assetsDir)
|
|
813
|
+
]);
|
|
814
|
+
}
|
|
815
|
+
async writeDocSnapshot(docId, snapshot) {
|
|
816
|
+
await ensureDir(this.docDir(docId));
|
|
817
|
+
await writeFileAtomic(this.docSnapshotPath(docId), snapshot);
|
|
818
|
+
}
|
|
819
|
+
async enqueueDocUpdate(docId, update) {
|
|
820
|
+
const dir = this.docUpdatesDir(docId);
|
|
821
|
+
await ensureDir(dir);
|
|
822
|
+
const counter = this.updateCounter = (this.updateCounter + 1) % 1e6;
|
|
823
|
+
const fileName = `${Date.now().toString().padStart(13, "0")}-${counter.toString().padStart(6, "0")}.bin`;
|
|
824
|
+
await writeFileAtomic(path.join(dir, fileName), update);
|
|
825
|
+
}
|
|
826
|
+
async writeAsset(assetId, data) {
|
|
827
|
+
const filePath = this.assetPath(assetId);
|
|
828
|
+
await ensureDir(path.dirname(filePath));
|
|
829
|
+
await writeFileAtomic(filePath, data);
|
|
830
|
+
}
|
|
831
|
+
docDir(docId) {
|
|
832
|
+
return path.join(this.docsDir, encodeComponent(docId));
|
|
833
|
+
}
|
|
834
|
+
docSnapshotPath(docId) {
|
|
835
|
+
return path.join(this.docDir(docId), "snapshot.bin");
|
|
836
|
+
}
|
|
837
|
+
docUpdatesDir(docId) {
|
|
838
|
+
return path.join(this.docDir(docId), "updates");
|
|
839
|
+
}
|
|
840
|
+
assetPath(assetId) {
|
|
841
|
+
return path.join(this.assetsDir, encodeComponent(assetId));
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
function encodeComponent(value) {
|
|
845
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
846
|
+
}
|
|
847
|
+
async function ensureDir(dir) {
|
|
848
|
+
await promises.mkdir(dir, { recursive: true });
|
|
739
849
|
}
|
|
850
|
+
async function readFileIfExists(filePath) {
|
|
851
|
+
try {
|
|
852
|
+
const data = await promises.readFile(filePath);
|
|
853
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
|
|
854
|
+
} catch (error) {
|
|
855
|
+
if (error.code === "ENOENT") return;
|
|
856
|
+
throw error;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async function removeIfExists(filePath) {
|
|
860
|
+
try {
|
|
861
|
+
await promises.rm(filePath);
|
|
862
|
+
} catch (error) {
|
|
863
|
+
if (error.code === "ENOENT") return;
|
|
864
|
+
throw error;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
async function listFiles(dir) {
|
|
868
|
+
try {
|
|
869
|
+
return (await promises.readdir(dir)).sort();
|
|
870
|
+
} catch (error) {
|
|
871
|
+
if (error.code === "ENOENT") return [];
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function writeFileAtomic(targetPath, data) {
|
|
876
|
+
const dir = path.dirname(targetPath);
|
|
877
|
+
await ensureDir(dir);
|
|
878
|
+
const tempPath = path.join(dir, `.tmp-${randomUUID()}`);
|
|
879
|
+
await promises.writeFile(tempPath, data);
|
|
880
|
+
await promises.rename(tempPath, targetPath);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
//#endregion
|
|
884
|
+
//#region src/internal/event-bus.ts
|
|
885
|
+
var RepoEventBus = class {
|
|
886
|
+
watchers = /* @__PURE__ */ new Set();
|
|
887
|
+
eventByStack = [];
|
|
888
|
+
watch(listener, filter = {}) {
|
|
889
|
+
const entry = {
|
|
890
|
+
listener,
|
|
891
|
+
filter
|
|
892
|
+
};
|
|
893
|
+
this.watchers.add(entry);
|
|
894
|
+
return { unsubscribe: () => {
|
|
895
|
+
this.watchers.delete(entry);
|
|
896
|
+
} };
|
|
897
|
+
}
|
|
898
|
+
emit(event) {
|
|
899
|
+
for (const entry of this.watchers) if (this.shouldNotify(entry.filter, event)) entry.listener(event);
|
|
900
|
+
}
|
|
901
|
+
clear() {
|
|
902
|
+
this.watchers.clear();
|
|
903
|
+
this.eventByStack.length = 0;
|
|
904
|
+
}
|
|
905
|
+
pushEventBy(by) {
|
|
906
|
+
this.eventByStack.push(by);
|
|
907
|
+
}
|
|
908
|
+
popEventBy() {
|
|
909
|
+
this.eventByStack.pop();
|
|
910
|
+
}
|
|
911
|
+
resolveEventBy(defaultBy) {
|
|
912
|
+
const index = this.eventByStack.length - 1;
|
|
913
|
+
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
914
|
+
}
|
|
915
|
+
shouldNotify(filter, event) {
|
|
916
|
+
if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
|
|
917
|
+
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
918
|
+
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
919
|
+
const docId = (() => {
|
|
920
|
+
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
921
|
+
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
922
|
+
})();
|
|
923
|
+
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
924
|
+
if (filter.docIds && !docId) return false;
|
|
925
|
+
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
926
|
+
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
927
|
+
}
|
|
928
|
+
return true;
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
//#endregion
|
|
933
|
+
//#region src/internal/doc-handle.ts
|
|
934
|
+
var RepoDocHandleImpl = class {
|
|
935
|
+
doc;
|
|
936
|
+
whenSyncedWithRemote;
|
|
937
|
+
docId;
|
|
938
|
+
onClose;
|
|
939
|
+
constructor(docId, doc, whenSyncedWithRemote, onClose) {
|
|
940
|
+
this.docId = docId;
|
|
941
|
+
this.doc = doc;
|
|
942
|
+
this.whenSyncedWithRemote = whenSyncedWithRemote;
|
|
943
|
+
this.onClose = onClose;
|
|
944
|
+
}
|
|
945
|
+
async close() {
|
|
946
|
+
await this.onClose(this.docId, this.doc);
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
//#endregion
|
|
951
|
+
//#region src/utils.ts
|
|
740
952
|
async function streamToUint8Array(stream) {
|
|
741
953
|
const reader = stream.getReader();
|
|
742
954
|
const chunks = [];
|
|
@@ -814,12 +1026,14 @@ function asJsonObject(value) {
|
|
|
814
1026
|
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
815
1027
|
}
|
|
816
1028
|
function isJsonObjectValue(value) {
|
|
817
|
-
return typeof value === "object" &&
|
|
1029
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
818
1030
|
}
|
|
819
1031
|
function stableStringify(value) {
|
|
820
|
-
if (value === null
|
|
1032
|
+
if (value === null) return "null";
|
|
1033
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1034
|
+
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
821
1035
|
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
822
|
-
if (!isJsonObjectValue(value)) return
|
|
1036
|
+
if (!isJsonObjectValue(value)) return "null";
|
|
823
1037
|
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
824
1038
|
}
|
|
825
1039
|
function jsonEquals(a, b) {
|
|
@@ -829,15 +1043,17 @@ function jsonEquals(a, b) {
|
|
|
829
1043
|
}
|
|
830
1044
|
function diffJsonObjects(previous, next) {
|
|
831
1045
|
const patch = {};
|
|
832
|
-
const keys =
|
|
1046
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1047
|
+
if (previous) for (const key of Object.keys(previous)) keys.add(key);
|
|
1048
|
+
for (const key of Object.keys(next)) keys.add(key);
|
|
833
1049
|
for (const key of keys) {
|
|
834
1050
|
const prevValue = previous ? previous[key] : void 0;
|
|
835
|
-
if (!Object.prototype.hasOwnProperty.call(next, key)) {
|
|
836
|
-
patch[key] = null;
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
1051
|
const nextValue = next[key];
|
|
840
1052
|
if (!jsonEquals(prevValue, nextValue)) {
|
|
1053
|
+
if (nextValue === void 0 && previous && key in previous) {
|
|
1054
|
+
patch[key] = null;
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
841
1057
|
const cloned = cloneJsonValue(nextValue);
|
|
842
1058
|
if (cloned !== void 0) patch[key] = cloned;
|
|
843
1059
|
}
|
|
@@ -893,15 +1109,6 @@ function toReadableStream(bytes) {
|
|
|
893
1109
|
controller.close();
|
|
894
1110
|
} });
|
|
895
1111
|
}
|
|
896
|
-
function computeVersionVector(doc) {
|
|
897
|
-
const candidate = doc;
|
|
898
|
-
if (typeof candidate.frontiers === "function" && typeof candidate.frontiersToVV === "function") {
|
|
899
|
-
const frontiers = candidate.frontiers();
|
|
900
|
-
return candidate.frontiersToVV(frontiers);
|
|
901
|
-
}
|
|
902
|
-
if (typeof candidate.version === "function") return candidate.version();
|
|
903
|
-
return {};
|
|
904
|
-
}
|
|
905
1112
|
function emptyFrontiers() {
|
|
906
1113
|
return [];
|
|
907
1114
|
}
|
|
@@ -933,21 +1140,6 @@ function canonicalizeVersionVector(vv) {
|
|
|
933
1140
|
key: stableStringify(json)
|
|
934
1141
|
};
|
|
935
1142
|
}
|
|
936
|
-
var RepoDocHandleImpl = class {
|
|
937
|
-
doc;
|
|
938
|
-
whenSyncedWithRemote;
|
|
939
|
-
docId;
|
|
940
|
-
onClose;
|
|
941
|
-
constructor(docId, doc, whenSyncedWithRemote, onClose) {
|
|
942
|
-
this.docId = docId;
|
|
943
|
-
this.doc = doc;
|
|
944
|
-
this.whenSyncedWithRemote = whenSyncedWithRemote;
|
|
945
|
-
this.onClose = onClose;
|
|
946
|
-
}
|
|
947
|
-
async close() {
|
|
948
|
-
await this.onClose(this.docId, this.doc);
|
|
949
|
-
}
|
|
950
|
-
};
|
|
951
1143
|
function matchesQuery(docId, _metadata, query) {
|
|
952
1144
|
if (!query) return true;
|
|
953
1145
|
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
@@ -955,185 +1147,164 @@ function matchesQuery(docId, _metadata, query) {
|
|
|
955
1147
|
if (query.end && docId > query.end) return false;
|
|
956
1148
|
return true;
|
|
957
1149
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1150
|
+
|
|
1151
|
+
//#endregion
|
|
1152
|
+
//#region src/internal/logging.ts
|
|
1153
|
+
function logAsyncError(context) {
|
|
1154
|
+
return (error) => {
|
|
1155
|
+
if (error instanceof Error) console.error(`[loro-repo] ${context} failed: ${error.message}`, error);
|
|
1156
|
+
else console.error(`[loro-repo] ${context} failed with non-error reason:`, error);
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
//#endregion
|
|
1161
|
+
//#region src/internal/doc-manager.ts
|
|
1162
|
+
var DocManager = class {
|
|
961
1163
|
storage;
|
|
962
|
-
assetTransport;
|
|
963
1164
|
docFactory;
|
|
964
|
-
|
|
965
|
-
|
|
1165
|
+
docFrontierDebounceMs;
|
|
1166
|
+
getMetaFlock;
|
|
1167
|
+
eventBus;
|
|
1168
|
+
persistMeta;
|
|
1169
|
+
state;
|
|
966
1170
|
docs = /* @__PURE__ */ new Map();
|
|
967
1171
|
docRefs = /* @__PURE__ */ new Map();
|
|
968
1172
|
docSubscriptions = /* @__PURE__ */ new Map();
|
|
969
|
-
docAssets = /* @__PURE__ */ new Map();
|
|
970
|
-
assets = /* @__PURE__ */ new Map();
|
|
971
|
-
orphanedAssets = /* @__PURE__ */ new Map();
|
|
972
|
-
assetToDocRefs = /* @__PURE__ */ new Map();
|
|
973
|
-
docFrontierKeys = /* @__PURE__ */ new Map();
|
|
974
1173
|
docFrontierUpdates = /* @__PURE__ */ new Map();
|
|
975
1174
|
docPersistedVersions = /* @__PURE__ */ new Map();
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
eventByStack = [];
|
|
979
|
-
metaRoomSubscription;
|
|
980
|
-
unsubscribeMetaFlock;
|
|
981
|
-
readyPromise;
|
|
982
|
-
constructor(options) {
|
|
983
|
-
this.options = options;
|
|
984
|
-
this.transport = options.transportAdapter;
|
|
985
|
-
this.storage = options.storageAdapter;
|
|
986
|
-
this.assetTransport = options.assetTransportAdapter;
|
|
987
|
-
this.docFactory = options.docFactory ?? (async () => new LoroDoc());
|
|
988
|
-
const configuredDebounce = options.docFrontierDebounceMs;
|
|
989
|
-
this.docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
|
|
1175
|
+
get docFrontierKeys() {
|
|
1176
|
+
return this.state.docFrontierKeys;
|
|
990
1177
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
1178
|
+
constructor(options) {
|
|
1179
|
+
this.storage = options.storage;
|
|
1180
|
+
this.docFactory = options.docFactory;
|
|
1181
|
+
this.docFrontierDebounceMs = options.docFrontierDebounceMs;
|
|
1182
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
1183
|
+
this.eventBus = options.eventBus;
|
|
1184
|
+
this.persistMeta = options.persistMeta;
|
|
1185
|
+
this.state = options.state;
|
|
1186
|
+
}
|
|
1187
|
+
async openCollaborativeDoc(docId, whenSyncedWithRemote) {
|
|
1188
|
+
const doc = await this.ensureDoc(docId);
|
|
1189
|
+
const refs = this.docRefs.get(docId) ?? 0;
|
|
1190
|
+
this.docRefs.set(docId, refs + 1);
|
|
1191
|
+
return new RepoDocHandleImpl(docId, doc, whenSyncedWithRemote, async () => this.onDocHandleClose(docId, doc));
|
|
994
1192
|
}
|
|
995
|
-
async
|
|
996
|
-
|
|
997
|
-
const snapshot = await this.storage.loadMeta();
|
|
998
|
-
if (snapshot) this.metaFlock = snapshot;
|
|
999
|
-
}
|
|
1000
|
-
this.hydrateMetadataFromFlock("sync");
|
|
1193
|
+
async openDetachedDoc(docId) {
|
|
1194
|
+
return new RepoDocHandleImpl(docId, await this.materializeDetachedDoc(docId), Promise.resolve(), async () => {});
|
|
1001
1195
|
}
|
|
1002
|
-
async
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
this.pushEventBy("sync");
|
|
1009
|
-
const recordedEvents = [];
|
|
1010
|
-
const unsubscribe = this.metaFlock.subscribe((batch) => {
|
|
1011
|
-
if (batch.source === "local") return;
|
|
1012
|
-
recordedEvents.push(...batch.events);
|
|
1013
|
-
});
|
|
1014
|
-
try {
|
|
1015
|
-
if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
|
|
1016
|
-
if (recordedEvents.length > 0) this.applyMetaFlockEvents(recordedEvents, "sync");
|
|
1017
|
-
else this.hydrateMetadataFromFlock("sync");
|
|
1018
|
-
await this.persistMeta();
|
|
1019
|
-
} finally {
|
|
1020
|
-
unsubscribe();
|
|
1021
|
-
this.popEventBy();
|
|
1022
|
-
}
|
|
1196
|
+
async ensureDoc(docId) {
|
|
1197
|
+
const cached = this.docs.get(docId);
|
|
1198
|
+
if (cached) {
|
|
1199
|
+
this.ensureDocSubscription(docId, cached);
|
|
1200
|
+
if (!this.docPersistedVersions.has(docId)) this.docPersistedVersions.set(docId, cached.version());
|
|
1201
|
+
return cached;
|
|
1023
1202
|
}
|
|
1024
|
-
if (
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
try {
|
|
1030
|
-
if (!(await this.transport.syncDoc(docId, doc)).ok) throw new Error(`Document sync failed for ${docId}`);
|
|
1031
|
-
} finally {
|
|
1032
|
-
this.popEventBy();
|
|
1033
|
-
}
|
|
1034
|
-
await this.persistDoc(docId, doc);
|
|
1035
|
-
await this.updateDocFrontiers(docId, doc, "sync");
|
|
1203
|
+
if (this.storage) {
|
|
1204
|
+
const stored = await this.storage.loadDoc(docId);
|
|
1205
|
+
if (stored) {
|
|
1206
|
+
this.registerDoc(docId, stored);
|
|
1207
|
+
return stored;
|
|
1036
1208
|
}
|
|
1037
1209
|
}
|
|
1210
|
+
const created = await this.docFactory(docId);
|
|
1211
|
+
this.registerDoc(docId, created);
|
|
1212
|
+
return created;
|
|
1038
1213
|
}
|
|
1039
|
-
|
|
1040
|
-
const
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
this.emit({
|
|
1046
|
-
kind: "doc-metadata",
|
|
1047
|
-
docId,
|
|
1048
|
-
patch: {},
|
|
1049
|
-
by
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1214
|
+
async persistDoc(docId, doc) {
|
|
1215
|
+
const previousVersion = this.docPersistedVersions.get(docId);
|
|
1216
|
+
const snapshot = doc.export({ mode: "snapshot" });
|
|
1217
|
+
const nextVersion = doc.version();
|
|
1218
|
+
if (!this.storage) {
|
|
1219
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1052
1220
|
return;
|
|
1053
1221
|
}
|
|
1054
|
-
this.
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
})
|
|
1222
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1223
|
+
try {
|
|
1224
|
+
await this.storage.save({
|
|
1225
|
+
type: "doc-snapshot",
|
|
1226
|
+
docId,
|
|
1227
|
+
snapshot
|
|
1228
|
+
});
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
if (previousVersion) this.docPersistedVersions.set(docId, previousVersion);
|
|
1231
|
+
else this.docPersistedVersions.delete(docId);
|
|
1232
|
+
throw error;
|
|
1233
|
+
}
|
|
1062
1234
|
}
|
|
1063
|
-
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
kind: "asset-unlink",
|
|
1072
|
-
docId,
|
|
1073
|
-
assetId,
|
|
1074
|
-
by
|
|
1075
|
-
});
|
|
1076
|
-
if (!Array.from(this.docAssets.values()).some((assets) => assets.has(assetId))) {
|
|
1077
|
-
const record = this.assets.get(assetId);
|
|
1078
|
-
if (record) {
|
|
1079
|
-
const deletedAt = this.orphanedAssets.get(assetId)?.deletedAt ?? Date.now();
|
|
1080
|
-
this.orphanedAssets.set(assetId, {
|
|
1081
|
-
metadata: record.metadata,
|
|
1082
|
-
deletedAt
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
this.docAssets.set(docId, mapping);
|
|
1091
|
-
const added = [];
|
|
1092
|
-
const removed = [];
|
|
1093
|
-
if (previous) {
|
|
1094
|
-
for (const assetId of previous.keys()) if (!mapping.has(assetId)) removed.push(assetId);
|
|
1095
|
-
}
|
|
1096
|
-
for (const assetId of mapping.keys()) if (!previous || !previous.has(assetId)) added.push(assetId);
|
|
1097
|
-
for (const assetId of removed) {
|
|
1098
|
-
this.emit({
|
|
1099
|
-
kind: "asset-unlink",
|
|
1235
|
+
async updateDocFrontiers(docId, doc, defaultBy) {
|
|
1236
|
+
const { json, key } = canonicalizeVersionVector(doc.version());
|
|
1237
|
+
const existingKeys = this.docFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1238
|
+
let mutated = false;
|
|
1239
|
+
if (existingKeys.size !== 1 || !existingKeys.has(key)) {
|
|
1240
|
+
const metaFlock = this.metaFlock;
|
|
1241
|
+
for (const entry of existingKeys) metaFlock.delete([
|
|
1242
|
+
"f",
|
|
1100
1243
|
docId,
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
deletedAt
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1244
|
+
entry
|
|
1245
|
+
]);
|
|
1246
|
+
metaFlock.put([
|
|
1247
|
+
"f",
|
|
1248
|
+
docId,
|
|
1249
|
+
key
|
|
1250
|
+
], json);
|
|
1251
|
+
this.docFrontierKeys.set(docId, new Set([key]));
|
|
1252
|
+
mutated = true;
|
|
1114
1253
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1254
|
+
if (mutated) await this.persistMeta();
|
|
1255
|
+
const by = this.eventBus.resolveEventBy(defaultBy);
|
|
1256
|
+
const frontiers = getDocFrontiers(doc);
|
|
1257
|
+
this.eventBus.emit({
|
|
1258
|
+
kind: "doc-frontiers",
|
|
1117
1259
|
docId,
|
|
1118
|
-
|
|
1260
|
+
frontiers,
|
|
1119
1261
|
by
|
|
1120
1262
|
});
|
|
1121
1263
|
}
|
|
1122
|
-
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1264
|
+
async flushScheduledDocFrontierUpdate(docId) {
|
|
1265
|
+
const pending = this.docFrontierUpdates.get(docId);
|
|
1266
|
+
if (!pending) return false;
|
|
1267
|
+
clearTimeout(pending.timeout);
|
|
1268
|
+
this.docFrontierUpdates.delete(docId);
|
|
1269
|
+
this.eventBus.pushEventBy(pending.by);
|
|
1270
|
+
try {
|
|
1271
|
+
await this.updateDocFrontiers(docId, pending.doc, pending.by);
|
|
1272
|
+
} finally {
|
|
1273
|
+
this.eventBus.popEventBy();
|
|
1128
1274
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1275
|
+
return true;
|
|
1276
|
+
}
|
|
1277
|
+
async close() {
|
|
1278
|
+
for (const unsubscribe of this.docSubscriptions.values()) try {
|
|
1279
|
+
unsubscribe();
|
|
1280
|
+
} catch {}
|
|
1281
|
+
this.docSubscriptions.clear();
|
|
1282
|
+
const pendingDocIds = Array.from(this.docFrontierUpdates.keys());
|
|
1283
|
+
for (const docId of pendingDocIds) try {
|
|
1284
|
+
await this.flushScheduledDocFrontierUpdate(docId);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
logAsyncError(`doc ${docId} frontier flush on close`)(error);
|
|
1287
|
+
}
|
|
1288
|
+
this.docFrontierUpdates.clear();
|
|
1289
|
+
this.docs.clear();
|
|
1290
|
+
this.docRefs.clear();
|
|
1291
|
+
this.docPersistedVersions.clear();
|
|
1292
|
+
this.docFrontierKeys.clear();
|
|
1293
|
+
}
|
|
1294
|
+
hydrateFrontierKeys() {
|
|
1295
|
+
const nextFrontierKeys = /* @__PURE__ */ new Map();
|
|
1296
|
+
const frontierRows = this.metaFlock.scan({ prefix: ["f"] });
|
|
1297
|
+
for (const row of frontierRows) {
|
|
1298
|
+
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1299
|
+
const docId = row.key[1];
|
|
1300
|
+
const frontierKey = row.key[2];
|
|
1301
|
+
if (typeof docId !== "string" || typeof frontierKey !== "string") continue;
|
|
1302
|
+
const set = nextFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1303
|
+
set.add(frontierKey);
|
|
1304
|
+
nextFrontierKeys.set(docId, set);
|
|
1305
|
+
}
|
|
1306
|
+
this.docFrontierKeys.clear();
|
|
1307
|
+
for (const [docId, keys] of nextFrontierKeys) this.docFrontierKeys.set(docId, keys);
|
|
1137
1308
|
}
|
|
1138
1309
|
refreshDocFrontierKeys(docId) {
|
|
1139
1310
|
const rows = this.metaFlock.scan({ prefix: ["f", docId] });
|
|
@@ -1146,154 +1317,166 @@ var LoroRepo = class {
|
|
|
1146
1317
|
if (keys.size > 0) this.docFrontierKeys.set(docId, keys);
|
|
1147
1318
|
else this.docFrontierKeys.delete(docId);
|
|
1148
1319
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
if (!rows.length) return void 0;
|
|
1152
|
-
const docMeta = {};
|
|
1153
|
-
let populated = false;
|
|
1154
|
-
for (const row of rows) {
|
|
1155
|
-
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1156
|
-
if (row.key.length === 2) {
|
|
1157
|
-
const obj = asJsonObject(row.value);
|
|
1158
|
-
if (!obj) continue;
|
|
1159
|
-
for (const [field, value] of Object.entries(obj)) {
|
|
1160
|
-
const cloned = cloneJsonValue(value);
|
|
1161
|
-
if (cloned !== void 0) {
|
|
1162
|
-
docMeta[field] = cloned;
|
|
1163
|
-
populated = true;
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
continue;
|
|
1167
|
-
}
|
|
1168
|
-
const fieldKey = row.key[2];
|
|
1169
|
-
if (typeof fieldKey !== "string") continue;
|
|
1170
|
-
if (fieldKey === "$tombstone") {
|
|
1171
|
-
docMeta.tombstone = Boolean(row.value);
|
|
1172
|
-
populated = true;
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
const jsonValue = cloneJsonValue(row.value);
|
|
1176
|
-
if (jsonValue === void 0) continue;
|
|
1177
|
-
docMeta[fieldKey] = jsonValue;
|
|
1178
|
-
populated = true;
|
|
1179
|
-
}
|
|
1180
|
-
return populated ? docMeta : void 0;
|
|
1181
|
-
}
|
|
1182
|
-
readDocAssetsFromFlock(docId) {
|
|
1183
|
-
const rows = this.metaFlock.scan({ prefix: ["ld", docId] });
|
|
1184
|
-
const mapping = /* @__PURE__ */ new Map();
|
|
1185
|
-
for (const row of rows) {
|
|
1186
|
-
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1187
|
-
const assetId = row.key[2];
|
|
1188
|
-
if (typeof assetId !== "string") continue;
|
|
1189
|
-
if (!(row.value !== void 0 && row.value !== null && row.value !== false)) continue;
|
|
1190
|
-
let metadata = this.assets.get(assetId)?.metadata;
|
|
1191
|
-
if (!metadata) {
|
|
1192
|
-
metadata = this.readAssetMetadataFromFlock(assetId);
|
|
1193
|
-
if (!metadata) continue;
|
|
1194
|
-
this.rememberAsset(metadata);
|
|
1195
|
-
}
|
|
1196
|
-
mapping.set(assetId, cloneRepoAssetMetadata(metadata));
|
|
1197
|
-
}
|
|
1198
|
-
return mapping;
|
|
1320
|
+
get metaFlock() {
|
|
1321
|
+
return this.getMetaFlock();
|
|
1199
1322
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1323
|
+
registerDoc(docId, doc) {
|
|
1324
|
+
this.docs.set(docId, doc);
|
|
1325
|
+
this.docPersistedVersions.set(docId, doc.version());
|
|
1326
|
+
this.ensureDocSubscription(docId, doc);
|
|
1202
1327
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
metadata: record.metadata,
|
|
1210
|
-
deletedAt
|
|
1328
|
+
ensureDocSubscription(docId, doc) {
|
|
1329
|
+
if (this.docSubscriptions.has(docId)) return;
|
|
1330
|
+
const unsubscribe = doc.subscribe((batch) => {
|
|
1331
|
+
const stackBy = this.eventBus.resolveEventBy("local");
|
|
1332
|
+
const by = stackBy === "local" && batch.by === "import" ? "live" : stackBy;
|
|
1333
|
+
this.onDocEvent(docId, doc, batch, by);
|
|
1211
1334
|
});
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1335
|
+
if (typeof unsubscribe === "function") this.docSubscriptions.set(docId, unsubscribe);
|
|
1336
|
+
}
|
|
1337
|
+
scheduleDocFrontierUpdate(docId, doc, by) {
|
|
1338
|
+
const existing = this.docFrontierUpdates.get(docId);
|
|
1339
|
+
const effectiveBy = existing ? this.mergeRepoEventBy(existing.by, by) : by;
|
|
1340
|
+
if (existing) clearTimeout(existing.timeout);
|
|
1341
|
+
const delay = this.docFrontierDebounceMs > 0 ? this.docFrontierDebounceMs : 0;
|
|
1342
|
+
const timeout = setTimeout(() => this.runScheduledDocFrontierUpdate(docId), delay);
|
|
1343
|
+
this.docFrontierUpdates.set(docId, {
|
|
1344
|
+
timeout,
|
|
1345
|
+
doc,
|
|
1346
|
+
by: effectiveBy
|
|
1222
1347
|
});
|
|
1223
1348
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1349
|
+
mergeRepoEventBy(current, next) {
|
|
1350
|
+
if (current === next) return current;
|
|
1351
|
+
if (current === "live" || next === "live") return "live";
|
|
1352
|
+
if (current === "sync" || next === "sync") return "sync";
|
|
1353
|
+
return "local";
|
|
1226
1354
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
if (!
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
if (this.metaRoomSubscription === wrapped) this.metaRoomSubscription = void 0;
|
|
1238
|
-
if (this.unsubscribeMetaFlock) {
|
|
1239
|
-
this.unsubscribeMetaFlock();
|
|
1240
|
-
this.unsubscribeMetaFlock = void 0;
|
|
1241
|
-
}
|
|
1242
|
-
},
|
|
1243
|
-
firstSyncedWithRemote: subscription.firstSyncedWithRemote,
|
|
1244
|
-
get connected() {
|
|
1245
|
-
return subscription.connected;
|
|
1355
|
+
runScheduledDocFrontierUpdate(docId) {
|
|
1356
|
+
const pending = this.docFrontierUpdates.get(docId);
|
|
1357
|
+
if (!pending) return;
|
|
1358
|
+
this.docFrontierUpdates.delete(docId);
|
|
1359
|
+
this.eventBus.pushEventBy(pending.by);
|
|
1360
|
+
(async () => {
|
|
1361
|
+
try {
|
|
1362
|
+
await this.updateDocFrontiers(docId, pending.doc, pending.by);
|
|
1363
|
+
} finally {
|
|
1364
|
+
this.eventBus.popEventBy();
|
|
1246
1365
|
}
|
|
1247
|
-
};
|
|
1248
|
-
this.metaRoomSubscription = wrapped;
|
|
1249
|
-
subscription.firstSyncedWithRemote.then(async () => {
|
|
1250
|
-
const by = this.resolveEventBy("live");
|
|
1251
|
-
this.hydrateMetadataFromFlock(by);
|
|
1252
|
-
await this.persistMeta();
|
|
1253
|
-
}).catch(logAsyncError("meta room first sync"));
|
|
1254
|
-
return wrapped;
|
|
1366
|
+
})().catch(logAsyncError(`doc ${docId} frontier debounce`));
|
|
1255
1367
|
}
|
|
1256
|
-
async
|
|
1257
|
-
|
|
1258
|
-
if (
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1368
|
+
async onDocHandleClose(docId, doc) {
|
|
1369
|
+
const refs = this.docRefs.get(docId) ?? 0;
|
|
1370
|
+
if (refs <= 1) {
|
|
1371
|
+
this.docRefs.delete(docId);
|
|
1372
|
+
this.docSubscriptions.get(docId)?.();
|
|
1373
|
+
this.docSubscriptions.delete(docId);
|
|
1374
|
+
this.docs.delete(docId);
|
|
1375
|
+
this.docPersistedVersions.delete(docId);
|
|
1376
|
+
} else this.docRefs.set(docId, refs - 1);
|
|
1377
|
+
await this.persistDocUpdate(docId, doc);
|
|
1378
|
+
if (!await this.flushScheduledDocFrontierUpdate(docId)) await this.updateDocFrontiers(docId, doc, "local");
|
|
1264
1379
|
}
|
|
1265
|
-
async
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
if (
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1380
|
+
async materializeDetachedDoc(docId) {
|
|
1381
|
+
const snapshot = await this.exportDocSnapshot(docId);
|
|
1382
|
+
if (snapshot) return LoroDoc.fromSnapshot(snapshot);
|
|
1383
|
+
return this.docFactory(docId);
|
|
1384
|
+
}
|
|
1385
|
+
async exportDocSnapshot(docId) {
|
|
1386
|
+
const cached = this.docs.get(docId);
|
|
1387
|
+
if (cached) return cached.export({ mode: "snapshot" });
|
|
1388
|
+
if (!this.storage) return;
|
|
1389
|
+
return (await this.storage.loadDoc(docId))?.export({ mode: "snapshot" });
|
|
1390
|
+
}
|
|
1391
|
+
async persistDocUpdate(docId, doc) {
|
|
1392
|
+
const previousVersion = this.docPersistedVersions.get(docId);
|
|
1393
|
+
const nextVersion = doc.version();
|
|
1394
|
+
if (!this.storage) {
|
|
1395
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
if (!previousVersion) {
|
|
1399
|
+
await this.persistDoc(docId, doc);
|
|
1400
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const update = doc.export({
|
|
1404
|
+
mode: "update",
|
|
1405
|
+
from: previousVersion
|
|
1406
|
+
});
|
|
1407
|
+
if (!update.length) {
|
|
1408
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1412
|
+
try {
|
|
1413
|
+
await this.storage.save({
|
|
1414
|
+
type: "doc-update",
|
|
1415
|
+
docId,
|
|
1416
|
+
update
|
|
1417
|
+
});
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
this.docPersistedVersions.set(docId, previousVersion);
|
|
1420
|
+
throw error;
|
|
1281
1421
|
}
|
|
1282
|
-
this.docFrontierUpdates.clear();
|
|
1283
|
-
this.watchers.clear();
|
|
1284
|
-
this.docs.clear();
|
|
1285
|
-
this.docRefs.clear();
|
|
1286
|
-
this.metadata.clear();
|
|
1287
|
-
this.docAssets.clear();
|
|
1288
|
-
this.assets.clear();
|
|
1289
|
-
this.docFrontierKeys.clear();
|
|
1290
|
-
this.docPersistedVersions.clear();
|
|
1291
|
-
this.readyPromise = void 0;
|
|
1292
|
-
await this.transport?.close();
|
|
1293
1422
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1423
|
+
onDocEvent(docId, doc, _batch, by) {
|
|
1424
|
+
(async () => {
|
|
1425
|
+
const persist = this.persistDocUpdate(docId, doc);
|
|
1426
|
+
if (by === "local") {
|
|
1427
|
+
this.scheduleDocFrontierUpdate(docId, doc, by);
|
|
1428
|
+
await persist;
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
const flushed = this.flushScheduledDocFrontierUpdate(docId);
|
|
1432
|
+
const updated = this.updateDocFrontiers(docId, doc, by);
|
|
1433
|
+
await Promise.all([
|
|
1434
|
+
persist,
|
|
1435
|
+
flushed,
|
|
1436
|
+
updated
|
|
1437
|
+
]);
|
|
1438
|
+
})().catch(logAsyncError(`doc ${docId} event processing`));
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
|
|
1442
|
+
//#endregion
|
|
1443
|
+
//#region src/internal/metadata-manager.ts
|
|
1444
|
+
var MetadataManager = class {
|
|
1445
|
+
getMetaFlock;
|
|
1446
|
+
eventBus;
|
|
1447
|
+
persistMeta;
|
|
1448
|
+
state;
|
|
1449
|
+
constructor(options) {
|
|
1450
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
1451
|
+
this.eventBus = options.eventBus;
|
|
1452
|
+
this.persistMeta = options.persistMeta;
|
|
1453
|
+
this.state = options.state;
|
|
1454
|
+
}
|
|
1455
|
+
getDocIds() {
|
|
1456
|
+
return Array.from(this.state.metadata.keys());
|
|
1457
|
+
}
|
|
1458
|
+
entries() {
|
|
1459
|
+
return this.state.metadata.entries();
|
|
1460
|
+
}
|
|
1461
|
+
get(docId) {
|
|
1462
|
+
const metadata = this.state.metadata.get(docId);
|
|
1463
|
+
return metadata ? cloneJsonObject(metadata) : void 0;
|
|
1464
|
+
}
|
|
1465
|
+
list(query) {
|
|
1466
|
+
const entries = [];
|
|
1467
|
+
for (const [docId, metadata] of this.state.metadata.entries()) {
|
|
1468
|
+
if (!matchesQuery(docId, metadata, query)) continue;
|
|
1469
|
+
entries.push({
|
|
1470
|
+
docId,
|
|
1471
|
+
meta: cloneJsonObject(metadata)
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
entries.sort((a, b) => a.docId < b.docId ? -1 : a.docId > b.docId ? 1 : 0);
|
|
1475
|
+
if (query?.limit !== void 0) return entries.slice(0, query.limit);
|
|
1476
|
+
return entries;
|
|
1477
|
+
}
|
|
1478
|
+
async upsert(docId, patch) {
|
|
1479
|
+
const base = this.state.metadata.get(docId);
|
|
1297
1480
|
const next = base ? cloneJsonObject(base) : {};
|
|
1298
1481
|
const outPatch = {};
|
|
1299
1482
|
let changed = false;
|
|
@@ -1318,70 +1501,139 @@ var LoroRepo = class {
|
|
|
1318
1501
|
changed = true;
|
|
1319
1502
|
}
|
|
1320
1503
|
if (!changed) {
|
|
1321
|
-
if (!this.metadata.has(docId)) this.metadata.set(docId, next);
|
|
1504
|
+
if (!this.state.metadata.has(docId)) this.state.metadata.set(docId, next);
|
|
1322
1505
|
return;
|
|
1323
1506
|
}
|
|
1324
|
-
this.metadata.set(docId, next);
|
|
1507
|
+
this.state.metadata.set(docId, next);
|
|
1325
1508
|
await this.persistMeta();
|
|
1326
|
-
this.emit({
|
|
1509
|
+
this.eventBus.emit({
|
|
1327
1510
|
kind: "doc-metadata",
|
|
1328
1511
|
docId,
|
|
1329
1512
|
patch: cloneJsonObject(outPatch),
|
|
1330
1513
|
by: "local"
|
|
1331
1514
|
});
|
|
1332
1515
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1516
|
+
refreshFromFlock(docId, by) {
|
|
1517
|
+
const previous = this.state.metadata.get(docId);
|
|
1518
|
+
const next = this.readDocMetadataFromFlock(docId);
|
|
1519
|
+
if (!next) {
|
|
1520
|
+
if (previous) {
|
|
1521
|
+
this.state.metadata.delete(docId);
|
|
1522
|
+
this.eventBus.emit({
|
|
1523
|
+
kind: "doc-metadata",
|
|
1524
|
+
docId,
|
|
1525
|
+
patch: {},
|
|
1526
|
+
by
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
this.state.metadata.set(docId, next);
|
|
1532
|
+
const patch = diffJsonObjects(previous, next);
|
|
1533
|
+
if (!previous || Object.keys(patch).length > 0) this.eventBus.emit({
|
|
1534
|
+
kind: "doc-metadata",
|
|
1535
|
+
docId,
|
|
1536
|
+
patch,
|
|
1537
|
+
by
|
|
1538
|
+
});
|
|
1337
1539
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
for (const [docId,
|
|
1342
|
-
|
|
1343
|
-
|
|
1540
|
+
replaceAll(nextMetadata, by) {
|
|
1541
|
+
const prevMetadata = new Map(this.state.metadata);
|
|
1542
|
+
this.state.metadata.clear();
|
|
1543
|
+
for (const [docId, meta] of nextMetadata) this.state.metadata.set(docId, meta);
|
|
1544
|
+
const docIds = new Set([...prevMetadata.keys(), ...nextMetadata.keys()]);
|
|
1545
|
+
for (const docId of docIds) {
|
|
1546
|
+
const previous = prevMetadata.get(docId);
|
|
1547
|
+
const current = nextMetadata.get(docId);
|
|
1548
|
+
if (!current) {
|
|
1549
|
+
if (previous) this.eventBus.emit({
|
|
1550
|
+
kind: "doc-metadata",
|
|
1551
|
+
docId,
|
|
1552
|
+
patch: {},
|
|
1553
|
+
by
|
|
1554
|
+
});
|
|
1555
|
+
continue;
|
|
1556
|
+
}
|
|
1557
|
+
const patch = diffJsonObjects(previous, current);
|
|
1558
|
+
if (!previous || Object.keys(patch).length > 0) this.eventBus.emit({
|
|
1559
|
+
kind: "doc-metadata",
|
|
1344
1560
|
docId,
|
|
1345
|
-
|
|
1561
|
+
patch,
|
|
1562
|
+
by
|
|
1346
1563
|
});
|
|
1347
1564
|
}
|
|
1348
|
-
entries.sort((a, b) => a.docId < b.docId ? -1 : a.docId > b.docId ? 1 : 0);
|
|
1349
|
-
if (query?.limit !== void 0) return entries.slice(0, query.limit);
|
|
1350
|
-
return entries;
|
|
1351
1565
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1566
|
+
clear() {
|
|
1567
|
+
this.state.metadata.clear();
|
|
1354
1568
|
}
|
|
1355
|
-
|
|
1356
|
-
const
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1569
|
+
readDocMetadataFromFlock(docId) {
|
|
1570
|
+
const rows = this.metaFlock.scan({ prefix: ["m", docId] });
|
|
1571
|
+
if (!rows.length) return void 0;
|
|
1572
|
+
const docMeta = {};
|
|
1573
|
+
let populated = false;
|
|
1574
|
+
for (const row of rows) {
|
|
1575
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1576
|
+
if (row.key.length === 2) {
|
|
1577
|
+
const obj = asJsonObject(row.value);
|
|
1578
|
+
if (!obj) continue;
|
|
1579
|
+
for (const [field, value] of Object.entries(obj)) {
|
|
1580
|
+
const cloned = cloneJsonValue(value);
|
|
1581
|
+
if (cloned !== void 0) {
|
|
1582
|
+
docMeta[field] = cloned;
|
|
1583
|
+
populated = true;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
const fieldKey = row.key[2];
|
|
1589
|
+
if (typeof fieldKey !== "string") continue;
|
|
1590
|
+
if (fieldKey === "$tombstone") {
|
|
1591
|
+
docMeta.tombstone = Boolean(row.value);
|
|
1592
|
+
populated = true;
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
const jsonValue = cloneJsonValue(row.value);
|
|
1596
|
+
if (jsonValue === void 0) continue;
|
|
1597
|
+
docMeta[fieldKey] = jsonValue;
|
|
1598
|
+
populated = true;
|
|
1599
|
+
}
|
|
1600
|
+
return populated ? docMeta : void 0;
|
|
1364
1601
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
* and schedules a doc-level sync so `whenSyncedWithRemote` resolves after remote backfills.
|
|
1368
|
-
*/
|
|
1369
|
-
async openCollaborativeDoc(docId) {
|
|
1370
|
-
const doc = await this.ensureDoc(docId);
|
|
1371
|
-
const refs = this.docRefs.get(docId) ?? 0;
|
|
1372
|
-
this.docRefs.set(docId, refs + 1);
|
|
1373
|
-
return new RepoDocHandleImpl(docId, doc, this.whenDocInSyncWithRemote(docId), async (id, instance) => this.onDocHandleClose(id, instance));
|
|
1602
|
+
get metaFlock() {
|
|
1603
|
+
return this.getMetaFlock();
|
|
1374
1604
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1605
|
+
};
|
|
1606
|
+
|
|
1607
|
+
//#endregion
|
|
1608
|
+
//#region src/internal/asset-manager.ts
|
|
1609
|
+
var AssetManager = class {
|
|
1610
|
+
storage;
|
|
1611
|
+
assetTransport;
|
|
1612
|
+
getMetaFlock;
|
|
1613
|
+
eventBus;
|
|
1614
|
+
persistMeta;
|
|
1615
|
+
state;
|
|
1616
|
+
get docAssets() {
|
|
1617
|
+
return this.state.docAssets;
|
|
1618
|
+
}
|
|
1619
|
+
get assets() {
|
|
1620
|
+
return this.state.assets;
|
|
1621
|
+
}
|
|
1622
|
+
get orphanedAssets() {
|
|
1623
|
+
return this.state.orphanedAssets;
|
|
1624
|
+
}
|
|
1625
|
+
get assetToDocRefs() {
|
|
1626
|
+
return this.state.assetToDocRefs;
|
|
1627
|
+
}
|
|
1628
|
+
constructor(options) {
|
|
1629
|
+
this.storage = options.storage;
|
|
1630
|
+
this.assetTransport = options.assetTransport;
|
|
1631
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
1632
|
+
this.eventBus = options.eventBus;
|
|
1633
|
+
this.persistMeta = options.persistMeta;
|
|
1634
|
+
this.state = options.state;
|
|
1382
1635
|
}
|
|
1383
1636
|
async uploadAsset(params) {
|
|
1384
|
-
await this.ready();
|
|
1385
1637
|
const bytes = await assetContentToUint8Array(params.content);
|
|
1386
1638
|
const assetId = await computeSha256(bytes);
|
|
1387
1639
|
if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
|
|
@@ -1418,7 +1670,7 @@ var LoroRepo = class {
|
|
|
1418
1670
|
existing.metadata = metadata$1;
|
|
1419
1671
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata$1));
|
|
1420
1672
|
await this.persistMeta();
|
|
1421
|
-
this.emit({
|
|
1673
|
+
this.eventBus.emit({
|
|
1422
1674
|
kind: "asset-metadata",
|
|
1423
1675
|
asset: this.createAssetDownload(assetId, metadata$1, existing.data),
|
|
1424
1676
|
by: "local"
|
|
@@ -1451,26 +1703,17 @@ var LoroRepo = class {
|
|
|
1451
1703
|
data: storedBytes.slice()
|
|
1452
1704
|
});
|
|
1453
1705
|
this.rememberAsset(metadata, storedBytes);
|
|
1454
|
-
|
|
1706
|
+
this.updateDocAssetMetadata(assetId, metadata);
|
|
1455
1707
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
1456
1708
|
await this.persistMeta();
|
|
1457
|
-
this.emit({
|
|
1709
|
+
this.eventBus.emit({
|
|
1458
1710
|
kind: "asset-metadata",
|
|
1459
1711
|
asset: this.createAssetDownload(assetId, metadata, storedBytes),
|
|
1460
1712
|
by: "local"
|
|
1461
1713
|
});
|
|
1462
1714
|
return assetId;
|
|
1463
1715
|
}
|
|
1464
|
-
async whenDocInSyncWithRemote(docId) {
|
|
1465
|
-
await this.ready();
|
|
1466
|
-
await this.ensureDoc(docId);
|
|
1467
|
-
await this.sync({
|
|
1468
|
-
scope: "doc",
|
|
1469
|
-
docIds: [docId]
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
1716
|
async linkAsset(docId, params) {
|
|
1473
|
-
await this.ready();
|
|
1474
1717
|
const bytes = await assetContentToUint8Array(params.content);
|
|
1475
1718
|
const assetId = await computeSha256(bytes);
|
|
1476
1719
|
if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
|
|
@@ -1524,7 +1767,7 @@ var LoroRepo = class {
|
|
|
1524
1767
|
metadata = nextMetadata;
|
|
1525
1768
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
1526
1769
|
await this.persistMeta();
|
|
1527
|
-
this.emit({
|
|
1770
|
+
this.eventBus.emit({
|
|
1528
1771
|
kind: "asset-metadata",
|
|
1529
1772
|
asset: this.createAssetDownload(assetId, metadata, existing.data),
|
|
1530
1773
|
by: "local"
|
|
@@ -1557,7 +1800,7 @@ var LoroRepo = class {
|
|
|
1557
1800
|
data: storedBytes.slice()
|
|
1558
1801
|
});
|
|
1559
1802
|
this.rememberAsset(metadata, storedBytes);
|
|
1560
|
-
|
|
1803
|
+
this.updateDocAssetMetadata(assetId, metadata);
|
|
1561
1804
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
1562
1805
|
created = true;
|
|
1563
1806
|
}
|
|
@@ -1573,26 +1816,20 @@ var LoroRepo = class {
|
|
|
1573
1816
|
assetId
|
|
1574
1817
|
], true);
|
|
1575
1818
|
await this.persistMeta();
|
|
1576
|
-
this.emit({
|
|
1819
|
+
this.eventBus.emit({
|
|
1577
1820
|
kind: "asset-link",
|
|
1578
1821
|
docId,
|
|
1579
1822
|
assetId,
|
|
1580
1823
|
by: "local"
|
|
1581
1824
|
});
|
|
1582
|
-
if (created) this.emit({
|
|
1825
|
+
if (created) this.eventBus.emit({
|
|
1583
1826
|
kind: "asset-metadata",
|
|
1584
1827
|
asset: this.createAssetDownload(assetId, metadata, storedBytes ?? bytes),
|
|
1585
1828
|
by: "local"
|
|
1586
1829
|
});
|
|
1587
1830
|
return assetId;
|
|
1588
1831
|
}
|
|
1589
|
-
async fetchAsset(assetId) {
|
|
1590
|
-
await this.ready();
|
|
1591
|
-
const { metadata, bytes } = await this.materializeAsset(assetId);
|
|
1592
|
-
return this.createAssetDownload(assetId, metadata, bytes);
|
|
1593
|
-
}
|
|
1594
1832
|
async unlinkAsset(docId, assetId) {
|
|
1595
|
-
await this.ready();
|
|
1596
1833
|
const mapping = this.docAssets.get(docId);
|
|
1597
1834
|
if (!mapping || !mapping.has(assetId)) return;
|
|
1598
1835
|
mapping.delete(assetId);
|
|
@@ -1617,7 +1854,7 @@ var LoroRepo = class {
|
|
|
1617
1854
|
}
|
|
1618
1855
|
}
|
|
1619
1856
|
await this.persistMeta();
|
|
1620
|
-
this.emit({
|
|
1857
|
+
this.eventBus.emit({
|
|
1621
1858
|
kind: "asset-unlink",
|
|
1622
1859
|
docId,
|
|
1623
1860
|
assetId,
|
|
@@ -1625,7 +1862,6 @@ var LoroRepo = class {
|
|
|
1625
1862
|
});
|
|
1626
1863
|
}
|
|
1627
1864
|
async listAssets(docId) {
|
|
1628
|
-
await this.ready();
|
|
1629
1865
|
const mapping = this.docAssets.get(docId);
|
|
1630
1866
|
if (!mapping) return [];
|
|
1631
1867
|
return Array.from(mapping.values()).map((asset) => ({ ...asset }));
|
|
@@ -1633,36 +1869,251 @@ var LoroRepo = class {
|
|
|
1633
1869
|
async ensureAsset(assetId) {
|
|
1634
1870
|
return this.fetchAsset(assetId);
|
|
1635
1871
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
return
|
|
1639
|
-
assetId,
|
|
1640
|
-
size: metadata.size,
|
|
1641
|
-
createdAt: metadata.createdAt,
|
|
1642
|
-
mime: metadata.mime,
|
|
1643
|
-
policy: metadata.policy,
|
|
1644
|
-
tag: metadata.tag,
|
|
1645
|
-
content: async () => {
|
|
1646
|
-
if (!cached) cached = (await this.materializeAsset(assetId)).bytes.slice();
|
|
1647
|
-
return toReadableStream(cached.slice());
|
|
1648
|
-
}
|
|
1649
|
-
};
|
|
1872
|
+
async fetchAsset(assetId) {
|
|
1873
|
+
const { metadata, bytes } = await this.materializeAsset(assetId);
|
|
1874
|
+
return this.createAssetDownload(assetId, metadata, bytes);
|
|
1650
1875
|
}
|
|
1651
|
-
async
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1876
|
+
async gcAssets(options = {}) {
|
|
1877
|
+
const { minKeepMs = 0 } = options;
|
|
1878
|
+
const now = Date.now();
|
|
1879
|
+
let removed = 0;
|
|
1880
|
+
for (const [assetId, orphan] of Array.from(this.orphanedAssets.entries())) {
|
|
1881
|
+
if (now - orphan.deletedAt < minKeepMs) continue;
|
|
1882
|
+
this.orphanedAssets.delete(assetId);
|
|
1883
|
+
if (this.storage?.deleteAsset) try {
|
|
1884
|
+
await this.storage.deleteAsset(assetId);
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
logAsyncError(`asset ${assetId} delete`)(error);
|
|
1887
|
+
}
|
|
1888
|
+
removed += 1;
|
|
1889
|
+
}
|
|
1890
|
+
return removed;
|
|
1891
|
+
}
|
|
1892
|
+
refreshDocAssetsEntry(docId, by) {
|
|
1893
|
+
const mapping = this.readDocAssetsFromFlock(docId);
|
|
1894
|
+
const previous = this.docAssets.get(docId);
|
|
1895
|
+
if (!mapping.size) {
|
|
1896
|
+
if (previous?.size) {
|
|
1897
|
+
this.docAssets.delete(docId);
|
|
1898
|
+
for (const assetId of previous.keys()) {
|
|
1899
|
+
this.removeDocAssetReference(assetId, docId);
|
|
1900
|
+
this.eventBus.emit({
|
|
1901
|
+
kind: "asset-unlink",
|
|
1902
|
+
docId,
|
|
1903
|
+
assetId,
|
|
1904
|
+
by
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
this.docAssets.set(docId, mapping);
|
|
1911
|
+
const removed = [];
|
|
1912
|
+
if (previous) {
|
|
1913
|
+
for (const assetId of previous.keys()) if (!mapping.has(assetId)) removed.push(assetId);
|
|
1914
|
+
}
|
|
1915
|
+
for (const assetId of removed) {
|
|
1916
|
+
this.removeDocAssetReference(assetId, docId);
|
|
1917
|
+
this.eventBus.emit({
|
|
1918
|
+
kind: "asset-unlink",
|
|
1919
|
+
docId,
|
|
1920
|
+
assetId,
|
|
1921
|
+
by
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
for (const assetId of mapping.keys()) {
|
|
1925
|
+
const isNew = !previous || !previous.has(assetId);
|
|
1926
|
+
this.addDocReference(assetId, docId);
|
|
1927
|
+
if (isNew) this.eventBus.emit({
|
|
1928
|
+
kind: "asset-link",
|
|
1929
|
+
docId,
|
|
1930
|
+
assetId,
|
|
1931
|
+
by
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
refreshAssetMetadataEntry(assetId, by) {
|
|
1936
|
+
const previous = this.assets.get(assetId);
|
|
1937
|
+
const metadata = assetMetaFromJson(this.metaFlock.get(["a", assetId]));
|
|
1938
|
+
if (!metadata) {
|
|
1939
|
+
this.handleAssetRemoval(assetId, by);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
const existingData = previous?.data;
|
|
1943
|
+
this.rememberAsset(metadata, existingData);
|
|
1944
|
+
this.updateDocAssetMetadata(assetId, cloneRepoAssetMetadata(metadata));
|
|
1945
|
+
if (!previous || !assetMetadataEqual(previous.metadata, metadata)) this.eventBus.emit({
|
|
1946
|
+
kind: "asset-metadata",
|
|
1947
|
+
asset: this.createAssetDownload(assetId, metadata, existingData),
|
|
1948
|
+
by
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
hydrateFromFlock(by) {
|
|
1952
|
+
const prevDocAssets = new Map(this.docAssets);
|
|
1953
|
+
const prevAssets = new Map(this.assets);
|
|
1954
|
+
const nextAssets = /* @__PURE__ */ new Map();
|
|
1955
|
+
const assetRows = this.metaFlock.scan({ prefix: ["a"] });
|
|
1956
|
+
for (const row of assetRows) {
|
|
1957
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1958
|
+
const assetId = row.key[1];
|
|
1959
|
+
if (typeof assetId !== "string") continue;
|
|
1960
|
+
const metadata = assetMetaFromJson(row.value);
|
|
1961
|
+
if (!metadata) continue;
|
|
1962
|
+
const existing = this.assets.get(assetId);
|
|
1963
|
+
nextAssets.set(assetId, {
|
|
1964
|
+
metadata,
|
|
1965
|
+
data: existing?.data
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
const nextDocAssets = /* @__PURE__ */ new Map();
|
|
1969
|
+
const linkRows = this.metaFlock.scan({ prefix: ["ld"] });
|
|
1970
|
+
for (const row of linkRows) {
|
|
1971
|
+
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1972
|
+
const docId = row.key[1];
|
|
1973
|
+
const assetId = row.key[2];
|
|
1974
|
+
if (typeof docId !== "string" || typeof assetId !== "string") continue;
|
|
1975
|
+
const metadata = nextAssets.get(assetId)?.metadata;
|
|
1976
|
+
if (!metadata) continue;
|
|
1977
|
+
const mapping = nextDocAssets.get(docId) ?? /* @__PURE__ */ new Map();
|
|
1978
|
+
mapping.set(assetId, metadata);
|
|
1979
|
+
nextDocAssets.set(docId, mapping);
|
|
1980
|
+
}
|
|
1981
|
+
const removedAssets = [];
|
|
1982
|
+
for (const [assetId, record] of prevAssets) if (!nextAssets.has(assetId)) removedAssets.push([assetId, record]);
|
|
1983
|
+
if (removedAssets.length > 0) {
|
|
1984
|
+
const now = Date.now();
|
|
1985
|
+
for (const [assetId, record] of removedAssets) {
|
|
1986
|
+
const deletedAt = this.orphanedAssets.get(assetId)?.deletedAt ?? now;
|
|
1987
|
+
this.orphanedAssets.set(assetId, {
|
|
1988
|
+
metadata: record.metadata,
|
|
1989
|
+
deletedAt
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
this.docAssets.clear();
|
|
1994
|
+
for (const [docId, assets] of nextDocAssets) this.docAssets.set(docId, assets);
|
|
1995
|
+
this.assetToDocRefs.clear();
|
|
1996
|
+
for (const [docId, assets] of nextDocAssets) for (const assetId of assets.keys()) {
|
|
1997
|
+
const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
|
|
1998
|
+
refs.add(docId);
|
|
1999
|
+
this.assetToDocRefs.set(assetId, refs);
|
|
2000
|
+
}
|
|
2001
|
+
this.assets.clear();
|
|
2002
|
+
for (const record of nextAssets.values()) this.rememberAsset(record.metadata, record.data);
|
|
2003
|
+
for (const [assetId, record] of nextAssets) {
|
|
2004
|
+
const previous = prevAssets.get(assetId)?.metadata;
|
|
2005
|
+
if (!assetMetadataEqual(previous, record.metadata)) this.eventBus.emit({
|
|
2006
|
+
kind: "asset-metadata",
|
|
2007
|
+
asset: this.createAssetDownload(assetId, record.metadata, record.data),
|
|
2008
|
+
by
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
for (const [docId, assets] of nextDocAssets) {
|
|
2012
|
+
const previous = prevDocAssets.get(docId);
|
|
2013
|
+
for (const assetId of assets.keys()) if (!previous || !previous.has(assetId)) this.eventBus.emit({
|
|
2014
|
+
kind: "asset-link",
|
|
2015
|
+
docId,
|
|
2016
|
+
assetId,
|
|
2017
|
+
by
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
for (const [docId, assets] of prevDocAssets) {
|
|
2021
|
+
const current = nextDocAssets.get(docId);
|
|
2022
|
+
for (const assetId of assets.keys()) if (!current || !current.has(assetId)) this.eventBus.emit({
|
|
2023
|
+
kind: "asset-unlink",
|
|
2024
|
+
docId,
|
|
2025
|
+
assetId,
|
|
2026
|
+
by
|
|
2027
|
+
});
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
clear() {
|
|
2031
|
+
this.docAssets.clear();
|
|
2032
|
+
this.assets.clear();
|
|
2033
|
+
this.orphanedAssets.clear();
|
|
2034
|
+
this.assetToDocRefs.clear();
|
|
2035
|
+
}
|
|
2036
|
+
readDocAssetsFromFlock(docId) {
|
|
2037
|
+
const rows = this.metaFlock.scan({ prefix: ["ld", docId] });
|
|
2038
|
+
const mapping = /* @__PURE__ */ new Map();
|
|
2039
|
+
for (const row of rows) {
|
|
2040
|
+
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
2041
|
+
const assetId = row.key[2];
|
|
2042
|
+
if (typeof assetId !== "string") continue;
|
|
2043
|
+
if (!(row.value !== void 0 && row.value !== null && row.value !== false)) continue;
|
|
2044
|
+
let metadata = this.assets.get(assetId)?.metadata;
|
|
2045
|
+
if (!metadata) {
|
|
2046
|
+
metadata = this.readAssetMetadataFromFlock(assetId);
|
|
2047
|
+
if (!metadata) continue;
|
|
2048
|
+
this.rememberAsset(metadata);
|
|
2049
|
+
}
|
|
2050
|
+
mapping.set(assetId, cloneRepoAssetMetadata(metadata));
|
|
2051
|
+
}
|
|
2052
|
+
return mapping;
|
|
2053
|
+
}
|
|
2054
|
+
readAssetMetadataFromFlock(assetId) {
|
|
2055
|
+
return assetMetaFromJson(this.metaFlock.get(["a", assetId]));
|
|
2056
|
+
}
|
|
2057
|
+
handleAssetRemoval(assetId, by) {
|
|
2058
|
+
const record = this.assets.get(assetId);
|
|
2059
|
+
if (!record) return;
|
|
2060
|
+
this.assets.delete(assetId);
|
|
2061
|
+
this.markAssetAsOrphan(assetId, record.metadata);
|
|
2062
|
+
const refs = this.assetToDocRefs.get(assetId);
|
|
2063
|
+
if (refs) {
|
|
2064
|
+
this.assetToDocRefs.delete(assetId);
|
|
2065
|
+
for (const docId of refs) {
|
|
2066
|
+
const assets = this.docAssets.get(docId);
|
|
2067
|
+
if (assets?.delete(assetId) && assets.size === 0) this.docAssets.delete(docId);
|
|
2068
|
+
this.eventBus.emit({
|
|
2069
|
+
kind: "asset-unlink",
|
|
2070
|
+
docId,
|
|
2071
|
+
assetId,
|
|
2072
|
+
by
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
for (const [docId, assets] of this.docAssets) if (assets.delete(assetId)) {
|
|
2078
|
+
if (assets.size === 0) this.docAssets.delete(docId);
|
|
2079
|
+
this.eventBus.emit({
|
|
2080
|
+
kind: "asset-unlink",
|
|
2081
|
+
docId,
|
|
2082
|
+
assetId,
|
|
2083
|
+
by
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
createAssetDownload(assetId, metadata, initialBytes) {
|
|
2088
|
+
let cached = initialBytes ? initialBytes.slice() : void 0;
|
|
2089
|
+
return {
|
|
2090
|
+
assetId,
|
|
2091
|
+
size: metadata.size,
|
|
2092
|
+
createdAt: metadata.createdAt,
|
|
2093
|
+
mime: metadata.mime,
|
|
2094
|
+
policy: metadata.policy,
|
|
2095
|
+
tag: metadata.tag,
|
|
2096
|
+
content: async () => {
|
|
2097
|
+
if (!cached) cached = (await this.materializeAsset(assetId)).bytes.slice();
|
|
2098
|
+
return toReadableStream(cached.slice());
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
async materializeAsset(assetId) {
|
|
2103
|
+
let record = this.assets.get(assetId);
|
|
2104
|
+
if (record?.data) return {
|
|
2105
|
+
metadata: record.metadata,
|
|
2106
|
+
bytes: record.data.slice()
|
|
2107
|
+
};
|
|
2108
|
+
if (record && this.storage) {
|
|
2109
|
+
const stored = await this.storage.loadAsset(assetId);
|
|
2110
|
+
if (stored) {
|
|
2111
|
+
const clone = stored.slice();
|
|
2112
|
+
record.data = clone.slice();
|
|
2113
|
+
return {
|
|
2114
|
+
metadata: record.metadata,
|
|
2115
|
+
bytes: clone
|
|
2116
|
+
};
|
|
1666
2117
|
}
|
|
1667
2118
|
}
|
|
1668
2119
|
if (!record && this.storage) {
|
|
@@ -1712,147 +2163,77 @@ var LoroRepo = class {
|
|
|
1712
2163
|
};
|
|
1713
2164
|
}
|
|
1714
2165
|
updateDocAssetMetadata(assetId, metadata) {
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
const now = Date.now();
|
|
1721
|
-
let removed = 0;
|
|
1722
|
-
for (const [assetId, orphan] of Array.from(this.orphanedAssets.entries())) {
|
|
1723
|
-
if (now - orphan.deletedAt < minKeepMs) continue;
|
|
1724
|
-
this.orphanedAssets.delete(assetId);
|
|
1725
|
-
if (this.storage?.deleteAsset) try {
|
|
1726
|
-
await this.storage.deleteAsset(assetId);
|
|
1727
|
-
} catch (error) {
|
|
1728
|
-
logAsyncError(`asset ${assetId} delete`)(error);
|
|
1729
|
-
}
|
|
1730
|
-
removed += 1;
|
|
2166
|
+
const refs = this.assetToDocRefs.get(assetId);
|
|
2167
|
+
if (!refs) return;
|
|
2168
|
+
for (const docId of refs) {
|
|
2169
|
+
const assets = this.docAssets.get(docId);
|
|
2170
|
+
if (assets) assets.set(assetId, metadata);
|
|
1731
2171
|
}
|
|
1732
|
-
return removed;
|
|
1733
2172
|
}
|
|
1734
|
-
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
2173
|
+
rememberAsset(metadata, bytes) {
|
|
2174
|
+
const data = bytes ? bytes.slice() : this.assets.get(metadata.assetId)?.data;
|
|
2175
|
+
this.assets.set(metadata.assetId, {
|
|
2176
|
+
metadata,
|
|
2177
|
+
data
|
|
2178
|
+
});
|
|
2179
|
+
this.orphanedAssets.delete(metadata.assetId);
|
|
1740
2180
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
return stored;
|
|
1754
|
-
}
|
|
2181
|
+
addDocReference(assetId, docId) {
|
|
2182
|
+
const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
|
|
2183
|
+
refs.add(docId);
|
|
2184
|
+
this.assetToDocRefs.set(assetId, refs);
|
|
2185
|
+
}
|
|
2186
|
+
removeDocAssetReference(assetId, docId) {
|
|
2187
|
+
const refs = this.assetToDocRefs.get(assetId);
|
|
2188
|
+
if (!refs) return;
|
|
2189
|
+
refs.delete(docId);
|
|
2190
|
+
if (refs.size === 0) {
|
|
2191
|
+
this.assetToDocRefs.delete(assetId);
|
|
2192
|
+
this.markAssetAsOrphan(assetId);
|
|
1755
2193
|
}
|
|
1756
|
-
const created = await this.docFactory(docId);
|
|
1757
|
-
this.registerDoc(docId, created);
|
|
1758
|
-
return created;
|
|
1759
2194
|
}
|
|
1760
|
-
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
const cached = this.docs.get(docId);
|
|
1768
|
-
if (cached) return cached.export({ mode: "snapshot" });
|
|
1769
|
-
if (!this.storage) return;
|
|
1770
|
-
return (await this.storage.loadDoc(docId))?.export({ mode: "snapshot" });
|
|
1771
|
-
}
|
|
1772
|
-
async persistMeta() {
|
|
1773
|
-
if (!this.storage) return;
|
|
1774
|
-
const bundle = this.metaFlock.exportJson();
|
|
1775
|
-
const encoded = textEncoder.encode(JSON.stringify(bundle));
|
|
1776
|
-
await this.storage.save({
|
|
1777
|
-
type: "meta",
|
|
1778
|
-
update: encoded
|
|
2195
|
+
markAssetAsOrphan(assetId, metadataOverride) {
|
|
2196
|
+
const metadata = metadataOverride ?? this.assets.get(assetId)?.metadata;
|
|
2197
|
+
if (!metadata) return;
|
|
2198
|
+
const deletedAt = this.orphanedAssets.get(assetId)?.deletedAt ?? Date.now();
|
|
2199
|
+
this.orphanedAssets.set(assetId, {
|
|
2200
|
+
metadata,
|
|
2201
|
+
deletedAt
|
|
1779
2202
|
});
|
|
1780
2203
|
}
|
|
1781
|
-
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
const snapshot = doc.export({ mode: "snapshot" });
|
|
1789
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1790
|
-
try {
|
|
1791
|
-
await this.storage.save({
|
|
1792
|
-
type: "doc-snapshot",
|
|
1793
|
-
docId,
|
|
1794
|
-
snapshot
|
|
1795
|
-
});
|
|
1796
|
-
} catch (error) {
|
|
1797
|
-
if (previousVersion) this.docPersistedVersions.set(docId, previousVersion);
|
|
1798
|
-
else this.docPersistedVersions.delete(docId);
|
|
1799
|
-
throw error;
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
async persistDocUpdate(docId, doc) {
|
|
1803
|
-
const previousVersion = this.docPersistedVersions.get(docId);
|
|
1804
|
-
const nextVersion = doc.version();
|
|
1805
|
-
if (!this.storage) {
|
|
1806
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1807
|
-
return;
|
|
1808
|
-
}
|
|
1809
|
-
if (!previousVersion) {
|
|
1810
|
-
await this.persistDoc(docId, doc);
|
|
1811
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1812
|
-
return;
|
|
1813
|
-
}
|
|
1814
|
-
const update = doc.export({
|
|
1815
|
-
mode: "update",
|
|
1816
|
-
from: previousVersion
|
|
1817
|
-
});
|
|
1818
|
-
if (!update.length) {
|
|
1819
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1823
|
-
try {
|
|
1824
|
-
await this.storage.save({
|
|
1825
|
-
type: "doc-update",
|
|
1826
|
-
docId,
|
|
1827
|
-
update
|
|
1828
|
-
});
|
|
1829
|
-
} catch (error) {
|
|
1830
|
-
this.docPersistedVersions.set(docId, previousVersion);
|
|
1831
|
-
throw error;
|
|
2204
|
+
getAssetMetadata(assetId) {
|
|
2205
|
+
const record = this.assets.get(assetId);
|
|
2206
|
+
if (record) return record.metadata;
|
|
2207
|
+
for (const assets of this.docAssets.values()) {
|
|
2208
|
+
const metadata = assets.get(assetId);
|
|
2209
|
+
if (metadata) return metadata;
|
|
1832
2210
|
}
|
|
1833
2211
|
}
|
|
1834
|
-
|
|
1835
|
-
this.
|
|
1836
|
-
}
|
|
1837
|
-
popEventBy() {
|
|
1838
|
-
this.eventByStack.pop();
|
|
1839
|
-
}
|
|
1840
|
-
resolveEventBy(defaultBy) {
|
|
1841
|
-
const index = this.eventByStack.length - 1;
|
|
1842
|
-
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
1843
|
-
}
|
|
1844
|
-
ensureMetaLiveMonitor() {
|
|
1845
|
-
if (this.unsubscribeMetaFlock) return;
|
|
1846
|
-
this.unsubscribeMetaFlock = this.metaFlock.subscribe((batch) => {
|
|
1847
|
-
if (batch.source === "local") return;
|
|
1848
|
-
const by = this.resolveEventBy("live");
|
|
1849
|
-
(async () => {
|
|
1850
|
-
this.applyMetaFlockEvents(batch.events, by);
|
|
1851
|
-
await this.persistMeta();
|
|
1852
|
-
})().catch(logAsyncError("meta live monitor sync"));
|
|
1853
|
-
});
|
|
2212
|
+
get metaFlock() {
|
|
2213
|
+
return this.getMetaFlock();
|
|
1854
2214
|
}
|
|
1855
|
-
|
|
2215
|
+
};
|
|
2216
|
+
|
|
2217
|
+
//#endregion
|
|
2218
|
+
//#region src/internal/flock-hydrator.ts
|
|
2219
|
+
var FlockHydrator = class {
|
|
2220
|
+
getMetaFlock;
|
|
2221
|
+
metadataManager;
|
|
2222
|
+
assetManager;
|
|
2223
|
+
docManager;
|
|
2224
|
+
constructor(options) {
|
|
2225
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
2226
|
+
this.metadataManager = options.metadataManager;
|
|
2227
|
+
this.assetManager = options.assetManager;
|
|
2228
|
+
this.docManager = options.docManager;
|
|
2229
|
+
}
|
|
2230
|
+
hydrateAll(by) {
|
|
2231
|
+
const nextMetadata = this.readAllDocMetadata();
|
|
2232
|
+
this.metadataManager.replaceAll(nextMetadata, by);
|
|
2233
|
+
this.assetManager.hydrateFromFlock(by);
|
|
2234
|
+
this.docManager.hydrateFrontierKeys();
|
|
2235
|
+
}
|
|
2236
|
+
applyEvents(events, by) {
|
|
1856
2237
|
if (!events.length) return;
|
|
1857
2238
|
const docMetadataIds = /* @__PURE__ */ new Set();
|
|
1858
2239
|
const docAssetIds = /* @__PURE__ */ new Set();
|
|
@@ -1878,134 +2259,12 @@ var LoroRepo = class {
|
|
|
1878
2259
|
if (typeof docId === "string") docFrontiersIds.add(docId);
|
|
1879
2260
|
}
|
|
1880
2261
|
}
|
|
1881
|
-
for (const assetId of assetIds) this.refreshAssetMetadataEntry(assetId, by);
|
|
1882
|
-
for (const docId of docMetadataIds) this.
|
|
1883
|
-
for (const docId of docAssetIds) this.refreshDocAssetsEntry(docId, by);
|
|
1884
|
-
for (const docId of docFrontiersIds) this.refreshDocFrontierKeys(docId);
|
|
1885
|
-
}
|
|
1886
|
-
registerDoc(docId, doc) {
|
|
1887
|
-
this.docs.set(docId, doc);
|
|
1888
|
-
this.docPersistedVersions.set(docId, doc.version());
|
|
1889
|
-
this.ensureDocSubscription(docId, doc);
|
|
1890
|
-
}
|
|
1891
|
-
ensureDocSubscription(docId, doc) {
|
|
1892
|
-
if (this.docSubscriptions.has(docId)) return;
|
|
1893
|
-
const unsubscribe = doc.subscribe((batch) => {
|
|
1894
|
-
const stackBy = this.resolveEventBy("local");
|
|
1895
|
-
const by = stackBy === "local" && batch.by === "import" ? "live" : stackBy;
|
|
1896
|
-
this.onDocEvent(docId, doc, batch, by);
|
|
1897
|
-
});
|
|
1898
|
-
if (typeof unsubscribe === "function") this.docSubscriptions.set(docId, unsubscribe);
|
|
1899
|
-
}
|
|
1900
|
-
rememberAsset(metadata, bytes) {
|
|
1901
|
-
const data = bytes ? bytes.slice() : this.assets.get(metadata.assetId)?.data;
|
|
1902
|
-
this.assets.set(metadata.assetId, {
|
|
1903
|
-
metadata,
|
|
1904
|
-
data
|
|
1905
|
-
});
|
|
1906
|
-
this.orphanedAssets.delete(metadata.assetId);
|
|
1907
|
-
}
|
|
1908
|
-
scheduleDocFrontierUpdate(docId, doc, by) {
|
|
1909
|
-
const existing = this.docFrontierUpdates.get(docId);
|
|
1910
|
-
const effectiveBy = existing ? this.mergeRepoEventBy(existing.by, by) : by;
|
|
1911
|
-
if (existing) clearTimeout(existing.timeout);
|
|
1912
|
-
const delay = this.docFrontierDebounceMs > 0 ? this.docFrontierDebounceMs : 0;
|
|
1913
|
-
const timeout = setTimeout(() => this.runScheduledDocFrontierUpdate(docId), delay);
|
|
1914
|
-
this.docFrontierUpdates.set(docId, {
|
|
1915
|
-
timeout,
|
|
1916
|
-
doc,
|
|
1917
|
-
by: effectiveBy
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
mergeRepoEventBy(current, next) {
|
|
1921
|
-
if (current === next) return current;
|
|
1922
|
-
if (current === "live" || next === "live") return "live";
|
|
1923
|
-
if (current === "sync" || next === "sync") return "sync";
|
|
1924
|
-
return "local";
|
|
1925
|
-
}
|
|
1926
|
-
runScheduledDocFrontierUpdate(docId) {
|
|
1927
|
-
const pending = this.docFrontierUpdates.get(docId);
|
|
1928
|
-
if (!pending) return;
|
|
1929
|
-
this.docFrontierUpdates.delete(docId);
|
|
1930
|
-
this.pushEventBy(pending.by);
|
|
1931
|
-
(async () => {
|
|
1932
|
-
try {
|
|
1933
|
-
await this.updateDocFrontiers(docId, pending.doc, pending.by);
|
|
1934
|
-
} finally {
|
|
1935
|
-
this.popEventBy();
|
|
1936
|
-
}
|
|
1937
|
-
})().catch(logAsyncError(`doc ${docId} frontier debounce`));
|
|
1938
|
-
}
|
|
1939
|
-
async flushScheduledDocFrontierUpdate(docId) {
|
|
1940
|
-
const pending = this.docFrontierUpdates.get(docId);
|
|
1941
|
-
if (!pending) return false;
|
|
1942
|
-
clearTimeout(pending.timeout);
|
|
1943
|
-
this.docFrontierUpdates.delete(docId);
|
|
1944
|
-
this.pushEventBy(pending.by);
|
|
1945
|
-
try {
|
|
1946
|
-
await this.updateDocFrontiers(docId, pending.doc, pending.by);
|
|
1947
|
-
} finally {
|
|
1948
|
-
this.popEventBy();
|
|
1949
|
-
}
|
|
1950
|
-
return true;
|
|
1951
|
-
}
|
|
1952
|
-
onDocEvent(docId, doc, _batch, by) {
|
|
1953
|
-
(async () => {
|
|
1954
|
-
const a = this.persistDocUpdate(docId, doc);
|
|
1955
|
-
if (by === "local") {
|
|
1956
|
-
this.scheduleDocFrontierUpdate(docId, doc, by);
|
|
1957
|
-
await a;
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
const b = this.flushScheduledDocFrontierUpdate(docId);
|
|
1961
|
-
const c = this.updateDocFrontiers(docId, doc, by);
|
|
1962
|
-
await Promise.all([
|
|
1963
|
-
a,
|
|
1964
|
-
b,
|
|
1965
|
-
c
|
|
1966
|
-
]);
|
|
1967
|
-
})().catch(logAsyncError(`doc ${docId} event processing`));
|
|
1968
|
-
}
|
|
1969
|
-
getAssetMetadata(assetId) {
|
|
1970
|
-
const record = this.assets.get(assetId);
|
|
1971
|
-
if (record) return record.metadata;
|
|
1972
|
-
for (const assets of this.docAssets.values()) {
|
|
1973
|
-
const metadata = assets.get(assetId);
|
|
1974
|
-
if (metadata) return metadata;
|
|
1975
|
-
}
|
|
1976
|
-
}
|
|
1977
|
-
async updateDocFrontiers(docId, doc, defaultBy = "local") {
|
|
1978
|
-
const { json, key } = canonicalizeVersionVector(computeVersionVector(doc));
|
|
1979
|
-
const existingKeys = this.docFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1980
|
-
let mutated = false;
|
|
1981
|
-
if (existingKeys.size !== 1 || !existingKeys.has(key)) {
|
|
1982
|
-
for (const entry of existingKeys) this.metaFlock.delete([
|
|
1983
|
-
"f",
|
|
1984
|
-
docId,
|
|
1985
|
-
entry
|
|
1986
|
-
]);
|
|
1987
|
-
this.metaFlock.put([
|
|
1988
|
-
"f",
|
|
1989
|
-
docId,
|
|
1990
|
-
key
|
|
1991
|
-
], json);
|
|
1992
|
-
this.docFrontierKeys.set(docId, new Set([key]));
|
|
1993
|
-
mutated = true;
|
|
1994
|
-
}
|
|
1995
|
-
if (mutated) await this.persistMeta();
|
|
1996
|
-
const by = this.resolveEventBy(defaultBy);
|
|
1997
|
-
const frontiers = getDocFrontiers(doc);
|
|
1998
|
-
this.emit({
|
|
1999
|
-
kind: "doc-frontiers",
|
|
2000
|
-
docId,
|
|
2001
|
-
frontiers,
|
|
2002
|
-
by
|
|
2003
|
-
});
|
|
2262
|
+
for (const assetId of assetIds) this.assetManager.refreshAssetMetadataEntry(assetId, by);
|
|
2263
|
+
for (const docId of docMetadataIds) this.metadataManager.refreshFromFlock(docId, by);
|
|
2264
|
+
for (const docId of docAssetIds) this.assetManager.refreshDocAssetsEntry(docId, by);
|
|
2265
|
+
for (const docId of docFrontiersIds) this.docManager.refreshDocFrontierKeys(docId);
|
|
2004
2266
|
}
|
|
2005
|
-
|
|
2006
|
-
const prevMetadata = new Map(this.metadata);
|
|
2007
|
-
const prevDocAssets = new Map(this.docAssets);
|
|
2008
|
-
const prevAssets = new Map(this.assets);
|
|
2267
|
+
readAllDocMetadata() {
|
|
2009
2268
|
const nextMetadata = /* @__PURE__ */ new Map();
|
|
2010
2269
|
const metadataRows = this.metaFlock.scan({ prefix: ["m"] });
|
|
2011
2270
|
for (const row of metadataRows) {
|
|
@@ -2036,135 +2295,337 @@ var LoroRepo = class {
|
|
|
2036
2295
|
if (jsonValue === void 0) continue;
|
|
2037
2296
|
docMeta[fieldKey] = jsonValue;
|
|
2038
2297
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2298
|
+
return nextMetadata;
|
|
2299
|
+
}
|
|
2300
|
+
get metaFlock() {
|
|
2301
|
+
return this.getMetaFlock();
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
|
|
2305
|
+
//#endregion
|
|
2306
|
+
//#region src/internal/sync-runner.ts
|
|
2307
|
+
var SyncRunner = class {
|
|
2308
|
+
storage;
|
|
2309
|
+
transport;
|
|
2310
|
+
eventBus;
|
|
2311
|
+
docManager;
|
|
2312
|
+
metadataManager;
|
|
2313
|
+
assetManager;
|
|
2314
|
+
flockHydrator;
|
|
2315
|
+
getMetaFlock;
|
|
2316
|
+
replaceMetaFlock;
|
|
2317
|
+
persistMeta;
|
|
2318
|
+
readyPromise;
|
|
2319
|
+
metaRoomSubscription;
|
|
2320
|
+
unsubscribeMetaFlock;
|
|
2321
|
+
constructor(options) {
|
|
2322
|
+
this.storage = options.storage;
|
|
2323
|
+
this.transport = options.transport;
|
|
2324
|
+
this.eventBus = options.eventBus;
|
|
2325
|
+
this.docManager = options.docManager;
|
|
2326
|
+
this.metadataManager = options.metadataManager;
|
|
2327
|
+
this.assetManager = options.assetManager;
|
|
2328
|
+
this.flockHydrator = options.flockHydrator;
|
|
2329
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
2330
|
+
this.replaceMetaFlock = options.replaceMetaFlock;
|
|
2331
|
+
this.persistMeta = options.persistMeta;
|
|
2332
|
+
}
|
|
2333
|
+
async ready() {
|
|
2334
|
+
if (!this.readyPromise) this.readyPromise = this.initialize();
|
|
2335
|
+
await this.readyPromise;
|
|
2336
|
+
}
|
|
2337
|
+
async sync(options = {}) {
|
|
2338
|
+
await this.ready();
|
|
2339
|
+
const { scope = "full", docIds } = options;
|
|
2340
|
+
if (!this.transport) return;
|
|
2341
|
+
if (!this.transport.isConnected()) await this.transport.connect();
|
|
2342
|
+
if (scope === "meta" || scope === "full") {
|
|
2343
|
+
this.eventBus.pushEventBy("sync");
|
|
2344
|
+
const recordedEvents = [];
|
|
2345
|
+
const unsubscribe = this.metaFlock.subscribe((batch) => {
|
|
2346
|
+
if (batch.source === "local") return;
|
|
2347
|
+
recordedEvents.push(...batch.events);
|
|
2051
2348
|
});
|
|
2349
|
+
try {
|
|
2350
|
+
if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
|
|
2351
|
+
if (recordedEvents.length > 0) this.flockHydrator.applyEvents(recordedEvents, "sync");
|
|
2352
|
+
else this.flockHydrator.hydrateAll("sync");
|
|
2353
|
+
await this.persistMeta();
|
|
2354
|
+
} finally {
|
|
2355
|
+
unsubscribe();
|
|
2356
|
+
this.eventBus.popEventBy();
|
|
2357
|
+
}
|
|
2052
2358
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
}
|
|
2066
|
-
const nextFrontierKeys = /* @__PURE__ */ new Map();
|
|
2067
|
-
const frontierRows = this.metaFlock.scan({ prefix: ["f"] });
|
|
2068
|
-
for (const row of frontierRows) {
|
|
2069
|
-
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
2070
|
-
const docId = row.key[1];
|
|
2071
|
-
const frontierKey = row.key[2];
|
|
2072
|
-
if (typeof docId !== "string" || typeof frontierKey !== "string") continue;
|
|
2073
|
-
const set = nextFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
2074
|
-
set.add(frontierKey);
|
|
2075
|
-
nextFrontierKeys.set(docId, set);
|
|
2076
|
-
}
|
|
2077
|
-
const removedAssets = [];
|
|
2078
|
-
for (const [assetId, record] of prevAssets) if (!nextAssets.has(assetId)) removedAssets.push([assetId, record]);
|
|
2079
|
-
if (removedAssets.length > 0) {
|
|
2080
|
-
const now = Date.now();
|
|
2081
|
-
for (const [assetId, record] of removedAssets) {
|
|
2082
|
-
const deletedAt = this.orphanedAssets.get(assetId)?.deletedAt ?? now;
|
|
2083
|
-
this.orphanedAssets.set(assetId, {
|
|
2084
|
-
metadata: record.metadata,
|
|
2085
|
-
deletedAt
|
|
2086
|
-
});
|
|
2359
|
+
if (scope === "doc" || scope === "full") {
|
|
2360
|
+
const targets = docIds ?? this.metadataManager.getDocIds();
|
|
2361
|
+
for (const docId of targets) {
|
|
2362
|
+
const doc = await this.docManager.ensureDoc(docId);
|
|
2363
|
+
this.eventBus.pushEventBy("sync");
|
|
2364
|
+
try {
|
|
2365
|
+
if (!(await this.transport.syncDoc(docId, doc)).ok) throw new Error(`Document sync failed for ${docId}`);
|
|
2366
|
+
} finally {
|
|
2367
|
+
this.eventBus.popEventBy();
|
|
2368
|
+
}
|
|
2369
|
+
await this.docManager.persistDoc(docId, doc);
|
|
2370
|
+
await this.docManager.updateDocFrontiers(docId, doc, "sync");
|
|
2087
2371
|
}
|
|
2088
2372
|
}
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
this.
|
|
2092
|
-
|
|
2093
|
-
this.
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
kind: "doc-metadata",
|
|
2110
|
-
docId,
|
|
2111
|
-
patch: {},
|
|
2112
|
-
by
|
|
2113
|
-
});
|
|
2114
|
-
continue;
|
|
2373
|
+
}
|
|
2374
|
+
async joinMetaRoom(params) {
|
|
2375
|
+
await this.ready();
|
|
2376
|
+
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
2377
|
+
if (!this.transport.isConnected()) await this.transport.connect();
|
|
2378
|
+
if (this.metaRoomSubscription) return this.metaRoomSubscription;
|
|
2379
|
+
this.ensureMetaLiveMonitor();
|
|
2380
|
+
const subscription = this.transport.joinMetaRoom(this.metaFlock, params);
|
|
2381
|
+
const wrapped = {
|
|
2382
|
+
unsubscribe: () => {
|
|
2383
|
+
subscription.unsubscribe();
|
|
2384
|
+
if (this.metaRoomSubscription === wrapped) this.metaRoomSubscription = void 0;
|
|
2385
|
+
if (this.unsubscribeMetaFlock) {
|
|
2386
|
+
this.unsubscribeMetaFlock();
|
|
2387
|
+
this.unsubscribeMetaFlock = void 0;
|
|
2388
|
+
}
|
|
2389
|
+
},
|
|
2390
|
+
firstSyncedWithRemote: subscription.firstSyncedWithRemote,
|
|
2391
|
+
get connected() {
|
|
2392
|
+
return subscription.connected;
|
|
2115
2393
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
for (const [docId, assets] of prevDocAssets) {
|
|
2142
|
-
const current = nextDocAssets.get(docId);
|
|
2143
|
-
for (const assetId of assets.keys()) if (!current || !current.has(assetId)) this.emit({
|
|
2144
|
-
kind: "asset-unlink",
|
|
2145
|
-
docId,
|
|
2146
|
-
assetId,
|
|
2147
|
-
by
|
|
2148
|
-
});
|
|
2394
|
+
};
|
|
2395
|
+
this.metaRoomSubscription = wrapped;
|
|
2396
|
+
subscription.firstSyncedWithRemote.then(async () => {
|
|
2397
|
+
const by = this.eventBus.resolveEventBy("live");
|
|
2398
|
+
this.flockHydrator.hydrateAll(by);
|
|
2399
|
+
await this.persistMeta();
|
|
2400
|
+
}).catch(logAsyncError("meta room first sync"));
|
|
2401
|
+
return wrapped;
|
|
2402
|
+
}
|
|
2403
|
+
async joinDocRoom(docId, params) {
|
|
2404
|
+
await this.ready();
|
|
2405
|
+
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
2406
|
+
if (!this.transport.isConnected()) await this.transport.connect();
|
|
2407
|
+
const doc = await this.docManager.ensureDoc(docId);
|
|
2408
|
+
const subscription = this.transport.joinDocRoom(docId, doc, params);
|
|
2409
|
+
subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
2410
|
+
return subscription;
|
|
2411
|
+
}
|
|
2412
|
+
async close() {
|
|
2413
|
+
await this.docManager.close();
|
|
2414
|
+
this.metaRoomSubscription?.unsubscribe();
|
|
2415
|
+
this.metaRoomSubscription = void 0;
|
|
2416
|
+
if (this.unsubscribeMetaFlock) {
|
|
2417
|
+
this.unsubscribeMetaFlock();
|
|
2418
|
+
this.unsubscribeMetaFlock = void 0;
|
|
2149
2419
|
}
|
|
2420
|
+
this.eventBus.clear();
|
|
2421
|
+
this.metadataManager.clear();
|
|
2422
|
+
this.assetManager.clear();
|
|
2423
|
+
this.readyPromise = void 0;
|
|
2424
|
+
await this.transport?.close();
|
|
2150
2425
|
}
|
|
2151
|
-
|
|
2152
|
-
if (
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
const docId = (() => {
|
|
2156
|
-
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
2157
|
-
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
2158
|
-
})();
|
|
2159
|
-
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
2160
|
-
if (filter.docIds && !docId) return false;
|
|
2161
|
-
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
2162
|
-
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
2426
|
+
async initialize() {
|
|
2427
|
+
if (this.storage) {
|
|
2428
|
+
const snapshot = await this.storage.loadMeta();
|
|
2429
|
+
if (snapshot) this.replaceMetaFlock(snapshot);
|
|
2163
2430
|
}
|
|
2164
|
-
|
|
2431
|
+
this.flockHydrator.hydrateAll("sync");
|
|
2432
|
+
}
|
|
2433
|
+
ensureMetaLiveMonitor() {
|
|
2434
|
+
if (this.unsubscribeMetaFlock) return;
|
|
2435
|
+
this.unsubscribeMetaFlock = this.metaFlock.subscribe((batch) => {
|
|
2436
|
+
if (batch.source === "local") return;
|
|
2437
|
+
const by = this.eventBus.resolveEventBy("live");
|
|
2438
|
+
(async () => {
|
|
2439
|
+
this.flockHydrator.applyEvents(batch.events, by);
|
|
2440
|
+
await this.persistMeta();
|
|
2441
|
+
})().catch(logAsyncError("meta live monitor sync"));
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
get metaFlock() {
|
|
2445
|
+
return this.getMetaFlock();
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
|
|
2449
|
+
//#endregion
|
|
2450
|
+
//#region src/internal/repo-state.ts
|
|
2451
|
+
function createRepoState() {
|
|
2452
|
+
return {
|
|
2453
|
+
metadata: /* @__PURE__ */ new Map(),
|
|
2454
|
+
docAssets: /* @__PURE__ */ new Map(),
|
|
2455
|
+
assets: /* @__PURE__ */ new Map(),
|
|
2456
|
+
orphanedAssets: /* @__PURE__ */ new Map(),
|
|
2457
|
+
assetToDocRefs: /* @__PURE__ */ new Map(),
|
|
2458
|
+
docFrontierKeys: /* @__PURE__ */ new Map()
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
//#endregion
|
|
2463
|
+
//#region src/index.ts
|
|
2464
|
+
const textEncoder = new TextEncoder();
|
|
2465
|
+
const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
|
|
2466
|
+
var LoroRepo = class {
|
|
2467
|
+
options;
|
|
2468
|
+
transport;
|
|
2469
|
+
storage;
|
|
2470
|
+
docFactory;
|
|
2471
|
+
metaFlock = new Flock();
|
|
2472
|
+
eventBus;
|
|
2473
|
+
docManager;
|
|
2474
|
+
metadataManager;
|
|
2475
|
+
assetManager;
|
|
2476
|
+
flockHydrator;
|
|
2477
|
+
state;
|
|
2478
|
+
syncRunner;
|
|
2479
|
+
constructor(options) {
|
|
2480
|
+
this.options = options;
|
|
2481
|
+
this.transport = options.transportAdapter;
|
|
2482
|
+
this.storage = options.storageAdapter;
|
|
2483
|
+
this.docFactory = options.docFactory ?? (async () => new LoroDoc());
|
|
2484
|
+
this.eventBus = new RepoEventBus();
|
|
2485
|
+
this.state = createRepoState();
|
|
2486
|
+
const configuredDebounce = options.docFrontierDebounceMs;
|
|
2487
|
+
const docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
|
|
2488
|
+
this.docManager = new DocManager({
|
|
2489
|
+
storage: this.storage,
|
|
2490
|
+
docFactory: this.docFactory,
|
|
2491
|
+
docFrontierDebounceMs,
|
|
2492
|
+
getMetaFlock: () => this.metaFlock,
|
|
2493
|
+
eventBus: this.eventBus,
|
|
2494
|
+
persistMeta: () => this.persistMeta(),
|
|
2495
|
+
state: this.state
|
|
2496
|
+
});
|
|
2497
|
+
this.metadataManager = new MetadataManager({
|
|
2498
|
+
getMetaFlock: () => this.metaFlock,
|
|
2499
|
+
eventBus: this.eventBus,
|
|
2500
|
+
persistMeta: () => this.persistMeta(),
|
|
2501
|
+
state: this.state
|
|
2502
|
+
});
|
|
2503
|
+
this.assetManager = new AssetManager({
|
|
2504
|
+
storage: this.storage,
|
|
2505
|
+
assetTransport: options.assetTransportAdapter,
|
|
2506
|
+
getMetaFlock: () => this.metaFlock,
|
|
2507
|
+
eventBus: this.eventBus,
|
|
2508
|
+
persistMeta: () => this.persistMeta(),
|
|
2509
|
+
state: this.state
|
|
2510
|
+
});
|
|
2511
|
+
this.flockHydrator = new FlockHydrator({
|
|
2512
|
+
getMetaFlock: () => this.metaFlock,
|
|
2513
|
+
metadataManager: this.metadataManager,
|
|
2514
|
+
assetManager: this.assetManager,
|
|
2515
|
+
docManager: this.docManager
|
|
2516
|
+
});
|
|
2517
|
+
this.syncRunner = new SyncRunner({
|
|
2518
|
+
storage: this.storage,
|
|
2519
|
+
transport: this.transport,
|
|
2520
|
+
eventBus: this.eventBus,
|
|
2521
|
+
docManager: this.docManager,
|
|
2522
|
+
metadataManager: this.metadataManager,
|
|
2523
|
+
assetManager: this.assetManager,
|
|
2524
|
+
flockHydrator: this.flockHydrator,
|
|
2525
|
+
getMetaFlock: () => this.metaFlock,
|
|
2526
|
+
replaceMetaFlock: (snapshot) => {
|
|
2527
|
+
this.metaFlock = snapshot;
|
|
2528
|
+
},
|
|
2529
|
+
persistMeta: () => this.persistMeta()
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
async ready() {
|
|
2533
|
+
await this.syncRunner.ready();
|
|
2534
|
+
}
|
|
2535
|
+
async sync(options = {}) {
|
|
2536
|
+
await this.syncRunner.sync(options);
|
|
2537
|
+
}
|
|
2538
|
+
async joinMetaRoom(params) {
|
|
2539
|
+
return this.syncRunner.joinMetaRoom(params);
|
|
2540
|
+
}
|
|
2541
|
+
async joinDocRoom(docId, params) {
|
|
2542
|
+
return this.syncRunner.joinDocRoom(docId, params);
|
|
2543
|
+
}
|
|
2544
|
+
async close() {
|
|
2545
|
+
await this.syncRunner.close();
|
|
2546
|
+
}
|
|
2547
|
+
async upsertDocMeta(docId, patch, _options = {}) {
|
|
2548
|
+
await this.ready();
|
|
2549
|
+
await this.metadataManager.upsert(docId, patch);
|
|
2550
|
+
}
|
|
2551
|
+
async getDocMeta(docId) {
|
|
2552
|
+
await this.ready();
|
|
2553
|
+
return this.metadataManager.get(docId);
|
|
2554
|
+
}
|
|
2555
|
+
async listDoc(query) {
|
|
2556
|
+
await this.ready();
|
|
2557
|
+
return this.metadataManager.list(query);
|
|
2558
|
+
}
|
|
2559
|
+
getMetaReplica() {
|
|
2560
|
+
return this.metaFlock;
|
|
2561
|
+
}
|
|
2562
|
+
watch(listener, filter = {}) {
|
|
2563
|
+
return this.eventBus.watch(listener, filter);
|
|
2564
|
+
}
|
|
2565
|
+
/**
|
|
2566
|
+
* Opens the repo-managed collaborative document, registers it for persistence,
|
|
2567
|
+
* and schedules a doc-level sync so `whenSyncedWithRemote` resolves after remote backfills.
|
|
2568
|
+
*/
|
|
2569
|
+
async openCollaborativeDoc(docId) {
|
|
2570
|
+
await this.ready();
|
|
2571
|
+
const whenSyncedWithRemote = this.whenDocInSyncWithRemote(docId);
|
|
2572
|
+
return this.docManager.openCollaborativeDoc(docId, whenSyncedWithRemote);
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Opens a detached `LoroDoc` snapshot that never registers with the repo, meaning
|
|
2576
|
+
* it neither participates in remote subscriptions nor persists edits back to storage.
|
|
2577
|
+
*/
|
|
2578
|
+
async openDetachedDoc(docId) {
|
|
2579
|
+
await this.ready();
|
|
2580
|
+
return this.docManager.openDetachedDoc(docId);
|
|
2581
|
+
}
|
|
2582
|
+
async uploadAsset(params) {
|
|
2583
|
+
await this.ready();
|
|
2584
|
+
return this.assetManager.uploadAsset(params);
|
|
2585
|
+
}
|
|
2586
|
+
async whenDocInSyncWithRemote(docId) {
|
|
2587
|
+
await this.ready();
|
|
2588
|
+
await this.docManager.ensureDoc(docId);
|
|
2589
|
+
await this.sync({
|
|
2590
|
+
scope: "doc",
|
|
2591
|
+
docIds: [docId]
|
|
2592
|
+
});
|
|
2593
|
+
}
|
|
2594
|
+
async linkAsset(docId, params) {
|
|
2595
|
+
await this.ready();
|
|
2596
|
+
return this.assetManager.linkAsset(docId, params);
|
|
2597
|
+
}
|
|
2598
|
+
async fetchAsset(assetId) {
|
|
2599
|
+
await this.ready();
|
|
2600
|
+
return this.assetManager.fetchAsset(assetId);
|
|
2601
|
+
}
|
|
2602
|
+
async unlinkAsset(docId, assetId) {
|
|
2603
|
+
await this.ready();
|
|
2604
|
+
await this.assetManager.unlinkAsset(docId, assetId);
|
|
2605
|
+
}
|
|
2606
|
+
async listAssets(docId) {
|
|
2607
|
+
await this.ready();
|
|
2608
|
+
return this.assetManager.listAssets(docId);
|
|
2609
|
+
}
|
|
2610
|
+
async ensureAsset(assetId) {
|
|
2611
|
+
await this.ready();
|
|
2612
|
+
return this.assetManager.ensureAsset(assetId);
|
|
2613
|
+
}
|
|
2614
|
+
async gcAssets(options = {}) {
|
|
2615
|
+
await this.ready();
|
|
2616
|
+
return this.assetManager.gcAssets(options);
|
|
2617
|
+
}
|
|
2618
|
+
async persistMeta() {
|
|
2619
|
+
if (!this.storage) return;
|
|
2620
|
+
const bundle = this.metaFlock.exportJson();
|
|
2621
|
+
const encoded = textEncoder.encode(JSON.stringify(bundle));
|
|
2622
|
+
await this.storage.save({
|
|
2623
|
+
type: "meta",
|
|
2624
|
+
update: encoded
|
|
2625
|
+
});
|
|
2165
2626
|
}
|
|
2166
2627
|
};
|
|
2167
2628
|
|
|
2168
2629
|
//#endregion
|
|
2169
|
-
export { BroadcastChannelTransportAdapter, IndexedDBStorageAdaptor, LoroRepo, WebSocketTransportAdapter };
|
|
2630
|
+
export { BroadcastChannelTransportAdapter, FileSystemStorageAdaptor, IndexedDBStorageAdaptor, LoroRepo, WebSocketTransportAdapter };
|
|
2170
2631
|
//# sourceMappingURL=index.js.map
|