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