loro-repo 0.1.0 → 0.3.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,16 +1,86 @@
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") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
1
28
  let __loro_dev_flock = require("@loro-dev/flock");
2
- let loro_crdt = require("loro-crdt");
3
29
  let loro_adaptors = require("loro-adaptors");
4
30
  let loro_protocol = require("loro-protocol");
5
31
  let loro_websocket = require("loro-websocket");
32
+ let loro_crdt = require("loro-crdt");
33
+ let node_fs = require("node:fs");
34
+ let node_path = require("node:path");
35
+ node_path = __toESM(node_path);
36
+ let node_crypto = require("node:crypto");
6
37
 
7
38
  //#region src/loro-adaptor.ts
8
39
  function createRepoFlockAdaptorFromDoc(flock, config = {}) {
9
40
  return new loro_adaptors.FlockAdaptor(flock, config);
10
41
  }
11
42
 
43
+ //#endregion
44
+ //#region src/internal/debug.ts
45
+ const getEnv = () => {
46
+ if (typeof globalThis !== "object" || globalThis === null) return;
47
+ return globalThis.process?.env;
48
+ };
49
+ const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
50
+ const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
51
+ const wildcardTokens = new Set([
52
+ "*",
53
+ "1",
54
+ "true",
55
+ "all"
56
+ ]);
57
+ const namespaceSet = new Set(normalizedNamespaces);
58
+ const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
59
+ const isDebugEnabled = (namespace) => {
60
+ if (!namespaceSet.size) return false;
61
+ if (!namespace) return hasWildcard;
62
+ const normalized = namespace.toLowerCase();
63
+ if (hasWildcard) return true;
64
+ if (namespaceSet.has(normalized)) return true;
65
+ const [root] = normalized.split(":");
66
+ return namespaceSet.has(root);
67
+ };
68
+ const createDebugLogger = (namespace) => {
69
+ const normalized = namespace.toLowerCase();
70
+ return (...args) => {
71
+ if (!isDebugEnabled(normalized)) return;
72
+ const prefix = `[loro-repo:${namespace}]`;
73
+ if (args.length === 0) {
74
+ console.info(prefix);
75
+ return;
76
+ }
77
+ console.info(prefix, ...args);
78
+ };
79
+ };
80
+
12
81
  //#endregion
13
82
  //#region src/transport/websocket.ts
83
+ const debug = createDebugLogger("transport:websocket");
14
84
  function withTimeout(promise, timeoutMs) {
15
85
  if (!timeoutMs || timeoutMs <= 0) return promise;
16
86
  return new Promise((resolve, reject) => {
@@ -55,30 +125,51 @@ var WebSocketTransportAdapter = class {
55
125
  }
56
126
  async connect(_options) {
57
127
  const client = this.ensureClient();
58
- await client.connect();
59
- await client.waitConnected();
128
+ debug("connect requested", { status: client.getStatus() });
129
+ try {
130
+ await client.connect();
131
+ debug("client.connect resolved");
132
+ await client.waitConnected();
133
+ debug("client.waitConnected resolved", { status: client.getStatus() });
134
+ } catch (error) {
135
+ debug("connect failed", error);
136
+ throw error;
137
+ }
60
138
  }
61
139
  async close() {
140
+ debug("close requested", {
141
+ docSessions: this.docSessions.size,
142
+ metadataSession: Boolean(this.metadataSession)
143
+ });
62
144
  for (const [docId] of this.docSessions) await this.leaveDocSession(docId).catch(() => {});
63
145
  this.docSessions.clear();
64
146
  await this.teardownMetadataSession().catch(() => {});
65
147
  if (this.client) {
66
- this.client.destroy();
148
+ const client = this.client;
67
149
  this.client = void 0;
150
+ client.destroy();
151
+ debug("websocket client destroyed");
68
152
  }
153
+ debug("close completed");
69
154
  }
70
155
  isConnected() {
71
156
  return this.client?.getStatus() === "connected";
72
157
  }
73
158
  async syncMeta(flock, options) {
74
- if (!this.options.metadataRoomId) return { ok: true };
159
+ if (!this.options.metadataRoomId) {
160
+ debug("syncMeta skipped; metadata room not configured");
161
+ return { ok: true };
162
+ }
163
+ debug("syncMeta requested", { roomId: this.options.metadataRoomId });
75
164
  try {
76
165
  await withTimeout((await this.ensureMetadataSession(flock, {
77
166
  roomId: this.options.metadataRoomId,
78
167
  auth: this.options.metadataAuth
79
168
  })).firstSynced, options?.timeout);
169
+ debug("syncMeta completed", { roomId: this.options.metadataRoomId });
80
170
  return { ok: true };
81
- } catch {
171
+ } catch (error) {
172
+ debug("syncMeta failed", error);
82
173
  return { ok: false };
83
174
  }
84
175
  }
@@ -87,6 +178,10 @@ var WebSocketTransportAdapter = class {
87
178
  const roomId = normalizeRoomId(params?.roomId, fallback);
88
179
  if (!roomId) throw new Error("Metadata room id not configured");
89
180
  const auth = params?.auth ?? this.options.metadataAuth;
181
+ debug("joinMetaRoom requested", {
182
+ roomId,
183
+ hasAuth: Boolean(auth && auth.length)
184
+ });
90
185
  const ensure = this.ensureMetadataSession(flock, {
91
186
  roomId,
92
187
  auth
@@ -97,7 +192,14 @@ var WebSocketTransportAdapter = class {
97
192
  unsubscribe: () => {
98
193
  ensure.then((session) => {
99
194
  session.refCount = Math.max(0, session.refCount - 1);
100
- if (session.refCount === 0) this.teardownMetadataSession(session).catch(() => {});
195
+ debug("metadata session refCount decremented", {
196
+ roomId: session.roomId,
197
+ refCount: session.refCount
198
+ });
199
+ if (session.refCount === 0) {
200
+ debug("tearing down metadata session due to refCount=0", { roomId: session.roomId });
201
+ this.teardownMetadataSession(session).catch(() => {});
202
+ }
101
203
  });
102
204
  },
103
205
  firstSyncedWithRemote: firstSynced,
@@ -107,18 +209,37 @@ var WebSocketTransportAdapter = class {
107
209
  };
108
210
  ensure.then((session) => {
109
211
  session.refCount += 1;
212
+ debug("metadata session refCount incremented", {
213
+ roomId: session.roomId,
214
+ refCount: session.refCount
215
+ });
110
216
  });
111
217
  return subscription;
112
218
  }
113
219
  async syncDoc(docId, doc, options) {
220
+ debug("syncDoc requested", { docId });
114
221
  try {
115
- await withTimeout((await this.ensureDocSession(docId, doc, {})).firstSynced, options?.timeout);
222
+ const session = await this.ensureDocSession(docId, doc, {});
223
+ await withTimeout(session.firstSynced, options?.timeout);
224
+ debug("syncDoc completed", {
225
+ docId,
226
+ roomId: session.roomId
227
+ });
116
228
  return { ok: true };
117
- } catch {
229
+ } catch (error) {
230
+ debug("syncDoc failed", {
231
+ docId,
232
+ error
233
+ });
118
234
  return { ok: false };
119
235
  }
120
236
  }
121
237
  joinDocRoom(docId, doc, params) {
238
+ debug("joinDocRoom requested", {
239
+ docId,
240
+ roomParamType: params?.roomId ? typeof params.roomId === "string" ? "string" : "uint8array" : void 0,
241
+ hasAuthOverride: Boolean(params?.auth && params.auth.length)
242
+ });
122
243
  const ensure = this.ensureDocSession(docId, doc, params ?? {});
123
244
  const firstSynced = ensure.then((session) => session.firstSynced);
124
245
  const getConnected = () => this.isConnected();
@@ -126,6 +247,11 @@ var WebSocketTransportAdapter = class {
126
247
  unsubscribe: () => {
127
248
  ensure.then((session) => {
128
249
  session.refCount = Math.max(0, session.refCount - 1);
250
+ debug("doc session refCount decremented", {
251
+ docId,
252
+ roomId: session.roomId,
253
+ refCount: session.refCount
254
+ });
129
255
  if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
130
256
  });
131
257
  },
@@ -136,12 +262,24 @@ var WebSocketTransportAdapter = class {
136
262
  };
137
263
  ensure.then((session) => {
138
264
  session.refCount += 1;
265
+ debug("doc session refCount incremented", {
266
+ docId,
267
+ roomId: session.roomId,
268
+ refCount: session.refCount
269
+ });
139
270
  });
140
271
  return subscription;
141
272
  }
142
273
  ensureClient() {
143
- if (this.client) return this.client;
274
+ if (this.client) {
275
+ debug("reusing websocket client", { status: this.client.getStatus() });
276
+ return this.client;
277
+ }
144
278
  const { url, client: clientOptions } = this.options;
279
+ debug("creating websocket client", {
280
+ url,
281
+ clientOptionsKeys: clientOptions ? Object.keys(clientOptions) : []
282
+ });
145
283
  const client = new loro_websocket.LoroWebsocketClient({
146
284
  url,
147
285
  ...clientOptions
@@ -150,22 +288,49 @@ var WebSocketTransportAdapter = class {
150
288
  return client;
151
289
  }
152
290
  async ensureMetadataSession(flock, params) {
291
+ debug("ensureMetadataSession invoked", {
292
+ roomId: params.roomId,
293
+ hasAuth: Boolean(params.auth && params.auth.length)
294
+ });
153
295
  const client = this.ensureClient();
154
296
  await client.waitConnected();
155
- if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) return this.metadataSession;
156
- if (this.metadataSession) await this.teardownMetadataSession(this.metadataSession).catch(() => {});
297
+ debug("websocket client ready for metadata session", { status: client.getStatus() });
298
+ if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) {
299
+ debug("reusing metadata session", {
300
+ roomId: this.metadataSession.roomId,
301
+ refCount: this.metadataSession.refCount
302
+ });
303
+ return this.metadataSession;
304
+ }
305
+ if (this.metadataSession) {
306
+ debug("tearing down previous metadata session", { roomId: this.metadataSession.roomId });
307
+ await this.teardownMetadataSession(this.metadataSession).catch(() => {});
308
+ }
157
309
  const configuredType = this.options.metadataCrdtType;
158
310
  if (configuredType && configuredType !== loro_protocol.CrdtType.Flock) throw new Error(`metadataCrdtType must be ${loro_protocol.CrdtType.Flock} when syncing Flock metadata`);
159
311
  const adaptor = createRepoFlockAdaptorFromDoc(flock, this.options.metadataAdaptorConfig ?? {});
312
+ debug("joining metadata room", {
313
+ roomId: params.roomId,
314
+ hasAuth: Boolean(params.auth && params.auth.length)
315
+ });
160
316
  const room = await client.join({
161
317
  roomId: params.roomId,
162
318
  crdtAdaptor: adaptor,
163
319
  auth: params.auth
164
320
  });
321
+ const firstSynced = room.waitForReachingServerVersion();
322
+ firstSynced.then(() => {
323
+ debug("metadata session firstSynced resolved", { roomId: params.roomId });
324
+ }, (error) => {
325
+ debug("metadata session firstSynced rejected", {
326
+ roomId: params.roomId,
327
+ error
328
+ });
329
+ });
165
330
  const session = {
166
331
  adaptor,
167
332
  room,
168
- firstSynced: room.waitForReachingServerVersion(),
333
+ firstSynced,
169
334
  flock,
170
335
  roomId: params.roomId,
171
336
  auth: params.auth,
@@ -177,34 +342,83 @@ var WebSocketTransportAdapter = class {
177
342
  async teardownMetadataSession(session) {
178
343
  const target = session ?? this.metadataSession;
179
344
  if (!target) return;
345
+ debug("teardownMetadataSession invoked", { roomId: target.roomId });
180
346
  if (this.metadataSession === target) this.metadataSession = void 0;
181
347
  const { adaptor, room } = target;
182
348
  try {
183
349
  await room.leave();
184
- } catch {
350
+ debug("metadata room left", { roomId: target.roomId });
351
+ } catch (error) {
352
+ debug("metadata room leave failed; destroying", {
353
+ roomId: target.roomId,
354
+ error
355
+ });
185
356
  await room.destroy().catch(() => {});
186
357
  }
187
358
  adaptor.destroy();
359
+ debug("metadata session destroyed", { roomId: target.roomId });
188
360
  }
189
361
  async ensureDocSession(docId, doc, params) {
362
+ debug("ensureDocSession invoked", { docId });
190
363
  const client = this.ensureClient();
191
364
  await client.waitConnected();
365
+ debug("websocket client ready for doc session", {
366
+ docId,
367
+ status: client.getStatus()
368
+ });
192
369
  const existing = this.docSessions.get(docId);
193
370
  const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
194
371
  const roomId = normalizeRoomId(params.roomId, derivedRoomId);
195
372
  const auth = params.auth ?? this.options.docAuth?.(docId);
196
- if (existing && existing.doc === doc && existing.roomId === roomId) return existing;
197
- if (existing) await this.leaveDocSession(docId).catch(() => {});
373
+ debug("doc session params resolved", {
374
+ docId,
375
+ roomId,
376
+ hasAuth: Boolean(auth && auth.length)
377
+ });
378
+ if (existing && existing.doc === doc && existing.roomId === roomId) {
379
+ debug("reusing doc session", {
380
+ docId,
381
+ roomId,
382
+ refCount: existing.refCount
383
+ });
384
+ return existing;
385
+ }
386
+ if (existing) {
387
+ debug("doc session mismatch; leaving existing session", {
388
+ docId,
389
+ previousRoomId: existing.roomId,
390
+ nextRoomId: roomId
391
+ });
392
+ await this.leaveDocSession(docId).catch(() => {});
393
+ }
198
394
  const adaptor = new loro_adaptors.LoroAdaptor(doc);
395
+ debug("joining doc room", {
396
+ docId,
397
+ roomId,
398
+ hasAuth: Boolean(auth && auth.length)
399
+ });
199
400
  const room = await client.join({
200
401
  roomId,
201
402
  crdtAdaptor: adaptor,
202
403
  auth
203
404
  });
405
+ const firstSynced = room.waitForReachingServerVersion();
406
+ firstSynced.then(() => {
407
+ debug("doc session firstSynced resolved", {
408
+ docId,
409
+ roomId
410
+ });
411
+ }, (error) => {
412
+ debug("doc session firstSynced rejected", {
413
+ docId,
414
+ roomId,
415
+ error
416
+ });
417
+ });
204
418
  const session = {
205
419
  adaptor,
206
420
  room,
207
- firstSynced: room.waitForReachingServerVersion(),
421
+ firstSynced,
208
422
  doc,
209
423
  roomId,
210
424
  refCount: 0
@@ -214,14 +428,34 @@ var WebSocketTransportAdapter = class {
214
428
  }
215
429
  async leaveDocSession(docId) {
216
430
  const session = this.docSessions.get(docId);
217
- if (!session) return;
431
+ if (!session) {
432
+ debug("leaveDocSession invoked but no session found", { docId });
433
+ return;
434
+ }
218
435
  this.docSessions.delete(docId);
436
+ debug("leaving doc session", {
437
+ docId,
438
+ roomId: session.roomId
439
+ });
219
440
  try {
220
441
  await session.room.leave();
221
- } catch {
442
+ debug("doc room left", {
443
+ docId,
444
+ roomId: session.roomId
445
+ });
446
+ } catch (error) {
447
+ debug("doc room leave failed; destroying", {
448
+ docId,
449
+ roomId: session.roomId,
450
+ error
451
+ });
222
452
  await session.room.destroy().catch(() => {});
223
453
  }
224
454
  session.adaptor.destroy();
455
+ debug("doc session destroyed", {
456
+ docId,
457
+ roomId: session.roomId
458
+ });
225
459
  }
226
460
  };
227
461
 
@@ -483,7 +717,7 @@ const DEFAULT_META_STORE = "meta";
483
717
  const DEFAULT_ASSET_STORE = "assets";
484
718
  const DEFAULT_DOC_UPDATE_STORE = "doc-updates";
485
719
  const DEFAULT_META_KEY = "snapshot";
486
- const textDecoder = new TextDecoder();
720
+ const textDecoder$1 = new TextDecoder();
487
721
  function describeUnknown(cause) {
488
722
  if (typeof cause === "string") return cause;
489
723
  if (typeof cause === "number" || typeof cause === "boolean") return String(cause);
@@ -584,7 +818,7 @@ var IndexedDBStorageAdaptor = class {
584
818
  const bytes = await this.getBinary(this.metaStore, this.metaKey);
585
819
  if (!bytes) return void 0;
586
820
  try {
587
- const json = textDecoder.decode(bytes);
821
+ const json = textDecoder$1.decode(bytes);
588
822
  const bundle = JSON.parse(json);
589
823
  const flock = new __loro_dev_flock.Flock();
590
824
  flock.importJson(bundle);
@@ -728,15 +962,206 @@ var IndexedDBStorageAdaptor = class {
728
962
  };
729
963
 
730
964
  //#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
- };
965
+ //#region src/storage/filesystem.ts
966
+ const textDecoder = new TextDecoder();
967
+ var FileSystemStorageAdaptor = class {
968
+ baseDir;
969
+ docsDir;
970
+ assetsDir;
971
+ metaPath;
972
+ initPromise;
973
+ updateCounter = 0;
974
+ constructor(options = {}) {
975
+ this.baseDir = node_path.resolve(options.baseDir ?? node_path.join(process.cwd(), ".loro-repo"));
976
+ this.docsDir = node_path.join(this.baseDir, options.docsDirName ?? "docs");
977
+ this.assetsDir = node_path.join(this.baseDir, options.assetsDirName ?? "assets");
978
+ this.metaPath = node_path.join(this.baseDir, options.metaFileName ?? "meta.json");
979
+ this.initPromise = this.ensureLayout();
980
+ }
981
+ async save(payload) {
982
+ await this.initPromise;
983
+ switch (payload.type) {
984
+ case "doc-snapshot":
985
+ await this.writeDocSnapshot(payload.docId, payload.snapshot);
986
+ return;
987
+ case "doc-update":
988
+ await this.enqueueDocUpdate(payload.docId, payload.update);
989
+ return;
990
+ case "asset":
991
+ await this.writeAsset(payload.assetId, payload.data);
992
+ return;
993
+ case "meta":
994
+ await writeFileAtomic(this.metaPath, payload.update);
995
+ return;
996
+ default: throw new Error(`Unsupported payload type: ${payload.type}`);
997
+ }
998
+ }
999
+ async deleteAsset(assetId) {
1000
+ await this.initPromise;
1001
+ await removeIfExists(this.assetPath(assetId));
1002
+ }
1003
+ async loadDoc(docId) {
1004
+ await this.initPromise;
1005
+ const snapshotBytes = await readFileIfExists(this.docSnapshotPath(docId));
1006
+ const updateDir = this.docUpdatesDir(docId);
1007
+ const updateFiles = await listFiles(updateDir);
1008
+ if (!snapshotBytes && updateFiles.length === 0) return;
1009
+ const doc = snapshotBytes ? loro_crdt.LoroDoc.fromSnapshot(snapshotBytes) : new loro_crdt.LoroDoc();
1010
+ if (updateFiles.length === 0) return doc;
1011
+ const updatePaths = updateFiles.map((file) => node_path.join(updateDir, file));
1012
+ for (const updatePath of updatePaths) {
1013
+ const update = await readFileIfExists(updatePath);
1014
+ if (!update) continue;
1015
+ doc.import(update);
1016
+ }
1017
+ await Promise.all(updatePaths.map((filePath) => removeIfExists(filePath)));
1018
+ const consolidated = doc.export({ mode: "snapshot" });
1019
+ await this.writeDocSnapshot(docId, consolidated);
1020
+ return doc;
1021
+ }
1022
+ async loadMeta() {
1023
+ await this.initPromise;
1024
+ const bytes = await readFileIfExists(this.metaPath);
1025
+ if (!bytes) return void 0;
1026
+ try {
1027
+ const bundle = JSON.parse(textDecoder.decode(bytes));
1028
+ const flock = new __loro_dev_flock.Flock();
1029
+ flock.importJson(bundle);
1030
+ return flock;
1031
+ } catch (error) {
1032
+ throw new Error("Failed to hydrate metadata snapshot", { cause: error });
1033
+ }
1034
+ }
1035
+ async loadAsset(assetId) {
1036
+ await this.initPromise;
1037
+ return readFileIfExists(this.assetPath(assetId));
1038
+ }
1039
+ async ensureLayout() {
1040
+ await Promise.all([
1041
+ ensureDir(this.baseDir),
1042
+ ensureDir(this.docsDir),
1043
+ ensureDir(this.assetsDir)
1044
+ ]);
1045
+ }
1046
+ async writeDocSnapshot(docId, snapshot) {
1047
+ await ensureDir(this.docDir(docId));
1048
+ await writeFileAtomic(this.docSnapshotPath(docId), snapshot);
1049
+ }
1050
+ async enqueueDocUpdate(docId, update) {
1051
+ const dir = this.docUpdatesDir(docId);
1052
+ await ensureDir(dir);
1053
+ const counter = this.updateCounter = (this.updateCounter + 1) % 1e6;
1054
+ const fileName = `${Date.now().toString().padStart(13, "0")}-${counter.toString().padStart(6, "0")}.bin`;
1055
+ await writeFileAtomic(node_path.join(dir, fileName), update);
1056
+ }
1057
+ async writeAsset(assetId, data) {
1058
+ const filePath = this.assetPath(assetId);
1059
+ await ensureDir(node_path.dirname(filePath));
1060
+ await writeFileAtomic(filePath, data);
1061
+ }
1062
+ docDir(docId) {
1063
+ return node_path.join(this.docsDir, encodeComponent(docId));
1064
+ }
1065
+ docSnapshotPath(docId) {
1066
+ return node_path.join(this.docDir(docId), "snapshot.bin");
1067
+ }
1068
+ docUpdatesDir(docId) {
1069
+ return node_path.join(this.docDir(docId), "updates");
1070
+ }
1071
+ assetPath(assetId) {
1072
+ return node_path.join(this.assetsDir, encodeComponent(assetId));
1073
+ }
1074
+ };
1075
+ function encodeComponent(value) {
1076
+ return Buffer.from(value, "utf8").toString("base64url");
1077
+ }
1078
+ async function ensureDir(dir) {
1079
+ await node_fs.promises.mkdir(dir, { recursive: true });
1080
+ }
1081
+ async function readFileIfExists(filePath) {
1082
+ try {
1083
+ const data = await node_fs.promises.readFile(filePath);
1084
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
1085
+ } catch (error) {
1086
+ if (error.code === "ENOENT") return;
1087
+ throw error;
1088
+ }
1089
+ }
1090
+ async function removeIfExists(filePath) {
1091
+ try {
1092
+ await node_fs.promises.rm(filePath);
1093
+ } catch (error) {
1094
+ if (error.code === "ENOENT") return;
1095
+ throw error;
1096
+ }
1097
+ }
1098
+ async function listFiles(dir) {
1099
+ try {
1100
+ return (await node_fs.promises.readdir(dir)).sort();
1101
+ } catch (error) {
1102
+ if (error.code === "ENOENT") return [];
1103
+ throw error;
1104
+ }
1105
+ }
1106
+ async function writeFileAtomic(targetPath, data) {
1107
+ const dir = node_path.dirname(targetPath);
1108
+ await ensureDir(dir);
1109
+ const tempPath = node_path.join(dir, `.tmp-${(0, node_crypto.randomUUID)()}`);
1110
+ await node_fs.promises.writeFile(tempPath, data);
1111
+ await node_fs.promises.rename(tempPath, targetPath);
739
1112
  }
1113
+
1114
+ //#endregion
1115
+ //#region src/internal/event-bus.ts
1116
+ var RepoEventBus = class {
1117
+ watchers = /* @__PURE__ */ new Set();
1118
+ eventByStack = [];
1119
+ watch(listener, filter = {}) {
1120
+ const entry = {
1121
+ listener,
1122
+ filter
1123
+ };
1124
+ this.watchers.add(entry);
1125
+ return { unsubscribe: () => {
1126
+ this.watchers.delete(entry);
1127
+ } };
1128
+ }
1129
+ emit(event) {
1130
+ for (const entry of this.watchers) if (this.shouldNotify(entry.filter, event)) entry.listener(event);
1131
+ }
1132
+ clear() {
1133
+ this.watchers.clear();
1134
+ this.eventByStack.length = 0;
1135
+ }
1136
+ pushEventBy(by) {
1137
+ this.eventByStack.push(by);
1138
+ }
1139
+ popEventBy() {
1140
+ this.eventByStack.pop();
1141
+ }
1142
+ resolveEventBy(defaultBy) {
1143
+ const index = this.eventByStack.length - 1;
1144
+ return index >= 0 ? this.eventByStack[index] : defaultBy;
1145
+ }
1146
+ shouldNotify(filter, event) {
1147
+ if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
1148
+ if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
1149
+ if (filter.by && !filter.by.includes(event.by)) return false;
1150
+ const docId = (() => {
1151
+ if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
1152
+ if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
1153
+ })();
1154
+ if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
1155
+ if (filter.docIds && !docId) return false;
1156
+ if (filter.metadataFields && event.kind === "doc-metadata") {
1157
+ if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
1158
+ }
1159
+ return true;
1160
+ }
1161
+ };
1162
+
1163
+ //#endregion
1164
+ //#region src/utils.ts
740
1165
  async function streamToUint8Array(stream) {
741
1166
  const reader = stream.getReader();
742
1167
  const chunks = [];
@@ -814,12 +1239,14 @@ function asJsonObject(value) {
814
1239
  if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
815
1240
  }
816
1241
  function isJsonObjectValue(value) {
817
- return typeof value === "object" && value !== null && !Array.isArray(value);
1242
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
818
1243
  }
819
1244
  function stableStringify(value) {
820
- if (value === null || typeof value !== "object") return JSON.stringify(value);
1245
+ if (value === null) return "null";
1246
+ if (typeof value === "string") return JSON.stringify(value);
1247
+ if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
821
1248
  if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
822
- if (!isJsonObjectValue(value)) return JSON.stringify(value);
1249
+ if (!isJsonObjectValue(value)) return "null";
823
1250
  return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
824
1251
  }
825
1252
  function jsonEquals(a, b) {
@@ -829,15 +1256,17 @@ function jsonEquals(a, b) {
829
1256
  }
830
1257
  function diffJsonObjects(previous, next) {
831
1258
  const patch = {};
832
- const keys = new Set([...Object.keys(next), ...previous ? Object.keys(previous) : []]);
1259
+ const keys = /* @__PURE__ */ new Set();
1260
+ if (previous) for (const key of Object.keys(previous)) keys.add(key);
1261
+ for (const key of Object.keys(next)) keys.add(key);
833
1262
  for (const key of keys) {
834
1263
  const prevValue = previous ? previous[key] : void 0;
835
- if (!Object.prototype.hasOwnProperty.call(next, key)) {
836
- patch[key] = null;
837
- continue;
838
- }
839
1264
  const nextValue = next[key];
840
1265
  if (!jsonEquals(prevValue, nextValue)) {
1266
+ if (nextValue === void 0 && previous && key in previous) {
1267
+ patch[key] = null;
1268
+ continue;
1269
+ }
841
1270
  const cloned = cloneJsonValue(nextValue);
842
1271
  if (cloned !== void 0) patch[key] = cloned;
843
1272
  }
@@ -893,61 +1322,24 @@ function toReadableStream(bytes) {
893
1322
  controller.close();
894
1323
  } });
895
1324
  }
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
- function emptyFrontiers() {
906
- return [];
907
- }
908
- function getDocFrontiers(doc) {
909
- const candidate = doc;
910
- if (typeof candidate.frontiers === "function") {
911
- const result = candidate.frontiers();
912
- if (result) return result;
913
- }
914
- return emptyFrontiers();
915
- }
916
- function versionVectorToJson(vv) {
917
- const map = vv.toJSON();
918
- const record = {};
919
- if (map instanceof Map) {
920
- const entries = Array.from(map.entries()).sort(([a], [b]) => String(a).localeCompare(String(b)));
921
- for (const [peer, counter] of entries) {
922
- if (typeof counter !== "number" || !Number.isFinite(counter)) continue;
923
- const key = typeof peer === "string" ? peer : JSON.stringify(peer);
924
- record[key] = counter;
925
- }
926
- }
927
- return record;
928
- }
929
- function canonicalizeVersionVector(vv) {
930
- const json = versionVectorToJson(vv);
1325
+ function canonicalizeFrontiers(frontiers) {
1326
+ const json = [...frontiers].sort((a, b) => {
1327
+ if (a.peer < b.peer) return -1;
1328
+ if (a.peer > b.peer) return 1;
1329
+ return a.counter - b.counter;
1330
+ }).map((f) => ({
1331
+ peer: f.peer,
1332
+ counter: f.counter
1333
+ }));
931
1334
  return {
932
1335
  json,
933
1336
  key: stableStringify(json)
934
1337
  };
935
1338
  }
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
- };
1339
+ function includesFrontiers(vv, frontiers) {
1340
+ for (const { peer, counter } of frontiers) if ((vv.get(peer) ?? 0) <= counter) return false;
1341
+ return true;
1342
+ }
951
1343
  function matchesQuery(docId, _metadata, query) {
952
1344
  if (!query) return true;
953
1345
  if (query.prefix && !docId.startsWith(query.prefix)) return false;
@@ -955,345 +1347,372 @@ function matchesQuery(docId, _metadata, query) {
955
1347
  if (query.end && docId > query.end) return false;
956
1348
  return true;
957
1349
  }
958
- var LoroRepo = class {
959
- options;
960
- transport;
1350
+
1351
+ //#endregion
1352
+ //#region src/internal/logging.ts
1353
+ function logAsyncError(context) {
1354
+ return (error) => {
1355
+ if (error instanceof Error) console.error(`[loro-repo] ${context} failed: ${error.message}`, error);
1356
+ else console.error(`[loro-repo] ${context} failed with non-error reason:`, error);
1357
+ };
1358
+ }
1359
+
1360
+ //#endregion
1361
+ //#region src/internal/doc-manager.ts
1362
+ var DocManager = class {
961
1363
  storage;
962
- assetTransport;
963
- docFactory;
964
- metaFlock = new __loro_dev_flock.Flock();
965
- metadata = /* @__PURE__ */ new Map();
1364
+ docFrontierDebounceMs;
1365
+ getMetaFlock;
1366
+ eventBus;
1367
+ persistMeta;
1368
+ state;
966
1369
  docs = /* @__PURE__ */ new Map();
967
- docRefs = /* @__PURE__ */ new Map();
968
1370
  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
1371
  docFrontierUpdates = /* @__PURE__ */ new Map();
975
1372
  docPersistedVersions = /* @__PURE__ */ new Map();
976
- docFrontierDebounceMs;
977
- watchers = /* @__PURE__ */ new Set();
978
- eventByStack = [];
979
- metaRoomSubscription;
980
- unsubscribeMetaFlock;
981
- readyPromise;
1373
+ get docFrontierKeys() {
1374
+ return this.state.docFrontierKeys;
1375
+ }
982
1376
  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;
1377
+ this.storage = options.storage;
1378
+ this.docFrontierDebounceMs = options.docFrontierDebounceMs;
1379
+ this.getMetaFlock = options.getMetaFlock;
1380
+ this.eventBus = options.eventBus;
1381
+ this.persistMeta = options.persistMeta;
1382
+ this.state = options.state;
990
1383
  }
991
- async ready() {
992
- if (!this.readyPromise) this.readyPromise = this.initialize();
993
- await this.readyPromise;
1384
+ async openCollaborativeDoc(docId) {
1385
+ return await this.ensureDoc(docId);
994
1386
  }
995
- async initialize() {
1387
+ async openDetachedDoc(docId) {
1388
+ return await this.materializeDetachedDoc(docId);
1389
+ }
1390
+ async ensureDoc(docId) {
1391
+ const cached = this.docs.get(docId);
1392
+ if (cached) {
1393
+ this.ensureDocSubscription(docId, cached);
1394
+ if (!this.docPersistedVersions.has(docId)) this.docPersistedVersions.set(docId, cached.version());
1395
+ return cached;
1396
+ }
996
1397
  if (this.storage) {
997
- const snapshot = await this.storage.loadMeta();
998
- if (snapshot) this.metaFlock = snapshot;
1398
+ const stored = await this.storage.loadDoc(docId);
1399
+ if (stored) {
1400
+ this.registerDoc(docId, stored);
1401
+ return stored;
1402
+ }
999
1403
  }
1000
- this.hydrateMetadataFromFlock("sync");
1404
+ const created = new loro_crdt.LoroDoc();
1405
+ this.registerDoc(docId, created);
1406
+ return created;
1001
1407
  }
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
- }
1408
+ async persistDoc(docId, doc) {
1409
+ const previousVersion = this.docPersistedVersions.get(docId);
1410
+ const snapshot = doc.export({ mode: "snapshot" });
1411
+ const nextVersion = doc.version();
1412
+ if (!this.storage) {
1413
+ this.docPersistedVersions.set(docId, nextVersion);
1414
+ return;
1023
1415
  }
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");
1036
- }
1416
+ this.docPersistedVersions.set(docId, nextVersion);
1417
+ try {
1418
+ await this.storage.save({
1419
+ type: "doc-snapshot",
1420
+ docId,
1421
+ snapshot
1422
+ });
1423
+ } catch (error) {
1424
+ if (previousVersion) this.docPersistedVersions.set(docId, previousVersion);
1425
+ else this.docPersistedVersions.delete(docId);
1426
+ throw error;
1037
1427
  }
1038
1428
  }
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
- });
1429
+ async updateDocFrontiers(docId, doc, defaultBy) {
1430
+ const frontiers = doc.oplogFrontiers();
1431
+ const { json, key } = canonicalizeFrontiers(frontiers);
1432
+ const existingKeys = this.docFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
1433
+ let mutated = false;
1434
+ const metaFlock = this.metaFlock;
1435
+ const vv = doc.version();
1436
+ for (const entry of existingKeys) {
1437
+ if (entry === key) continue;
1438
+ let oldFrontiers;
1439
+ try {
1440
+ oldFrontiers = JSON.parse(entry);
1441
+ } catch {
1442
+ continue;
1051
1443
  }
1052
- return;
1053
- }
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
- }
1444
+ if (includesFrontiers(vv, oldFrontiers)) {
1445
+ metaFlock.delete([
1446
+ "f",
1447
+ docId,
1448
+ entry
1449
+ ]);
1450
+ mutated = true;
1087
1451
  }
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
1452
  }
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",
1453
+ if (!existingKeys.has(key)) {
1454
+ metaFlock.put([
1455
+ "f",
1100
1456
  docId,
1101
- assetId,
1102
- by
1103
- });
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
- }
1457
+ key
1458
+ ], json);
1459
+ mutated = true;
1114
1460
  }
1115
- for (const assetId of added) this.emit({
1116
- kind: "asset-link",
1461
+ if (mutated) {
1462
+ this.refreshDocFrontierKeys(docId);
1463
+ await this.persistMeta();
1464
+ }
1465
+ const by = this.eventBus.resolveEventBy(defaultBy);
1466
+ this.eventBus.emit({
1467
+ kind: "doc-frontiers",
1117
1468
  docId,
1118
- assetId,
1469
+ frontiers,
1119
1470
  by
1120
1471
  });
1121
1472
  }
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;
1473
+ async flushScheduledDocFrontierUpdate(docId) {
1474
+ const pending = this.docFrontierUpdates.get(docId);
1475
+ if (!pending) return false;
1476
+ clearTimeout(pending.timeout);
1477
+ this.docFrontierUpdates.delete(docId);
1478
+ this.eventBus.pushEventBy(pending.by);
1479
+ try {
1480
+ await this.updateDocFrontiers(docId, pending.doc, pending.by);
1481
+ } finally {
1482
+ this.eventBus.popEventBy();
1128
1483
  }
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
- });
1484
+ return true;
1485
+ }
1486
+ async unloadDoc(docId) {
1487
+ const doc = this.docs.get(docId);
1488
+ if (!doc) return;
1489
+ await this.flushScheduledDocFrontierUpdate(docId);
1490
+ await this.persistDocUpdate(docId, doc);
1491
+ await this.updateDocFrontiers(docId, doc, "local");
1492
+ this.docSubscriptions.get(docId)?.();
1493
+ this.docSubscriptions.delete(docId);
1494
+ this.docs.delete(docId);
1495
+ this.docPersistedVersions.delete(docId);
1496
+ }
1497
+ async flush() {
1498
+ const promises = [];
1499
+ for (const [docId, doc] of this.docs) promises.push((async () => {
1500
+ await this.persistDocUpdate(docId, doc);
1501
+ await this.flushScheduledDocFrontierUpdate(docId);
1502
+ })());
1503
+ await Promise.all(promises);
1504
+ }
1505
+ async close() {
1506
+ await this.flush();
1507
+ for (const unsubscribe of this.docSubscriptions.values()) try {
1508
+ unsubscribe();
1509
+ } catch {}
1510
+ this.docSubscriptions.clear();
1511
+ this.docFrontierUpdates.clear();
1512
+ this.docs.clear();
1513
+ this.docPersistedVersions.clear();
1514
+ this.docFrontierKeys.clear();
1515
+ }
1516
+ hydrateFrontierKeys() {
1517
+ const nextFrontierKeys = /* @__PURE__ */ new Map();
1518
+ const frontierRows = this.metaFlock.scan({ prefix: ["f"] });
1519
+ for (const row of frontierRows) {
1520
+ if (!Array.isArray(row.key) || row.key.length < 3) continue;
1521
+ const docId = row.key[1];
1522
+ const frontierKey = row.key[2];
1523
+ if (typeof docId !== "string" || typeof frontierKey !== "string") continue;
1524
+ const set = nextFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
1525
+ set.add(frontierKey);
1526
+ nextFrontierKeys.set(docId, set);
1527
+ }
1528
+ this.docFrontierKeys.clear();
1529
+ for (const [docId, keys] of nextFrontierKeys) this.docFrontierKeys.set(docId, keys);
1137
1530
  }
1138
1531
  refreshDocFrontierKeys(docId) {
1139
1532
  const rows = this.metaFlock.scan({ prefix: ["f", docId] });
1140
1533
  const keys = /* @__PURE__ */ new Set();
1141
1534
  for (const row of rows) {
1142
1535
  if (!Array.isArray(row.key) || row.key.length < 3) continue;
1536
+ if (row.value === void 0 || row.value === null) continue;
1143
1537
  const frontierKey = row.key[2];
1144
1538
  if (typeof frontierKey === "string") keys.add(frontierKey);
1145
1539
  }
1146
1540
  if (keys.size > 0) this.docFrontierKeys.set(docId, keys);
1147
1541
  else this.docFrontierKeys.delete(docId);
1148
1542
  }
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;
1543
+ get metaFlock() {
1544
+ return this.getMetaFlock();
1199
1545
  }
1200
- readAssetMetadataFromFlock(assetId) {
1201
- return assetMetaFromJson(this.metaFlock.get(["a", assetId]));
1546
+ registerDoc(docId, doc) {
1547
+ this.docs.set(docId, doc);
1548
+ this.docPersistedVersions.set(docId, doc.version());
1549
+ this.ensureDocSubscription(docId, doc);
1202
1550
  }
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
1551
+ ensureDocSubscription(docId, doc) {
1552
+ if (this.docSubscriptions.has(docId)) return;
1553
+ const unsubscribe = doc.subscribe((batch) => {
1554
+ const stackBy = this.eventBus.resolveEventBy("local");
1555
+ const by = stackBy === "local" && batch.by === "import" ? "live" : stackBy;
1556
+ this.onDocEvent(docId, doc, batch, by);
1211
1557
  });
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
1558
+ if (typeof unsubscribe === "function") this.docSubscriptions.set(docId, unsubscribe);
1559
+ }
1560
+ scheduleDocFrontierUpdate(docId, doc, by) {
1561
+ const existing = this.docFrontierUpdates.get(docId);
1562
+ const effectiveBy = existing ? this.mergeRepoEventBy(existing.by, by) : by;
1563
+ if (existing) clearTimeout(existing.timeout);
1564
+ const delay = this.docFrontierDebounceMs > 0 ? this.docFrontierDebounceMs : 0;
1565
+ const timeout = setTimeout(() => this.runScheduledDocFrontierUpdate(docId), delay);
1566
+ this.docFrontierUpdates.set(docId, {
1567
+ timeout,
1568
+ doc,
1569
+ by: effectiveBy
1222
1570
  });
1223
1571
  }
1224
- emit(event) {
1225
- for (const entry of this.watchers) if (this.shouldNotify(entry.filter, event)) entry.listener(event);
1572
+ mergeRepoEventBy(current, next) {
1573
+ if (current === next) return current;
1574
+ if (current === "live" || next === "live") return "live";
1575
+ if (current === "sync" || next === "sync") return "sync";
1576
+ return "local";
1226
1577
  }
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;
1578
+ runScheduledDocFrontierUpdate(docId) {
1579
+ const pending = this.docFrontierUpdates.get(docId);
1580
+ if (!pending) return;
1581
+ this.docFrontierUpdates.delete(docId);
1582
+ this.eventBus.pushEventBy(pending.by);
1583
+ (async () => {
1584
+ try {
1585
+ await this.updateDocFrontiers(docId, pending.doc, pending.by);
1586
+ } finally {
1587
+ this.eventBus.popEventBy();
1246
1588
  }
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;
1589
+ })().catch(logAsyncError(`doc ${docId} frontier debounce`));
1255
1590
  }
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;
1591
+ async materializeDetachedDoc(docId) {
1592
+ const snapshot = await this.exportDocSnapshot(docId);
1593
+ if (snapshot) return loro_crdt.LoroDoc.fromSnapshot(snapshot);
1594
+ return new loro_crdt.LoroDoc();
1264
1595
  }
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;
1596
+ async exportDocSnapshot(docId) {
1597
+ const cached = this.docs.get(docId);
1598
+ if (cached) return cached.export({ mode: "snapshot" });
1599
+ if (!this.storage) return;
1600
+ return (await this.storage.loadDoc(docId))?.export({ mode: "snapshot" });
1601
+ }
1602
+ async persistDocUpdate(docId, doc) {
1603
+ const previousVersion = this.docPersistedVersions.get(docId);
1604
+ const nextVersion = doc.oplogVersion();
1605
+ if (!this.storage) {
1606
+ this.docPersistedVersions.set(docId, nextVersion);
1607
+ return;
1275
1608
  }
1276
- const pendingDocIds = Array.from(this.docFrontierUpdates.keys());
1277
- for (const docId of pendingDocIds) try {
1278
- await this.flushScheduledDocFrontierUpdate(docId);
1609
+ if (!previousVersion) {
1610
+ await this.persistDoc(docId, doc);
1611
+ this.docPersistedVersions.set(docId, nextVersion);
1612
+ return;
1613
+ }
1614
+ if (previousVersion.compare(nextVersion) === 0) return;
1615
+ const update = doc.export({
1616
+ mode: "update",
1617
+ from: previousVersion
1618
+ });
1619
+ this.docPersistedVersions.set(docId, nextVersion);
1620
+ try {
1621
+ await this.storage.save({
1622
+ type: "doc-update",
1623
+ docId,
1624
+ update
1625
+ });
1279
1626
  } catch (error) {
1280
- logAsyncError(`doc ${docId} frontier flush on close`)(error);
1627
+ this.docPersistedVersions.set(docId, previousVersion);
1628
+ throw error;
1281
1629
  }
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
1630
  }
1294
- async upsertDocMeta(docId, patch, _options = {}) {
1295
- await this.ready();
1296
- const base = this.metadata.get(docId);
1631
+ onDocEvent(docId, doc, _batch, by) {
1632
+ (async () => {
1633
+ const persist = this.persistDocUpdate(docId, doc);
1634
+ if (by === "local") {
1635
+ this.scheduleDocFrontierUpdate(docId, doc, by);
1636
+ await persist;
1637
+ return;
1638
+ }
1639
+ const flushed = this.flushScheduledDocFrontierUpdate(docId);
1640
+ const updated = (async () => {
1641
+ this.eventBus.pushEventBy(by);
1642
+ try {
1643
+ await this.updateDocFrontiers(docId, doc, by);
1644
+ } finally {
1645
+ this.eventBus.popEventBy();
1646
+ }
1647
+ })();
1648
+ await Promise.all([
1649
+ persist,
1650
+ flushed,
1651
+ updated
1652
+ ]);
1653
+ })().catch(logAsyncError(`doc ${docId} event processing`));
1654
+ }
1655
+ };
1656
+
1657
+ //#endregion
1658
+ //#region src/internal/metadata-manager.ts
1659
+ var MetadataManager = class {
1660
+ getMetaFlock;
1661
+ eventBus;
1662
+ persistMeta;
1663
+ state;
1664
+ constructor(options) {
1665
+ this.getMetaFlock = options.getMetaFlock;
1666
+ this.eventBus = options.eventBus;
1667
+ this.persistMeta = options.persistMeta;
1668
+ this.state = options.state;
1669
+ }
1670
+ getDocIds() {
1671
+ return Array.from(this.state.metadata.keys());
1672
+ }
1673
+ entries() {
1674
+ return this.state.metadata.entries();
1675
+ }
1676
+ get(docId) {
1677
+ const metadata = this.state.metadata.get(docId);
1678
+ return metadata ? cloneJsonObject(metadata) : void 0;
1679
+ }
1680
+ listDoc(query) {
1681
+ if (query?.limit !== void 0 && query.limit <= 0) return [];
1682
+ const { startKey, endKey } = this.computeDocRangeKeys(query);
1683
+ if (startKey && endKey && startKey >= endKey) return [];
1684
+ const scanOptions = { prefix: ["m"] };
1685
+ if (startKey) scanOptions.start = {
1686
+ kind: "inclusive",
1687
+ key: ["m", startKey]
1688
+ };
1689
+ if (endKey) scanOptions.end = {
1690
+ kind: "exclusive",
1691
+ key: ["m", endKey]
1692
+ };
1693
+ const rows = this.metaFlock.scan(scanOptions);
1694
+ const seen = /* @__PURE__ */ new Set();
1695
+ const entries = [];
1696
+ for (const row of rows) {
1697
+ if (query?.limit !== void 0 && entries.length >= query.limit) break;
1698
+ if (!Array.isArray(row.key) || row.key.length < 2) continue;
1699
+ const docId = row.key[1];
1700
+ if (typeof docId !== "string") continue;
1701
+ if (seen.has(docId)) continue;
1702
+ seen.add(docId);
1703
+ const metadata = this.state.metadata.get(docId);
1704
+ if (!metadata) continue;
1705
+ if (!matchesQuery(docId, metadata, query)) continue;
1706
+ entries.push({
1707
+ docId,
1708
+ meta: cloneJsonObject(metadata)
1709
+ });
1710
+ if (query?.limit !== void 0 && entries.length >= query.limit) break;
1711
+ }
1712
+ return entries;
1713
+ }
1714
+ async upsert(docId, patch) {
1715
+ const base = this.state.metadata.get(docId);
1297
1716
  const next = base ? cloneJsonObject(base) : {};
1298
1717
  const outPatch = {};
1299
1718
  let changed = false;
@@ -1318,70 +1737,159 @@ var LoroRepo = class {
1318
1737
  changed = true;
1319
1738
  }
1320
1739
  if (!changed) {
1321
- if (!this.metadata.has(docId)) this.metadata.set(docId, next);
1740
+ if (!this.state.metadata.has(docId)) this.state.metadata.set(docId, next);
1322
1741
  return;
1323
1742
  }
1324
- this.metadata.set(docId, next);
1743
+ this.state.metadata.set(docId, next);
1325
1744
  await this.persistMeta();
1326
- this.emit({
1745
+ this.eventBus.emit({
1327
1746
  kind: "doc-metadata",
1328
1747
  docId,
1329
1748
  patch: cloneJsonObject(outPatch),
1330
1749
  by: "local"
1331
1750
  });
1332
1751
  }
1333
- async getDocMeta(docId) {
1334
- await this.ready();
1335
- const metadata = this.metadata.get(docId);
1336
- return metadata ? cloneJsonObject(metadata) : void 0;
1752
+ refreshFromFlock(docId, by) {
1753
+ const previous = this.state.metadata.get(docId);
1754
+ const next = this.readDocMetadataFromFlock(docId);
1755
+ if (!next) {
1756
+ if (previous) {
1757
+ this.state.metadata.delete(docId);
1758
+ this.eventBus.emit({
1759
+ kind: "doc-metadata",
1760
+ docId,
1761
+ patch: {},
1762
+ by
1763
+ });
1764
+ }
1765
+ return;
1766
+ }
1767
+ this.state.metadata.set(docId, next);
1768
+ const patch = diffJsonObjects(previous, next);
1769
+ if (!previous || Object.keys(patch).length > 0) this.eventBus.emit({
1770
+ kind: "doc-metadata",
1771
+ docId,
1772
+ patch,
1773
+ by
1774
+ });
1337
1775
  }
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({
1776
+ replaceAll(nextMetadata, by) {
1777
+ const prevMetadata = new Map(this.state.metadata);
1778
+ this.state.metadata.clear();
1779
+ for (const [docId, meta] of nextMetadata) this.state.metadata.set(docId, meta);
1780
+ const docIds = new Set([...prevMetadata.keys(), ...nextMetadata.keys()]);
1781
+ for (const docId of docIds) {
1782
+ const previous = prevMetadata.get(docId);
1783
+ const current = nextMetadata.get(docId);
1784
+ if (!current) {
1785
+ if (previous) this.eventBus.emit({
1786
+ kind: "doc-metadata",
1787
+ docId,
1788
+ patch: {},
1789
+ by
1790
+ });
1791
+ continue;
1792
+ }
1793
+ const patch = diffJsonObjects(previous, current);
1794
+ if (!previous || Object.keys(patch).length > 0) this.eventBus.emit({
1795
+ kind: "doc-metadata",
1344
1796
  docId,
1345
- meta: cloneJsonObject(metadata)
1797
+ patch,
1798
+ by
1346
1799
  });
1347
1800
  }
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
1801
  }
1352
- getMetaReplica() {
1353
- return this.metaFlock;
1802
+ clear() {
1803
+ this.state.metadata.clear();
1354
1804
  }
1355
- watch(listener, filter = {}) {
1356
- const entry = {
1357
- listener,
1358
- filter
1805
+ computeDocRangeKeys(query) {
1806
+ if (!query) return {};
1807
+ const prefix = query.prefix && query.prefix.length > 0 ? query.prefix : void 0;
1808
+ let startKey = query.start;
1809
+ if (prefix) startKey = !startKey || prefix > startKey ? prefix : startKey;
1810
+ let endKey = query.end;
1811
+ const prefixEnd = this.nextLexicographicString(prefix);
1812
+ if (prefixEnd) endKey = !endKey || prefixEnd < endKey ? prefixEnd : endKey;
1813
+ return {
1814
+ startKey,
1815
+ endKey
1359
1816
  };
1360
- this.watchers.add(entry);
1361
- return { unsubscribe: () => {
1362
- this.watchers.delete(entry);
1363
- } };
1364
1817
  }
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));
1818
+ nextLexicographicString(value) {
1819
+ if (!value) return void 0;
1820
+ for (let i = value.length - 1; i >= 0; i -= 1) {
1821
+ const code = value.charCodeAt(i);
1822
+ if (code < 65535) return `${value.slice(0, i)}${String.fromCharCode(code + 1)}`;
1823
+ }
1374
1824
  }
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 () => {});
1825
+ readDocMetadataFromFlock(docId) {
1826
+ const rows = this.metaFlock.scan({ prefix: ["m", docId] });
1827
+ if (!rows.length) return void 0;
1828
+ const docMeta = {};
1829
+ let populated = false;
1830
+ for (const row of rows) {
1831
+ if (!Array.isArray(row.key) || row.key.length < 2) continue;
1832
+ if (row.key.length === 2) {
1833
+ const obj = asJsonObject(row.value);
1834
+ if (!obj) continue;
1835
+ for (const [field, value] of Object.entries(obj)) {
1836
+ const cloned = cloneJsonValue(value);
1837
+ if (cloned !== void 0) {
1838
+ docMeta[field] = cloned;
1839
+ populated = true;
1840
+ }
1841
+ }
1842
+ continue;
1843
+ }
1844
+ const fieldKey = row.key[2];
1845
+ if (typeof fieldKey !== "string") continue;
1846
+ if (fieldKey === "$tombstone") {
1847
+ docMeta.tombstone = Boolean(row.value);
1848
+ populated = true;
1849
+ continue;
1850
+ }
1851
+ const jsonValue = cloneJsonValue(row.value);
1852
+ if (jsonValue === void 0) continue;
1853
+ docMeta[fieldKey] = jsonValue;
1854
+ populated = true;
1855
+ }
1856
+ return populated ? docMeta : void 0;
1857
+ }
1858
+ get metaFlock() {
1859
+ return this.getMetaFlock();
1860
+ }
1861
+ };
1862
+
1863
+ //#endregion
1864
+ //#region src/internal/asset-manager.ts
1865
+ var AssetManager = class {
1866
+ storage;
1867
+ assetTransport;
1868
+ getMetaFlock;
1869
+ eventBus;
1870
+ persistMeta;
1871
+ state;
1872
+ get docAssets() {
1873
+ return this.state.docAssets;
1874
+ }
1875
+ get assets() {
1876
+ return this.state.assets;
1877
+ }
1878
+ get orphanedAssets() {
1879
+ return this.state.orphanedAssets;
1880
+ }
1881
+ get assetToDocRefs() {
1882
+ return this.state.assetToDocRefs;
1883
+ }
1884
+ constructor(options) {
1885
+ this.storage = options.storage;
1886
+ this.assetTransport = options.assetTransport;
1887
+ this.getMetaFlock = options.getMetaFlock;
1888
+ this.eventBus = options.eventBus;
1889
+ this.persistMeta = options.persistMeta;
1890
+ this.state = options.state;
1382
1891
  }
1383
1892
  async uploadAsset(params) {
1384
- await this.ready();
1385
1893
  const bytes = await assetContentToUint8Array(params.content);
1386
1894
  const assetId = await computeSha256(bytes);
1387
1895
  if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
@@ -1418,7 +1926,7 @@ var LoroRepo = class {
1418
1926
  existing.metadata = metadata$1;
1419
1927
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata$1));
1420
1928
  await this.persistMeta();
1421
- this.emit({
1929
+ this.eventBus.emit({
1422
1930
  kind: "asset-metadata",
1423
1931
  asset: this.createAssetDownload(assetId, metadata$1, existing.data),
1424
1932
  by: "local"
@@ -1451,26 +1959,17 @@ var LoroRepo = class {
1451
1959
  data: storedBytes.slice()
1452
1960
  });
1453
1961
  this.rememberAsset(metadata, storedBytes);
1454
- for (const assets of this.docAssets.values()) if (assets.has(assetId)) assets.set(assetId, metadata);
1962
+ this.updateDocAssetMetadata(assetId, metadata);
1455
1963
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
1456
1964
  await this.persistMeta();
1457
- this.emit({
1965
+ this.eventBus.emit({
1458
1966
  kind: "asset-metadata",
1459
1967
  asset: this.createAssetDownload(assetId, metadata, storedBytes),
1460
1968
  by: "local"
1461
1969
  });
1462
1970
  return assetId;
1463
1971
  }
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
1972
  async linkAsset(docId, params) {
1473
- await this.ready();
1474
1973
  const bytes = await assetContentToUint8Array(params.content);
1475
1974
  const assetId = await computeSha256(bytes);
1476
1975
  if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
@@ -1524,7 +2023,7 @@ var LoroRepo = class {
1524
2023
  metadata = nextMetadata;
1525
2024
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
1526
2025
  await this.persistMeta();
1527
- this.emit({
2026
+ this.eventBus.emit({
1528
2027
  kind: "asset-metadata",
1529
2028
  asset: this.createAssetDownload(assetId, metadata, existing.data),
1530
2029
  by: "local"
@@ -1557,7 +2056,7 @@ var LoroRepo = class {
1557
2056
  data: storedBytes.slice()
1558
2057
  });
1559
2058
  this.rememberAsset(metadata, storedBytes);
1560
- for (const assets of this.docAssets.values()) if (assets.has(assetId)) assets.set(assetId, metadata);
2059
+ this.updateDocAssetMetadata(assetId, metadata);
1561
2060
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
1562
2061
  created = true;
1563
2062
  }
@@ -1573,26 +2072,20 @@ var LoroRepo = class {
1573
2072
  assetId
1574
2073
  ], true);
1575
2074
  await this.persistMeta();
1576
- this.emit({
2075
+ this.eventBus.emit({
1577
2076
  kind: "asset-link",
1578
2077
  docId,
1579
2078
  assetId,
1580
2079
  by: "local"
1581
2080
  });
1582
- if (created) this.emit({
2081
+ if (created) this.eventBus.emit({
1583
2082
  kind: "asset-metadata",
1584
2083
  asset: this.createAssetDownload(assetId, metadata, storedBytes ?? bytes),
1585
2084
  by: "local"
1586
2085
  });
1587
2086
  return assetId;
1588
2087
  }
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
2088
  async unlinkAsset(docId, assetId) {
1595
- await this.ready();
1596
2089
  const mapping = this.docAssets.get(docId);
1597
2090
  if (!mapping || !mapping.has(assetId)) return;
1598
2091
  mapping.delete(assetId);
@@ -1617,7 +2110,7 @@ var LoroRepo = class {
1617
2110
  }
1618
2111
  }
1619
2112
  await this.persistMeta();
1620
- this.emit({
2113
+ this.eventBus.emit({
1621
2114
  kind: "asset-unlink",
1622
2115
  docId,
1623
2116
  assetId,
@@ -1625,7 +2118,6 @@ var LoroRepo = class {
1625
2118
  });
1626
2119
  }
1627
2120
  async listAssets(docId) {
1628
- await this.ready();
1629
2121
  const mapping = this.docAssets.get(docId);
1630
2122
  if (!mapping) return [];
1631
2123
  return Array.from(mapping.values()).map((asset) => ({ ...asset }));
@@ -1633,21 +2125,236 @@ var LoroRepo = class {
1633
2125
  async ensureAsset(assetId) {
1634
2126
  return this.fetchAsset(assetId);
1635
2127
  }
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
- };
1650
- }
2128
+ async fetchAsset(assetId) {
2129
+ const { metadata, bytes } = await this.materializeAsset(assetId);
2130
+ return this.createAssetDownload(assetId, metadata, bytes);
2131
+ }
2132
+ async gcAssets(options = {}) {
2133
+ const { minKeepMs = 0 } = options;
2134
+ const now = Date.now();
2135
+ let removed = 0;
2136
+ for (const [assetId, orphan] of Array.from(this.orphanedAssets.entries())) {
2137
+ if (now - orphan.deletedAt < minKeepMs) continue;
2138
+ this.orphanedAssets.delete(assetId);
2139
+ if (this.storage?.deleteAsset) try {
2140
+ await this.storage.deleteAsset(assetId);
2141
+ } catch (error) {
2142
+ logAsyncError(`asset ${assetId} delete`)(error);
2143
+ }
2144
+ removed += 1;
2145
+ }
2146
+ return removed;
2147
+ }
2148
+ refreshDocAssetsEntry(docId, by) {
2149
+ const mapping = this.readDocAssetsFromFlock(docId);
2150
+ const previous = this.docAssets.get(docId);
2151
+ if (!mapping.size) {
2152
+ if (previous?.size) {
2153
+ this.docAssets.delete(docId);
2154
+ for (const assetId of previous.keys()) {
2155
+ this.removeDocAssetReference(assetId, docId);
2156
+ this.eventBus.emit({
2157
+ kind: "asset-unlink",
2158
+ docId,
2159
+ assetId,
2160
+ by
2161
+ });
2162
+ }
2163
+ }
2164
+ return;
2165
+ }
2166
+ this.docAssets.set(docId, mapping);
2167
+ const removed = [];
2168
+ if (previous) {
2169
+ for (const assetId of previous.keys()) if (!mapping.has(assetId)) removed.push(assetId);
2170
+ }
2171
+ for (const assetId of removed) {
2172
+ this.removeDocAssetReference(assetId, docId);
2173
+ this.eventBus.emit({
2174
+ kind: "asset-unlink",
2175
+ docId,
2176
+ assetId,
2177
+ by
2178
+ });
2179
+ }
2180
+ for (const assetId of mapping.keys()) {
2181
+ const isNew = !previous || !previous.has(assetId);
2182
+ this.addDocReference(assetId, docId);
2183
+ if (isNew) this.eventBus.emit({
2184
+ kind: "asset-link",
2185
+ docId,
2186
+ assetId,
2187
+ by
2188
+ });
2189
+ }
2190
+ }
2191
+ refreshAssetMetadataEntry(assetId, by) {
2192
+ const previous = this.assets.get(assetId);
2193
+ const metadata = assetMetaFromJson(this.metaFlock.get(["a", assetId]));
2194
+ if (!metadata) {
2195
+ this.handleAssetRemoval(assetId, by);
2196
+ return;
2197
+ }
2198
+ const existingData = previous?.data;
2199
+ this.rememberAsset(metadata, existingData);
2200
+ this.updateDocAssetMetadata(assetId, cloneRepoAssetMetadata(metadata));
2201
+ if (!previous || !assetMetadataEqual(previous.metadata, metadata)) this.eventBus.emit({
2202
+ kind: "asset-metadata",
2203
+ asset: this.createAssetDownload(assetId, metadata, existingData),
2204
+ by
2205
+ });
2206
+ }
2207
+ hydrateFromFlock(by) {
2208
+ const prevDocAssets = new Map(this.docAssets);
2209
+ const prevAssets = new Map(this.assets);
2210
+ const nextAssets = /* @__PURE__ */ new Map();
2211
+ const assetRows = this.metaFlock.scan({ prefix: ["a"] });
2212
+ for (const row of assetRows) {
2213
+ if (!Array.isArray(row.key) || row.key.length < 2) continue;
2214
+ const assetId = row.key[1];
2215
+ if (typeof assetId !== "string") continue;
2216
+ const metadata = assetMetaFromJson(row.value);
2217
+ if (!metadata) continue;
2218
+ const existing = this.assets.get(assetId);
2219
+ nextAssets.set(assetId, {
2220
+ metadata,
2221
+ data: existing?.data
2222
+ });
2223
+ }
2224
+ const nextDocAssets = /* @__PURE__ */ new Map();
2225
+ const linkRows = this.metaFlock.scan({ prefix: ["ld"] });
2226
+ for (const row of linkRows) {
2227
+ if (!Array.isArray(row.key) || row.key.length < 3) continue;
2228
+ const docId = row.key[1];
2229
+ const assetId = row.key[2];
2230
+ if (typeof docId !== "string" || typeof assetId !== "string") continue;
2231
+ const metadata = nextAssets.get(assetId)?.metadata;
2232
+ if (!metadata) continue;
2233
+ const mapping = nextDocAssets.get(docId) ?? /* @__PURE__ */ new Map();
2234
+ mapping.set(assetId, metadata);
2235
+ nextDocAssets.set(docId, mapping);
2236
+ }
2237
+ const removedAssets = [];
2238
+ for (const [assetId, record] of prevAssets) if (!nextAssets.has(assetId)) removedAssets.push([assetId, record]);
2239
+ if (removedAssets.length > 0) {
2240
+ const now = Date.now();
2241
+ for (const [assetId, record] of removedAssets) {
2242
+ const deletedAt = this.orphanedAssets.get(assetId)?.deletedAt ?? now;
2243
+ this.orphanedAssets.set(assetId, {
2244
+ metadata: record.metadata,
2245
+ deletedAt
2246
+ });
2247
+ }
2248
+ }
2249
+ this.docAssets.clear();
2250
+ for (const [docId, assets] of nextDocAssets) this.docAssets.set(docId, assets);
2251
+ this.assetToDocRefs.clear();
2252
+ for (const [docId, assets] of nextDocAssets) for (const assetId of assets.keys()) {
2253
+ const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
2254
+ refs.add(docId);
2255
+ this.assetToDocRefs.set(assetId, refs);
2256
+ }
2257
+ this.assets.clear();
2258
+ for (const record of nextAssets.values()) this.rememberAsset(record.metadata, record.data);
2259
+ for (const [assetId, record] of nextAssets) {
2260
+ const previous = prevAssets.get(assetId)?.metadata;
2261
+ if (!assetMetadataEqual(previous, record.metadata)) this.eventBus.emit({
2262
+ kind: "asset-metadata",
2263
+ asset: this.createAssetDownload(assetId, record.metadata, record.data),
2264
+ by
2265
+ });
2266
+ }
2267
+ for (const [docId, assets] of nextDocAssets) {
2268
+ const previous = prevDocAssets.get(docId);
2269
+ for (const assetId of assets.keys()) if (!previous || !previous.has(assetId)) this.eventBus.emit({
2270
+ kind: "asset-link",
2271
+ docId,
2272
+ assetId,
2273
+ by
2274
+ });
2275
+ }
2276
+ for (const [docId, assets] of prevDocAssets) {
2277
+ const current = nextDocAssets.get(docId);
2278
+ for (const assetId of assets.keys()) if (!current || !current.has(assetId)) this.eventBus.emit({
2279
+ kind: "asset-unlink",
2280
+ docId,
2281
+ assetId,
2282
+ by
2283
+ });
2284
+ }
2285
+ }
2286
+ clear() {
2287
+ this.docAssets.clear();
2288
+ this.assets.clear();
2289
+ this.orphanedAssets.clear();
2290
+ this.assetToDocRefs.clear();
2291
+ }
2292
+ readDocAssetsFromFlock(docId) {
2293
+ const rows = this.metaFlock.scan({ prefix: ["ld", docId] });
2294
+ const mapping = /* @__PURE__ */ new Map();
2295
+ for (const row of rows) {
2296
+ if (!Array.isArray(row.key) || row.key.length < 3) continue;
2297
+ const assetId = row.key[2];
2298
+ if (typeof assetId !== "string") continue;
2299
+ if (!(row.value !== void 0 && row.value !== null && row.value !== false)) continue;
2300
+ let metadata = this.assets.get(assetId)?.metadata;
2301
+ if (!metadata) {
2302
+ metadata = this.readAssetMetadataFromFlock(assetId);
2303
+ if (!metadata) continue;
2304
+ this.rememberAsset(metadata);
2305
+ }
2306
+ mapping.set(assetId, cloneRepoAssetMetadata(metadata));
2307
+ }
2308
+ return mapping;
2309
+ }
2310
+ readAssetMetadataFromFlock(assetId) {
2311
+ return assetMetaFromJson(this.metaFlock.get(["a", assetId]));
2312
+ }
2313
+ handleAssetRemoval(assetId, by) {
2314
+ const record = this.assets.get(assetId);
2315
+ if (!record) return;
2316
+ this.assets.delete(assetId);
2317
+ this.markAssetAsOrphan(assetId, record.metadata);
2318
+ const refs = this.assetToDocRefs.get(assetId);
2319
+ if (refs) {
2320
+ this.assetToDocRefs.delete(assetId);
2321
+ for (const docId of refs) {
2322
+ const assets = this.docAssets.get(docId);
2323
+ if (assets?.delete(assetId) && assets.size === 0) this.docAssets.delete(docId);
2324
+ this.eventBus.emit({
2325
+ kind: "asset-unlink",
2326
+ docId,
2327
+ assetId,
2328
+ by
2329
+ });
2330
+ }
2331
+ return;
2332
+ }
2333
+ for (const [docId, assets] of this.docAssets) if (assets.delete(assetId)) {
2334
+ if (assets.size === 0) this.docAssets.delete(docId);
2335
+ this.eventBus.emit({
2336
+ kind: "asset-unlink",
2337
+ docId,
2338
+ assetId,
2339
+ by
2340
+ });
2341
+ }
2342
+ }
2343
+ createAssetDownload(assetId, metadata, initialBytes) {
2344
+ let cached = initialBytes ? initialBytes.slice() : void 0;
2345
+ return {
2346
+ assetId,
2347
+ size: metadata.size,
2348
+ createdAt: metadata.createdAt,
2349
+ mime: metadata.mime,
2350
+ policy: metadata.policy,
2351
+ tag: metadata.tag,
2352
+ content: async () => {
2353
+ if (!cached) cached = (await this.materializeAsset(assetId)).bytes.slice();
2354
+ return toReadableStream(cached.slice());
2355
+ }
2356
+ };
2357
+ }
1651
2358
  async materializeAsset(assetId) {
1652
2359
  let record = this.assets.get(assetId);
1653
2360
  if (record?.data) return {
@@ -1712,147 +2419,77 @@ var LoroRepo = class {
1712
2419
  };
1713
2420
  }
1714
2421
  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;
2422
+ const refs = this.assetToDocRefs.get(assetId);
2423
+ if (!refs) return;
2424
+ for (const docId of refs) {
2425
+ const assets = this.docAssets.get(docId);
2426
+ if (assets) assets.set(assetId, metadata);
1731
2427
  }
1732
- return removed;
1733
2428
  }
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");
2429
+ rememberAsset(metadata, bytes) {
2430
+ const data = bytes ? bytes.slice() : this.assets.get(metadata.assetId)?.data;
2431
+ this.assets.set(metadata.assetId, {
2432
+ metadata,
2433
+ data
2434
+ });
2435
+ this.orphanedAssets.delete(metadata.assetId);
1740
2436
  }
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
- }
1755
- }
1756
- const created = await this.docFactory(docId);
1757
- this.registerDoc(docId, created);
1758
- return created;
2437
+ addDocReference(assetId, docId) {
2438
+ const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
2439
+ refs.add(docId);
2440
+ this.assetToDocRefs.set(assetId, refs);
1759
2441
  }
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;
2442
+ removeDocAssetReference(assetId, docId) {
2443
+ const refs = this.assetToDocRefs.get(assetId);
2444
+ if (!refs) return;
2445
+ refs.delete(docId);
2446
+ if (refs.size === 0) {
2447
+ this.assetToDocRefs.delete(assetId);
2448
+ this.markAssetAsOrphan(assetId);
2449
+ }
1765
2450
  }
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
2451
+ markAssetAsOrphan(assetId, metadataOverride) {
2452
+ const metadata = metadataOverride ?? this.assets.get(assetId)?.metadata;
2453
+ if (!metadata) return;
2454
+ const deletedAt = this.orphanedAssets.get(assetId)?.deletedAt ?? Date.now();
2455
+ this.orphanedAssets.set(assetId, {
2456
+ metadata,
2457
+ deletedAt
1779
2458
  });
1780
2459
  }
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;
2460
+ getAssetMetadata(assetId) {
2461
+ const record = this.assets.get(assetId);
2462
+ if (record) return record.metadata;
2463
+ for (const assets of this.docAssets.values()) {
2464
+ const metadata = assets.get(assetId);
2465
+ if (metadata) return metadata;
1832
2466
  }
1833
2467
  }
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
- });
2468
+ get metaFlock() {
2469
+ return this.getMetaFlock();
1854
2470
  }
1855
- applyMetaFlockEvents(events, by) {
2471
+ };
2472
+
2473
+ //#endregion
2474
+ //#region src/internal/flock-hydrator.ts
2475
+ var FlockHydrator = class {
2476
+ getMetaFlock;
2477
+ metadataManager;
2478
+ assetManager;
2479
+ docManager;
2480
+ constructor(options) {
2481
+ this.getMetaFlock = options.getMetaFlock;
2482
+ this.metadataManager = options.metadataManager;
2483
+ this.assetManager = options.assetManager;
2484
+ this.docManager = options.docManager;
2485
+ }
2486
+ hydrateAll(by) {
2487
+ const nextMetadata = this.readAllDocMetadata();
2488
+ this.metadataManager.replaceAll(nextMetadata, by);
2489
+ this.assetManager.hydrateFromFlock(by);
2490
+ this.docManager.hydrateFrontierKeys();
2491
+ }
2492
+ applyEvents(events, by) {
1856
2493
  if (!events.length) return;
1857
2494
  const docMetadataIds = /* @__PURE__ */ new Set();
1858
2495
  const docAssetIds = /* @__PURE__ */ new Set();
@@ -1878,134 +2515,12 @@ var LoroRepo = class {
1878
2515
  if (typeof docId === "string") docFrontiersIds.add(docId);
1879
2516
  }
1880
2517
  }
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
- }
2518
+ for (const assetId of assetIds) this.assetManager.refreshAssetMetadataEntry(assetId, by);
2519
+ for (const docId of docMetadataIds) this.metadataManager.refreshFromFlock(docId, by);
2520
+ for (const docId of docAssetIds) this.assetManager.refreshDocAssetsEntry(docId, by);
2521
+ for (const docId of docFrontiersIds) this.docManager.refreshDocFrontierKeys(docId);
1976
2522
  }
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
- });
2004
- }
2005
- hydrateMetadataFromFlock(by) {
2006
- const prevMetadata = new Map(this.metadata);
2007
- const prevDocAssets = new Map(this.docAssets);
2008
- const prevAssets = new Map(this.assets);
2523
+ readAllDocMetadata() {
2009
2524
  const nextMetadata = /* @__PURE__ */ new Map();
2010
2525
  const metadataRows = this.metaFlock.scan({ prefix: ["m"] });
2011
2526
  for (const row of metadataRows) {
@@ -2036,137 +2551,407 @@ var LoroRepo = class {
2036
2551
  if (jsonValue === void 0) continue;
2037
2552
  docMeta[fieldKey] = jsonValue;
2038
2553
  }
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
2554
+ return nextMetadata;
2555
+ }
2556
+ get metaFlock() {
2557
+ return this.getMetaFlock();
2558
+ }
2559
+ };
2560
+
2561
+ //#endregion
2562
+ //#region src/internal/sync-runner.ts
2563
+ /**
2564
+ * Sync data between storage and transport layer
2565
+ */
2566
+ var SyncRunner = class {
2567
+ storage;
2568
+ transport;
2569
+ eventBus;
2570
+ docManager;
2571
+ metadataManager;
2572
+ assetManager;
2573
+ flockHydrator;
2574
+ getMetaFlock;
2575
+ replaceMetaFlock;
2576
+ persistMeta;
2577
+ readyPromise;
2578
+ metaRoomSubscription;
2579
+ unsubscribeMetaFlock;
2580
+ docSubscriptions = /* @__PURE__ */ new Map();
2581
+ constructor(options) {
2582
+ this.storage = options.storage;
2583
+ this.transport = options.transport;
2584
+ this.eventBus = options.eventBus;
2585
+ this.docManager = options.docManager;
2586
+ this.metadataManager = options.metadataManager;
2587
+ this.assetManager = options.assetManager;
2588
+ this.flockHydrator = options.flockHydrator;
2589
+ this.getMetaFlock = options.getMetaFlock;
2590
+ this.replaceMetaFlock = options.mergeFlock;
2591
+ this.persistMeta = options.persistMeta;
2592
+ }
2593
+ async ready() {
2594
+ if (!this.readyPromise) this.readyPromise = this.initialize();
2595
+ await this.readyPromise;
2596
+ }
2597
+ async sync(options = {}) {
2598
+ await this.ready();
2599
+ const { scope = "full", docIds } = options;
2600
+ if (!this.transport) return;
2601
+ if (!this.transport.isConnected()) await this.transport.connect();
2602
+ if (scope === "meta" || scope === "full") {
2603
+ this.eventBus.pushEventBy("sync");
2604
+ const recordedEvents = [];
2605
+ const unsubscribe = this.metaFlock.subscribe((batch) => {
2606
+ if (batch.source === "local") return;
2607
+ recordedEvents.push(...batch.events);
2051
2608
  });
2609
+ try {
2610
+ if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
2611
+ if (recordedEvents.length > 0) this.flockHydrator.applyEvents(recordedEvents, "sync");
2612
+ else this.flockHydrator.hydrateAll("sync");
2613
+ await this.persistMeta();
2614
+ } finally {
2615
+ unsubscribe();
2616
+ this.eventBus.popEventBy();
2617
+ }
2052
2618
  }
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);
2619
+ if (scope === "doc" || scope === "full") {
2620
+ const targets = docIds ?? this.metadataManager.getDocIds();
2621
+ for (const docId of targets) {
2622
+ const doc = await this.docManager.ensureDoc(docId);
2623
+ this.eventBus.pushEventBy("sync");
2624
+ try {
2625
+ if (!(await this.transport.syncDoc(docId, doc)).ok) throw new Error(`Document sync failed for ${docId}`);
2626
+ } finally {
2627
+ this.eventBus.popEventBy();
2628
+ }
2629
+ await this.docManager.persistDoc(docId, doc);
2630
+ await this.docManager.updateDocFrontiers(docId, doc, "sync");
2631
+ }
2076
2632
  }
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
- });
2633
+ }
2634
+ async joinMetaRoom(params) {
2635
+ await this.ready();
2636
+ if (!this.transport) throw new Error("Transport adapter not configured");
2637
+ if (!this.transport.isConnected()) await this.transport.connect();
2638
+ if (this.metaRoomSubscription) return this.metaRoomSubscription;
2639
+ this.ensureMetaLiveMonitor();
2640
+ const subscription = this.transport.joinMetaRoom(this.metaFlock, params);
2641
+ const wrapped = {
2642
+ unsubscribe: () => {
2643
+ subscription.unsubscribe();
2644
+ if (this.metaRoomSubscription === wrapped) this.metaRoomSubscription = void 0;
2645
+ if (this.unsubscribeMetaFlock) {
2646
+ this.unsubscribeMetaFlock();
2647
+ this.unsubscribeMetaFlock = void 0;
2648
+ }
2649
+ },
2650
+ firstSyncedWithRemote: subscription.firstSyncedWithRemote,
2651
+ get connected() {
2652
+ return subscription.connected;
2087
2653
  }
2654
+ };
2655
+ this.metaRoomSubscription = wrapped;
2656
+ subscription.firstSyncedWithRemote.then(async () => {
2657
+ const by = this.eventBus.resolveEventBy("live");
2658
+ this.flockHydrator.hydrateAll(by);
2659
+ await this.persistMeta();
2660
+ }).catch(logAsyncError("meta room first sync"));
2661
+ return wrapped;
2662
+ }
2663
+ async joinDocRoom(docId, params) {
2664
+ await this.ready();
2665
+ if (!this.transport) throw new Error("Transport adapter not configured");
2666
+ if (!this.transport.isConnected()) await this.transport.connect();
2667
+ const existing = this.docSubscriptions.get(docId);
2668
+ if (existing) return existing;
2669
+ const doc = await this.docManager.ensureDoc(docId);
2670
+ const subscription = this.transport.joinDocRoom(docId, doc, params);
2671
+ const wrapped = {
2672
+ unsubscribe: () => {
2673
+ subscription.unsubscribe();
2674
+ if (this.docSubscriptions.get(docId) === wrapped) this.docSubscriptions.delete(docId);
2675
+ },
2676
+ firstSyncedWithRemote: subscription.firstSyncedWithRemote,
2677
+ get connected() {
2678
+ return subscription.connected;
2679
+ }
2680
+ };
2681
+ this.docSubscriptions.set(docId, wrapped);
2682
+ subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
2683
+ return wrapped;
2684
+ }
2685
+ async destroy() {
2686
+ await this.docManager.close();
2687
+ this.metaRoomSubscription?.unsubscribe();
2688
+ this.metaRoomSubscription = void 0;
2689
+ for (const sub of this.docSubscriptions.values()) sub.unsubscribe();
2690
+ this.docSubscriptions.clear();
2691
+ if (this.unsubscribeMetaFlock) {
2692
+ this.unsubscribeMetaFlock();
2693
+ this.unsubscribeMetaFlock = void 0;
2088
2694
  }
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);
2695
+ this.eventBus.clear();
2696
+ this.metadataManager.clear();
2697
+ this.assetManager.clear();
2698
+ this.readyPromise = void 0;
2699
+ await this.transport?.close();
2700
+ }
2701
+ async initialize() {
2702
+ if (this.storage) {
2703
+ const snapshot = await this.storage.loadMeta();
2704
+ if (snapshot) this.replaceMetaFlock(snapshot);
2098
2705
  }
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
2706
+ this.flockHydrator.hydrateAll("sync");
2707
+ }
2708
+ ensureMetaLiveMonitor() {
2709
+ if (this.unsubscribeMetaFlock) return;
2710
+ this.unsubscribeMetaFlock = this.metaFlock.subscribe((batch) => {
2711
+ if (batch.source === "local") return;
2712
+ const by = this.eventBus.resolveEventBy("live");
2713
+ (async () => {
2714
+ this.flockHydrator.applyEvents(batch.events, by);
2715
+ await this.persistMeta();
2716
+ })().catch(logAsyncError("meta live monitor sync"));
2717
+ });
2718
+ }
2719
+ get metaFlock() {
2720
+ return this.getMetaFlock();
2721
+ }
2722
+ };
2723
+
2724
+ //#endregion
2725
+ //#region src/internal/repo-state.ts
2726
+ function createRepoState() {
2727
+ return {
2728
+ metadata: /* @__PURE__ */ new Map(),
2729
+ docAssets: /* @__PURE__ */ new Map(),
2730
+ assets: /* @__PURE__ */ new Map(),
2731
+ orphanedAssets: /* @__PURE__ */ new Map(),
2732
+ assetToDocRefs: /* @__PURE__ */ new Map(),
2733
+ docFrontierKeys: /* @__PURE__ */ new Map()
2734
+ };
2735
+ }
2736
+
2737
+ //#endregion
2738
+ //#region src/index.ts
2739
+ const textEncoder = new TextEncoder();
2740
+ const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
2741
+ var LoroRepo = class LoroRepo {
2742
+ options;
2743
+ _destroyed = false;
2744
+ transport;
2745
+ storage;
2746
+ metaFlock = new __loro_dev_flock.Flock();
2747
+ eventBus;
2748
+ docManager;
2749
+ metadataManager;
2750
+ assetManager;
2751
+ assetTransport;
2752
+ flockHydrator;
2753
+ state;
2754
+ syncRunner;
2755
+ constructor(options) {
2756
+ this.options = options;
2757
+ this.transport = options.transportAdapter;
2758
+ this.storage = options.storageAdapter;
2759
+ this.assetTransport = options.assetTransportAdapter;
2760
+ this.eventBus = new RepoEventBus();
2761
+ this.state = createRepoState();
2762
+ const configuredDebounce = options.docFrontierDebounceMs;
2763
+ const docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
2764
+ this.docManager = new DocManager({
2765
+ storage: this.storage,
2766
+ docFrontierDebounceMs,
2767
+ getMetaFlock: () => this.metaFlock,
2768
+ eventBus: this.eventBus,
2769
+ persistMeta: () => this.persistMeta(),
2770
+ state: this.state
2771
+ });
2772
+ this.metadataManager = new MetadataManager({
2773
+ getMetaFlock: () => this.metaFlock,
2774
+ eventBus: this.eventBus,
2775
+ persistMeta: () => this.persistMeta(),
2776
+ state: this.state
2777
+ });
2778
+ this.assetManager = new AssetManager({
2779
+ storage: this.storage,
2780
+ assetTransport: this.assetTransport,
2781
+ getMetaFlock: () => this.metaFlock,
2782
+ eventBus: this.eventBus,
2783
+ persistMeta: () => this.persistMeta(),
2784
+ state: this.state
2785
+ });
2786
+ this.flockHydrator = new FlockHydrator({
2787
+ getMetaFlock: () => this.metaFlock,
2788
+ metadataManager: this.metadataManager,
2789
+ assetManager: this.assetManager,
2790
+ docManager: this.docManager
2791
+ });
2792
+ this.syncRunner = new SyncRunner({
2793
+ storage: this.storage,
2794
+ transport: this.transport,
2795
+ eventBus: this.eventBus,
2796
+ docManager: this.docManager,
2797
+ metadataManager: this.metadataManager,
2798
+ assetManager: this.assetManager,
2799
+ flockHydrator: this.flockHydrator,
2800
+ getMetaFlock: () => this.metaFlock,
2801
+ mergeFlock: (snapshot) => {
2802
+ this.metaFlock.merge(snapshot);
2803
+ },
2804
+ persistMeta: () => this.persistMeta()
2805
+ });
2806
+ }
2807
+ static async create(options) {
2808
+ const repo = new LoroRepo(options);
2809
+ await repo.storage?.init?.();
2810
+ await repo.ready();
2811
+ return repo;
2812
+ }
2813
+ /**
2814
+ * Load meta from storage.
2815
+ *
2816
+ * You need to call this before all other operations to make the app functioning correctly.
2817
+ * Though we do that implicitly already
2818
+ */
2819
+ async ready() {
2820
+ await this.syncRunner.ready();
2821
+ }
2822
+ /**
2823
+ * Sync selected data via the transport adaptor
2824
+ * @param options
2825
+ */
2826
+ async sync(options = {}) {
2827
+ await this.syncRunner.sync(options);
2828
+ }
2829
+ /**
2830
+ * Start syncing the metadata (Flock) room. It will establish a realtime connection to the transport adaptor.
2831
+ * All changes on the room will be synced to the Flock, and all changes on the Flock will be synced to the room.
2832
+ * @param params
2833
+ * @returns
2834
+ */
2835
+ async joinMetaRoom(params) {
2836
+ return this.syncRunner.joinMetaRoom(params);
2837
+ }
2838
+ /**
2839
+ * Start syncing the given doc. It will establish a realtime connection to the transport adaptor.
2840
+ * All changes on the doc will be synced to the transport, and all changes on the transport will be synced to the doc.
2841
+ *
2842
+ * All the changes on the room will be reflected on the same doc you get from `repo.openCollaborativeDoc(docId)`
2843
+ * @param docId
2844
+ * @param params
2845
+ * @returns
2846
+ */
2847
+ async joinDocRoom(docId, params) {
2848
+ return this.syncRunner.joinDocRoom(docId, params);
2849
+ }
2850
+ /**
2851
+ * Opens a document that is automatically persisted to the configured storage adapter.
2852
+ *
2853
+ * - Edits are saved to storage (debounced).
2854
+ * - Frontiers are synced to the metadata (Flock).
2855
+ * - Realtime collaboration is NOT enabled by default; use `joinDocRoom` to connect.
2856
+ */
2857
+ async openPersistedDoc(docId) {
2858
+ return {
2859
+ doc: await this.docManager.openCollaborativeDoc(docId),
2860
+ syncOnce: () => {
2861
+ return this.sync({
2862
+ scope: "doc",
2863
+ docIds: [docId]
2113
2864
  });
2114
- continue;
2865
+ },
2866
+ joinRoom: (auth) => {
2867
+ return this.syncRunner.joinDocRoom(docId, { auth });
2115
2868
  }
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
- });
2149
- }
2869
+ };
2150
2870
  }
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;
2163
- }
2164
- return true;
2871
+ async upsertDocMeta(docId, patch) {
2872
+ await this.metadataManager.upsert(docId, patch);
2873
+ }
2874
+ async getDocMeta(docId) {
2875
+ return this.metadataManager.get(docId);
2876
+ }
2877
+ async listDoc(query) {
2878
+ return this.metadataManager.listDoc(query);
2879
+ }
2880
+ getMeta() {
2881
+ return this.metaFlock;
2882
+ }
2883
+ watch(listener, filter = {}) {
2884
+ return this.eventBus.watch(listener, filter);
2885
+ }
2886
+ /**
2887
+ * Opens a detached `LoroDoc` snapshot.
2888
+ *
2889
+ * - **No Persistence**: Edits to this document are NOT saved to storage.
2890
+ * - **No Sync**: This document does not participate in realtime updates.
2891
+ * - **Use Case**: Ideal for read-only history inspection, temporary drafts, or conflict resolution without affecting the main state.
2892
+ */
2893
+ async openDetachedDoc(docId) {
2894
+ return this.docManager.openDetachedDoc(docId);
2895
+ }
2896
+ /**
2897
+ * Explicitly unloads a document from memory.
2898
+ *
2899
+ * - **Persists Immediately**: Forces a save of the document's current state to storage.
2900
+ * - **Frees Memory**: Removes the document from the internal cache.
2901
+ * - **Note**: If the document is currently being synced (via `joinDocRoom`), you should also unsubscribe from the room to fully release resources.
2902
+ */
2903
+ async unloadDoc(docId) {
2904
+ await this.docManager.unloadDoc(docId);
2905
+ }
2906
+ async flush() {
2907
+ await this.docManager.flush();
2908
+ }
2909
+ async uploadAsset(params) {
2910
+ return this.assetManager.uploadAsset(params);
2911
+ }
2912
+ async linkAsset(docId, params) {
2913
+ return this.assetManager.linkAsset(docId, params);
2914
+ }
2915
+ async fetchAsset(assetId) {
2916
+ return this.assetManager.fetchAsset(assetId);
2917
+ }
2918
+ async unlinkAsset(docId, assetId) {
2919
+ await this.assetManager.unlinkAsset(docId, assetId);
2920
+ }
2921
+ async listAssets(docId) {
2922
+ return this.assetManager.listAssets(docId);
2923
+ }
2924
+ async ensureAsset(assetId) {
2925
+ return this.assetManager.ensureAsset(assetId);
2926
+ }
2927
+ async gcAssets(options = {}) {
2928
+ return this.assetManager.gcAssets(options);
2929
+ }
2930
+ async persistMeta() {
2931
+ if (!this.storage) return;
2932
+ const bundle = this.metaFlock.exportJson();
2933
+ const encoded = textEncoder.encode(JSON.stringify(bundle));
2934
+ await this.storage.save({
2935
+ type: "meta",
2936
+ update: encoded
2937
+ });
2938
+ }
2939
+ get destroyed() {
2940
+ return this._destroyed;
2941
+ }
2942
+ async destroy() {
2943
+ if (this._destroyed) return;
2944
+ this._destroyed = true;
2945
+ await this.syncRunner.destroy();
2946
+ this.assetTransport?.close?.();
2947
+ this.storage?.close?.();
2948
+ await this.transport?.close();
2165
2949
  }
2166
2950
  };
2167
2951
 
2168
2952
  //#endregion
2169
2953
  exports.BroadcastChannelTransportAdapter = BroadcastChannelTransportAdapter;
2954
+ exports.FileSystemStorageAdaptor = FileSystemStorageAdaptor;
2170
2955
  exports.IndexedDBStorageAdaptor = IndexedDBStorageAdaptor;
2171
2956
  exports.LoroRepo = LoroRepo;
2172
2957
  exports.WebSocketTransportAdapter = WebSocketTransportAdapter;