loro-repo 0.2.0 → 0.4.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
@@ -22,22 +22,70 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
22
 
23
23
  //#endregion
24
24
  let __loro_dev_flock = require("@loro-dev/flock");
25
- let loro_crdt = require("loro-crdt");
26
- let loro_adaptors = require("loro-adaptors");
25
+ __loro_dev_flock = __toESM(__loro_dev_flock);
26
+ let loro_adaptors_loro = require("loro-adaptors/loro");
27
+ loro_adaptors_loro = __toESM(loro_adaptors_loro);
27
28
  let loro_protocol = require("loro-protocol");
29
+ loro_protocol = __toESM(loro_protocol);
28
30
  let loro_websocket = require("loro-websocket");
31
+ loro_websocket = __toESM(loro_websocket);
32
+ let loro_crdt = require("loro-crdt");
33
+ loro_crdt = __toESM(loro_crdt);
34
+ let loro_adaptors_flock = require("loro-adaptors/flock");
35
+ loro_adaptors_flock = __toESM(loro_adaptors_flock);
29
36
  let node_fs = require("node:fs");
37
+ node_fs = __toESM(node_fs);
30
38
  let node_path = require("node:path");
31
39
  node_path = __toESM(node_path);
32
40
  let node_crypto = require("node:crypto");
41
+ node_crypto = __toESM(node_crypto);
33
42
 
34
43
  //#region src/loro-adaptor.ts
35
44
  function createRepoFlockAdaptorFromDoc(flock, config = {}) {
36
- return new loro_adaptors.FlockAdaptor(flock, config);
45
+ return new loro_adaptors_flock.FlockAdaptor(flock, config);
37
46
  }
38
47
 
48
+ //#endregion
49
+ //#region src/internal/debug.ts
50
+ const getEnv = () => {
51
+ if (typeof globalThis !== "object" || globalThis === null) return;
52
+ return globalThis.process?.env;
53
+ };
54
+ const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
55
+ const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
56
+ const wildcardTokens = new Set([
57
+ "*",
58
+ "1",
59
+ "true",
60
+ "all"
61
+ ]);
62
+ const namespaceSet = new Set(normalizedNamespaces);
63
+ const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
64
+ const isDebugEnabled = (namespace) => {
65
+ if (!namespaceSet.size) return false;
66
+ if (!namespace) return hasWildcard;
67
+ const normalized = namespace.toLowerCase();
68
+ if (hasWildcard) return true;
69
+ if (namespaceSet.has(normalized)) return true;
70
+ const [root] = normalized.split(":");
71
+ return namespaceSet.has(root);
72
+ };
73
+ const createDebugLogger = (namespace) => {
74
+ const normalized = namespace.toLowerCase();
75
+ return (...args) => {
76
+ if (!isDebugEnabled(normalized)) return;
77
+ const prefix = `[loro-repo:${namespace}]`;
78
+ if (args.length === 0) {
79
+ console.info(prefix);
80
+ return;
81
+ }
82
+ console.info(prefix, ...args);
83
+ };
84
+ };
85
+
39
86
  //#endregion
40
87
  //#region src/transport/websocket.ts
88
+ const debug = createDebugLogger("transport:websocket");
41
89
  function withTimeout(promise, timeoutMs) {
42
90
  if (!timeoutMs || timeoutMs <= 0) return promise;
43
91
  return new Promise((resolve, reject) => {
@@ -82,30 +130,51 @@ var WebSocketTransportAdapter = class {
82
130
  }
83
131
  async connect(_options) {
84
132
  const client = this.ensureClient();
85
- await client.connect();
86
- await client.waitConnected();
133
+ debug("connect requested", { status: client.getStatus() });
134
+ try {
135
+ await client.connect();
136
+ debug("client.connect resolved");
137
+ await client.waitConnected();
138
+ debug("client.waitConnected resolved", { status: client.getStatus() });
139
+ } catch (error) {
140
+ debug("connect failed", error);
141
+ throw error;
142
+ }
87
143
  }
88
144
  async close() {
145
+ debug("close requested", {
146
+ docSessions: this.docSessions.size,
147
+ metadataSession: Boolean(this.metadataSession)
148
+ });
89
149
  for (const [docId] of this.docSessions) await this.leaveDocSession(docId).catch(() => {});
90
150
  this.docSessions.clear();
91
151
  await this.teardownMetadataSession().catch(() => {});
92
152
  if (this.client) {
93
- this.client.destroy();
153
+ const client = this.client;
94
154
  this.client = void 0;
155
+ client.destroy();
156
+ debug("websocket client destroyed");
95
157
  }
158
+ debug("close completed");
96
159
  }
97
160
  isConnected() {
98
161
  return this.client?.getStatus() === "connected";
99
162
  }
100
163
  async syncMeta(flock, options) {
101
- if (!this.options.metadataRoomId) return { ok: true };
164
+ if (!this.options.metadataRoomId) {
165
+ debug("syncMeta skipped; metadata room not configured");
166
+ return { ok: true };
167
+ }
168
+ debug("syncMeta requested", { roomId: this.options.metadataRoomId });
102
169
  try {
103
170
  await withTimeout((await this.ensureMetadataSession(flock, {
104
171
  roomId: this.options.metadataRoomId,
105
172
  auth: this.options.metadataAuth
106
173
  })).firstSynced, options?.timeout);
174
+ debug("syncMeta completed", { roomId: this.options.metadataRoomId });
107
175
  return { ok: true };
108
- } catch {
176
+ } catch (error) {
177
+ debug("syncMeta failed", error);
109
178
  return { ok: false };
110
179
  }
111
180
  }
@@ -114,6 +183,10 @@ var WebSocketTransportAdapter = class {
114
183
  const roomId = normalizeRoomId(params?.roomId, fallback);
115
184
  if (!roomId) throw new Error("Metadata room id not configured");
116
185
  const auth = params?.auth ?? this.options.metadataAuth;
186
+ debug("joinMetaRoom requested", {
187
+ roomId,
188
+ hasAuth: Boolean(auth && auth.length)
189
+ });
117
190
  const ensure = this.ensureMetadataSession(flock, {
118
191
  roomId,
119
192
  auth
@@ -124,7 +197,14 @@ var WebSocketTransportAdapter = class {
124
197
  unsubscribe: () => {
125
198
  ensure.then((session) => {
126
199
  session.refCount = Math.max(0, session.refCount - 1);
127
- if (session.refCount === 0) this.teardownMetadataSession(session).catch(() => {});
200
+ debug("metadata session refCount decremented", {
201
+ roomId: session.roomId,
202
+ refCount: session.refCount
203
+ });
204
+ if (session.refCount === 0) {
205
+ debug("tearing down metadata session due to refCount=0", { roomId: session.roomId });
206
+ this.teardownMetadataSession(session).catch(() => {});
207
+ }
128
208
  });
129
209
  },
130
210
  firstSyncedWithRemote: firstSynced,
@@ -134,18 +214,37 @@ var WebSocketTransportAdapter = class {
134
214
  };
135
215
  ensure.then((session) => {
136
216
  session.refCount += 1;
217
+ debug("metadata session refCount incremented", {
218
+ roomId: session.roomId,
219
+ refCount: session.refCount
220
+ });
137
221
  });
138
222
  return subscription;
139
223
  }
140
224
  async syncDoc(docId, doc, options) {
225
+ debug("syncDoc requested", { docId });
141
226
  try {
142
- await withTimeout((await this.ensureDocSession(docId, doc, {})).firstSynced, options?.timeout);
227
+ const session = await this.ensureDocSession(docId, doc, {});
228
+ await withTimeout(session.firstSynced, options?.timeout);
229
+ debug("syncDoc completed", {
230
+ docId,
231
+ roomId: session.roomId
232
+ });
143
233
  return { ok: true };
144
- } catch {
234
+ } catch (error) {
235
+ debug("syncDoc failed", {
236
+ docId,
237
+ error
238
+ });
145
239
  return { ok: false };
146
240
  }
147
241
  }
148
242
  joinDocRoom(docId, doc, params) {
243
+ debug("joinDocRoom requested", {
244
+ docId,
245
+ roomParamType: params?.roomId ? typeof params.roomId === "string" ? "string" : "uint8array" : void 0,
246
+ hasAuthOverride: Boolean(params?.auth && params.auth.length)
247
+ });
149
248
  const ensure = this.ensureDocSession(docId, doc, params ?? {});
150
249
  const firstSynced = ensure.then((session) => session.firstSynced);
151
250
  const getConnected = () => this.isConnected();
@@ -153,6 +252,11 @@ var WebSocketTransportAdapter = class {
153
252
  unsubscribe: () => {
154
253
  ensure.then((session) => {
155
254
  session.refCount = Math.max(0, session.refCount - 1);
255
+ debug("doc session refCount decremented", {
256
+ docId,
257
+ roomId: session.roomId,
258
+ refCount: session.refCount
259
+ });
156
260
  if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
157
261
  });
158
262
  },
@@ -163,12 +267,24 @@ var WebSocketTransportAdapter = class {
163
267
  };
164
268
  ensure.then((session) => {
165
269
  session.refCount += 1;
270
+ debug("doc session refCount incremented", {
271
+ docId,
272
+ roomId: session.roomId,
273
+ refCount: session.refCount
274
+ });
166
275
  });
167
276
  return subscription;
168
277
  }
169
278
  ensureClient() {
170
- if (this.client) return this.client;
279
+ if (this.client) {
280
+ debug("reusing websocket client", { status: this.client.getStatus() });
281
+ return this.client;
282
+ }
171
283
  const { url, client: clientOptions } = this.options;
284
+ debug("creating websocket client", {
285
+ url,
286
+ clientOptionsKeys: clientOptions ? Object.keys(clientOptions) : []
287
+ });
172
288
  const client = new loro_websocket.LoroWebsocketClient({
173
289
  url,
174
290
  ...clientOptions
@@ -177,22 +293,49 @@ var WebSocketTransportAdapter = class {
177
293
  return client;
178
294
  }
179
295
  async ensureMetadataSession(flock, params) {
296
+ debug("ensureMetadataSession invoked", {
297
+ roomId: params.roomId,
298
+ hasAuth: Boolean(params.auth && params.auth.length)
299
+ });
180
300
  const client = this.ensureClient();
181
301
  await client.waitConnected();
182
- if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) return this.metadataSession;
183
- if (this.metadataSession) await this.teardownMetadataSession(this.metadataSession).catch(() => {});
302
+ debug("websocket client ready for metadata session", { status: client.getStatus() });
303
+ if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) {
304
+ debug("reusing metadata session", {
305
+ roomId: this.metadataSession.roomId,
306
+ refCount: this.metadataSession.refCount
307
+ });
308
+ return this.metadataSession;
309
+ }
310
+ if (this.metadataSession) {
311
+ debug("tearing down previous metadata session", { roomId: this.metadataSession.roomId });
312
+ await this.teardownMetadataSession(this.metadataSession).catch(() => {});
313
+ }
184
314
  const configuredType = this.options.metadataCrdtType;
185
315
  if (configuredType && configuredType !== loro_protocol.CrdtType.Flock) throw new Error(`metadataCrdtType must be ${loro_protocol.CrdtType.Flock} when syncing Flock metadata`);
186
316
  const adaptor = createRepoFlockAdaptorFromDoc(flock, this.options.metadataAdaptorConfig ?? {});
317
+ debug("joining metadata room", {
318
+ roomId: params.roomId,
319
+ hasAuth: Boolean(params.auth && params.auth.length)
320
+ });
187
321
  const room = await client.join({
188
322
  roomId: params.roomId,
189
323
  crdtAdaptor: adaptor,
190
324
  auth: params.auth
191
325
  });
326
+ const firstSynced = room.waitForReachingServerVersion();
327
+ firstSynced.then(() => {
328
+ debug("metadata session firstSynced resolved", { roomId: params.roomId });
329
+ }, (error) => {
330
+ debug("metadata session firstSynced rejected", {
331
+ roomId: params.roomId,
332
+ error
333
+ });
334
+ });
192
335
  const session = {
193
336
  adaptor,
194
337
  room,
195
- firstSynced: room.waitForReachingServerVersion(),
338
+ firstSynced,
196
339
  flock,
197
340
  roomId: params.roomId,
198
341
  auth: params.auth,
@@ -204,34 +347,83 @@ var WebSocketTransportAdapter = class {
204
347
  async teardownMetadataSession(session) {
205
348
  const target = session ?? this.metadataSession;
206
349
  if (!target) return;
350
+ debug("teardownMetadataSession invoked", { roomId: target.roomId });
207
351
  if (this.metadataSession === target) this.metadataSession = void 0;
208
352
  const { adaptor, room } = target;
209
353
  try {
210
354
  await room.leave();
211
- } catch {
355
+ debug("metadata room left", { roomId: target.roomId });
356
+ } catch (error) {
357
+ debug("metadata room leave failed; destroying", {
358
+ roomId: target.roomId,
359
+ error
360
+ });
212
361
  await room.destroy().catch(() => {});
213
362
  }
214
363
  adaptor.destroy();
364
+ debug("metadata session destroyed", { roomId: target.roomId });
215
365
  }
216
366
  async ensureDocSession(docId, doc, params) {
367
+ debug("ensureDocSession invoked", { docId });
217
368
  const client = this.ensureClient();
218
369
  await client.waitConnected();
370
+ debug("websocket client ready for doc session", {
371
+ docId,
372
+ status: client.getStatus()
373
+ });
219
374
  const existing = this.docSessions.get(docId);
220
375
  const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
221
376
  const roomId = normalizeRoomId(params.roomId, derivedRoomId);
222
377
  const auth = params.auth ?? this.options.docAuth?.(docId);
223
- if (existing && existing.doc === doc && existing.roomId === roomId) return existing;
224
- if (existing) await this.leaveDocSession(docId).catch(() => {});
225
- const adaptor = new loro_adaptors.LoroAdaptor(doc);
378
+ debug("doc session params resolved", {
379
+ docId,
380
+ roomId,
381
+ hasAuth: Boolean(auth && auth.length)
382
+ });
383
+ if (existing && existing.doc === doc && existing.roomId === roomId) {
384
+ debug("reusing doc session", {
385
+ docId,
386
+ roomId,
387
+ refCount: existing.refCount
388
+ });
389
+ return existing;
390
+ }
391
+ if (existing) {
392
+ debug("doc session mismatch; leaving existing session", {
393
+ docId,
394
+ previousRoomId: existing.roomId,
395
+ nextRoomId: roomId
396
+ });
397
+ await this.leaveDocSession(docId).catch(() => {});
398
+ }
399
+ const adaptor = new loro_adaptors_loro.LoroAdaptor(doc);
400
+ debug("joining doc room", {
401
+ docId,
402
+ roomId,
403
+ hasAuth: Boolean(auth && auth.length)
404
+ });
226
405
  const room = await client.join({
227
406
  roomId,
228
407
  crdtAdaptor: adaptor,
229
408
  auth
230
409
  });
410
+ const firstSynced = room.waitForReachingServerVersion();
411
+ firstSynced.then(() => {
412
+ debug("doc session firstSynced resolved", {
413
+ docId,
414
+ roomId
415
+ });
416
+ }, (error) => {
417
+ debug("doc session firstSynced rejected", {
418
+ docId,
419
+ roomId,
420
+ error
421
+ });
422
+ });
231
423
  const session = {
232
424
  adaptor,
233
425
  room,
234
- firstSynced: room.waitForReachingServerVersion(),
426
+ firstSynced,
235
427
  doc,
236
428
  roomId,
237
429
  refCount: 0
@@ -241,14 +433,34 @@ var WebSocketTransportAdapter = class {
241
433
  }
242
434
  async leaveDocSession(docId) {
243
435
  const session = this.docSessions.get(docId);
244
- if (!session) return;
436
+ if (!session) {
437
+ debug("leaveDocSession invoked but no session found", { docId });
438
+ return;
439
+ }
245
440
  this.docSessions.delete(docId);
441
+ debug("leaving doc session", {
442
+ docId,
443
+ roomId: session.roomId
444
+ });
246
445
  try {
247
446
  await session.room.leave();
248
- } catch {
447
+ debug("doc room left", {
448
+ docId,
449
+ roomId: session.roomId
450
+ });
451
+ } catch (error) {
452
+ debug("doc room leave failed; destroying", {
453
+ docId,
454
+ roomId: session.roomId,
455
+ error
456
+ });
249
457
  await session.room.destroy().catch(() => {});
250
458
  }
251
459
  session.adaptor.destroy();
460
+ debug("doc session destroyed", {
461
+ docId,
462
+ roomId: session.roomId
463
+ });
252
464
  }
253
465
  };
254
466
 
@@ -953,24 +1165,6 @@ var RepoEventBus = class {
953
1165
  }
954
1166
  };
955
1167
 
956
- //#endregion
957
- //#region src/internal/doc-handle.ts
958
- var RepoDocHandleImpl = class {
959
- doc;
960
- whenSyncedWithRemote;
961
- docId;
962
- onClose;
963
- constructor(docId, doc, whenSyncedWithRemote, onClose) {
964
- this.docId = docId;
965
- this.doc = doc;
966
- this.whenSyncedWithRemote = whenSyncedWithRemote;
967
- this.onClose = onClose;
968
- }
969
- async close() {
970
- await this.onClose(this.docId, this.doc);
971
- }
972
- };
973
-
974
1168
  //#endregion
975
1169
  //#region src/utils.ts
976
1170
  async function streamToUint8Array(stream) {
@@ -1133,37 +1327,24 @@ function toReadableStream(bytes) {
1133
1327
  controller.close();
1134
1328
  } });
1135
1329
  }
1136
- function emptyFrontiers() {
1137
- return [];
1138
- }
1139
- function getDocFrontiers(doc) {
1140
- const candidate = doc;
1141
- if (typeof candidate.frontiers === "function") {
1142
- const result = candidate.frontiers();
1143
- if (result) return result;
1144
- }
1145
- return emptyFrontiers();
1146
- }
1147
- function versionVectorToJson(vv) {
1148
- const map = vv.toJSON();
1149
- const record = {};
1150
- if (map instanceof Map) {
1151
- const entries = Array.from(map.entries()).sort(([a], [b]) => String(a).localeCompare(String(b)));
1152
- for (const [peer, counter] of entries) {
1153
- if (typeof counter !== "number" || !Number.isFinite(counter)) continue;
1154
- const key = typeof peer === "string" ? peer : JSON.stringify(peer);
1155
- record[key] = counter;
1156
- }
1157
- }
1158
- return record;
1159
- }
1160
- function canonicalizeVersionVector(vv) {
1161
- const json = versionVectorToJson(vv);
1330
+ function canonicalizeFrontiers(frontiers) {
1331
+ const json = [...frontiers].sort((a, b) => {
1332
+ if (a.peer < b.peer) return -1;
1333
+ if (a.peer > b.peer) return 1;
1334
+ return a.counter - b.counter;
1335
+ }).map((f) => ({
1336
+ peer: f.peer,
1337
+ counter: f.counter
1338
+ }));
1162
1339
  return {
1163
1340
  json,
1164
1341
  key: stableStringify(json)
1165
1342
  };
1166
1343
  }
1344
+ function includesFrontiers(vv, frontiers) {
1345
+ for (const { peer, counter } of frontiers) if ((vv.get(peer) ?? 0) <= counter) return false;
1346
+ return true;
1347
+ }
1167
1348
  function matchesQuery(docId, _metadata, query) {
1168
1349
  if (!query) return true;
1169
1350
  if (query.prefix && !docId.startsWith(query.prefix)) return false;
@@ -1185,14 +1366,12 @@ function logAsyncError(context) {
1185
1366
  //#region src/internal/doc-manager.ts
1186
1367
  var DocManager = class {
1187
1368
  storage;
1188
- docFactory;
1189
1369
  docFrontierDebounceMs;
1190
1370
  getMetaFlock;
1191
1371
  eventBus;
1192
1372
  persistMeta;
1193
1373
  state;
1194
1374
  docs = /* @__PURE__ */ new Map();
1195
- docRefs = /* @__PURE__ */ new Map();
1196
1375
  docSubscriptions = /* @__PURE__ */ new Map();
1197
1376
  docFrontierUpdates = /* @__PURE__ */ new Map();
1198
1377
  docPersistedVersions = /* @__PURE__ */ new Map();
@@ -1201,21 +1380,17 @@ var DocManager = class {
1201
1380
  }
1202
1381
  constructor(options) {
1203
1382
  this.storage = options.storage;
1204
- this.docFactory = options.docFactory;
1205
1383
  this.docFrontierDebounceMs = options.docFrontierDebounceMs;
1206
1384
  this.getMetaFlock = options.getMetaFlock;
1207
1385
  this.eventBus = options.eventBus;
1208
1386
  this.persistMeta = options.persistMeta;
1209
1387
  this.state = options.state;
1210
1388
  }
1211
- async openCollaborativeDoc(docId, whenSyncedWithRemote) {
1212
- const doc = await this.ensureDoc(docId);
1213
- const refs = this.docRefs.get(docId) ?? 0;
1214
- this.docRefs.set(docId, refs + 1);
1215
- return new RepoDocHandleImpl(docId, doc, whenSyncedWithRemote, async () => this.onDocHandleClose(docId, doc));
1389
+ async openCollaborativeDoc(docId) {
1390
+ return await this.ensureDoc(docId);
1216
1391
  }
1217
1392
  async openDetachedDoc(docId) {
1218
- return new RepoDocHandleImpl(docId, await this.materializeDetachedDoc(docId), Promise.resolve(), async () => {});
1393
+ return await this.materializeDetachedDoc(docId);
1219
1394
  }
1220
1395
  async ensureDoc(docId) {
1221
1396
  const cached = this.docs.get(docId);
@@ -1231,7 +1406,7 @@ var DocManager = class {
1231
1406
  return stored;
1232
1407
  }
1233
1408
  }
1234
- const created = await this.docFactory(docId);
1409
+ const created = new loro_crdt.LoroDoc();
1235
1410
  this.registerDoc(docId, created);
1236
1411
  return created;
1237
1412
  }
@@ -1257,27 +1432,42 @@ var DocManager = class {
1257
1432
  }
1258
1433
  }
1259
1434
  async updateDocFrontiers(docId, doc, defaultBy) {
1260
- const { json, key } = canonicalizeVersionVector(doc.version());
1435
+ const frontiers = doc.oplogFrontiers();
1436
+ const { json, key } = canonicalizeFrontiers(frontiers);
1261
1437
  const existingKeys = this.docFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
1262
1438
  let mutated = false;
1263
- if (existingKeys.size !== 1 || !existingKeys.has(key)) {
1264
- const metaFlock = this.metaFlock;
1265
- for (const entry of existingKeys) metaFlock.delete([
1266
- "f",
1267
- docId,
1268
- entry
1269
- ]);
1439
+ const metaFlock = this.metaFlock;
1440
+ const vv = doc.version();
1441
+ for (const entry of existingKeys) {
1442
+ if (entry === key) continue;
1443
+ let oldFrontiers;
1444
+ try {
1445
+ oldFrontiers = JSON.parse(entry);
1446
+ } catch {
1447
+ continue;
1448
+ }
1449
+ if (includesFrontiers(vv, oldFrontiers)) {
1450
+ metaFlock.delete([
1451
+ "f",
1452
+ docId,
1453
+ entry
1454
+ ]);
1455
+ mutated = true;
1456
+ }
1457
+ }
1458
+ if (!existingKeys.has(key)) {
1270
1459
  metaFlock.put([
1271
1460
  "f",
1272
1461
  docId,
1273
1462
  key
1274
1463
  ], json);
1275
- this.docFrontierKeys.set(docId, new Set([key]));
1276
1464
  mutated = true;
1277
1465
  }
1278
- if (mutated) await this.persistMeta();
1466
+ if (mutated) {
1467
+ this.refreshDocFrontierKeys(docId);
1468
+ await this.persistMeta();
1469
+ }
1279
1470
  const by = this.eventBus.resolveEventBy(defaultBy);
1280
- const frontiers = getDocFrontiers(doc);
1281
1471
  this.eventBus.emit({
1282
1472
  kind: "doc-frontiers",
1283
1473
  docId,
@@ -1298,20 +1488,33 @@ var DocManager = class {
1298
1488
  }
1299
1489
  return true;
1300
1490
  }
1491
+ async unloadDoc(docId) {
1492
+ const doc = this.docs.get(docId);
1493
+ if (!doc) return;
1494
+ await this.flushScheduledDocFrontierUpdate(docId);
1495
+ await this.persistDocUpdate(docId, doc);
1496
+ await this.updateDocFrontiers(docId, doc, "local");
1497
+ this.docSubscriptions.get(docId)?.();
1498
+ this.docSubscriptions.delete(docId);
1499
+ this.docs.delete(docId);
1500
+ this.docPersistedVersions.delete(docId);
1501
+ }
1502
+ async flush() {
1503
+ const promises = [];
1504
+ for (const [docId, doc] of this.docs) promises.push((async () => {
1505
+ await this.persistDocUpdate(docId, doc);
1506
+ await this.flushScheduledDocFrontierUpdate(docId);
1507
+ })());
1508
+ await Promise.all(promises);
1509
+ }
1301
1510
  async close() {
1511
+ await this.flush();
1302
1512
  for (const unsubscribe of this.docSubscriptions.values()) try {
1303
1513
  unsubscribe();
1304
1514
  } catch {}
1305
1515
  this.docSubscriptions.clear();
1306
- const pendingDocIds = Array.from(this.docFrontierUpdates.keys());
1307
- for (const docId of pendingDocIds) try {
1308
- await this.flushScheduledDocFrontierUpdate(docId);
1309
- } catch (error) {
1310
- logAsyncError(`doc ${docId} frontier flush on close`)(error);
1311
- }
1312
1516
  this.docFrontierUpdates.clear();
1313
1517
  this.docs.clear();
1314
- this.docRefs.clear();
1315
1518
  this.docPersistedVersions.clear();
1316
1519
  this.docFrontierKeys.clear();
1317
1520
  }
@@ -1335,6 +1538,7 @@ var DocManager = class {
1335
1538
  const keys = /* @__PURE__ */ new Set();
1336
1539
  for (const row of rows) {
1337
1540
  if (!Array.isArray(row.key) || row.key.length < 3) continue;
1541
+ if (row.value === void 0 || row.value === null) continue;
1338
1542
  const frontierKey = row.key[2];
1339
1543
  if (typeof frontierKey === "string") keys.add(frontierKey);
1340
1544
  }
@@ -1389,22 +1593,10 @@ var DocManager = class {
1389
1593
  }
1390
1594
  })().catch(logAsyncError(`doc ${docId} frontier debounce`));
1391
1595
  }
1392
- async onDocHandleClose(docId, doc) {
1393
- const refs = this.docRefs.get(docId) ?? 0;
1394
- if (refs <= 1) {
1395
- this.docRefs.delete(docId);
1396
- this.docSubscriptions.get(docId)?.();
1397
- this.docSubscriptions.delete(docId);
1398
- this.docs.delete(docId);
1399
- this.docPersistedVersions.delete(docId);
1400
- } else this.docRefs.set(docId, refs - 1);
1401
- await this.persistDocUpdate(docId, doc);
1402
- if (!await this.flushScheduledDocFrontierUpdate(docId)) await this.updateDocFrontiers(docId, doc, "local");
1403
- }
1404
1596
  async materializeDetachedDoc(docId) {
1405
1597
  const snapshot = await this.exportDocSnapshot(docId);
1406
1598
  if (snapshot) return loro_crdt.LoroDoc.fromSnapshot(snapshot);
1407
- return this.docFactory(docId);
1599
+ return new loro_crdt.LoroDoc();
1408
1600
  }
1409
1601
  async exportDocSnapshot(docId) {
1410
1602
  const cached = this.docs.get(docId);
@@ -1414,7 +1606,7 @@ var DocManager = class {
1414
1606
  }
1415
1607
  async persistDocUpdate(docId, doc) {
1416
1608
  const previousVersion = this.docPersistedVersions.get(docId);
1417
- const nextVersion = doc.version();
1609
+ const nextVersion = doc.oplogVersion();
1418
1610
  if (!this.storage) {
1419
1611
  this.docPersistedVersions.set(docId, nextVersion);
1420
1612
  return;
@@ -1424,14 +1616,11 @@ var DocManager = class {
1424
1616
  this.docPersistedVersions.set(docId, nextVersion);
1425
1617
  return;
1426
1618
  }
1619
+ if (previousVersion.compare(nextVersion) === 0) return;
1427
1620
  const update = doc.export({
1428
1621
  mode: "update",
1429
1622
  from: previousVersion
1430
1623
  });
1431
- if (!update.length) {
1432
- this.docPersistedVersions.set(docId, nextVersion);
1433
- return;
1434
- }
1435
1624
  this.docPersistedVersions.set(docId, nextVersion);
1436
1625
  try {
1437
1626
  await this.storage.save({
@@ -1453,7 +1642,14 @@ var DocManager = class {
1453
1642
  return;
1454
1643
  }
1455
1644
  const flushed = this.flushScheduledDocFrontierUpdate(docId);
1456
- const updated = this.updateDocFrontiers(docId, doc, by);
1645
+ const updated = (async () => {
1646
+ this.eventBus.pushEventBy(by);
1647
+ try {
1648
+ await this.updateDocFrontiers(docId, doc, by);
1649
+ } finally {
1650
+ this.eventBus.popEventBy();
1651
+ }
1652
+ })();
1457
1653
  await Promise.all([
1458
1654
  persist,
1459
1655
  flushed,
@@ -1486,17 +1682,38 @@ var MetadataManager = class {
1486
1682
  const metadata = this.state.metadata.get(docId);
1487
1683
  return metadata ? cloneJsonObject(metadata) : void 0;
1488
1684
  }
1489
- list(query) {
1685
+ listDoc(query) {
1686
+ if (query?.limit !== void 0 && query.limit <= 0) return [];
1687
+ const { startKey, endKey } = this.computeDocRangeKeys(query);
1688
+ if (startKey && endKey && startKey >= endKey) return [];
1689
+ const scanOptions = { prefix: ["m"] };
1690
+ if (startKey) scanOptions.start = {
1691
+ kind: "inclusive",
1692
+ key: ["m", startKey]
1693
+ };
1694
+ if (endKey) scanOptions.end = {
1695
+ kind: "exclusive",
1696
+ key: ["m", endKey]
1697
+ };
1698
+ const rows = this.metaFlock.scan(scanOptions);
1699
+ const seen = /* @__PURE__ */ new Set();
1490
1700
  const entries = [];
1491
- for (const [docId, metadata] of this.state.metadata.entries()) {
1701
+ for (const row of rows) {
1702
+ if (query?.limit !== void 0 && entries.length >= query.limit) break;
1703
+ if (!Array.isArray(row.key) || row.key.length < 2) continue;
1704
+ const docId = row.key[1];
1705
+ if (typeof docId !== "string") continue;
1706
+ if (seen.has(docId)) continue;
1707
+ seen.add(docId);
1708
+ const metadata = this.state.metadata.get(docId);
1709
+ if (!metadata) continue;
1492
1710
  if (!matchesQuery(docId, metadata, query)) continue;
1493
1711
  entries.push({
1494
1712
  docId,
1495
1713
  meta: cloneJsonObject(metadata)
1496
1714
  });
1715
+ if (query?.limit !== void 0 && entries.length >= query.limit) break;
1497
1716
  }
1498
- entries.sort((a, b) => a.docId < b.docId ? -1 : a.docId > b.docId ? 1 : 0);
1499
- if (query?.limit !== void 0) return entries.slice(0, query.limit);
1500
1717
  return entries;
1501
1718
  }
1502
1719
  async upsert(docId, patch) {
@@ -1590,6 +1807,26 @@ var MetadataManager = class {
1590
1807
  clear() {
1591
1808
  this.state.metadata.clear();
1592
1809
  }
1810
+ computeDocRangeKeys(query) {
1811
+ if (!query) return {};
1812
+ const prefix = query.prefix && query.prefix.length > 0 ? query.prefix : void 0;
1813
+ let startKey = query.start;
1814
+ if (prefix) startKey = !startKey || prefix > startKey ? prefix : startKey;
1815
+ let endKey = query.end;
1816
+ const prefixEnd = this.nextLexicographicString(prefix);
1817
+ if (prefixEnd) endKey = !endKey || prefixEnd < endKey ? prefixEnd : endKey;
1818
+ return {
1819
+ startKey,
1820
+ endKey
1821
+ };
1822
+ }
1823
+ nextLexicographicString(value) {
1824
+ if (!value) return void 0;
1825
+ for (let i = value.length - 1; i >= 0; i -= 1) {
1826
+ const code = value.charCodeAt(i);
1827
+ if (code < 65535) return `${value.slice(0, i)}${String.fromCharCode(code + 1)}`;
1828
+ }
1829
+ }
1593
1830
  readDocMetadataFromFlock(docId) {
1594
1831
  const rows = this.metaFlock.scan({ prefix: ["m", docId] });
1595
1832
  if (!rows.length) return void 0;
@@ -1663,13 +1900,11 @@ var AssetManager = class {
1663
1900
  if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
1664
1901
  const existing = this.assets.get(assetId);
1665
1902
  if (existing) {
1666
- if (!existing.data) {
1667
- const clone = bytes.slice();
1668
- existing.data = clone;
1669
- if (this.storage) await this.storage.save({
1903
+ if (this.storage) {
1904
+ if (!await this.storage.loadAsset(assetId)) await this.storage.save({
1670
1905
  type: "asset",
1671
1906
  assetId,
1672
- data: clone.slice()
1907
+ data: bytes.slice()
1673
1908
  });
1674
1909
  }
1675
1910
  let metadataMutated = false;
@@ -1696,7 +1931,7 @@ var AssetManager = class {
1696
1931
  await this.persistMeta();
1697
1932
  this.eventBus.emit({
1698
1933
  kind: "asset-metadata",
1699
- asset: this.createAssetDownload(assetId, metadata$1, existing.data),
1934
+ asset: this.createAssetDownload(assetId, metadata$1, bytes),
1700
1935
  by: "local"
1701
1936
  });
1702
1937
  }
@@ -1726,7 +1961,8 @@ var AssetManager = class {
1726
1961
  assetId,
1727
1962
  data: storedBytes.slice()
1728
1963
  });
1729
- this.rememberAsset(metadata, storedBytes);
1964
+ this.rememberAsset(metadata);
1965
+ this.markAssetAsOrphan(assetId, metadata);
1730
1966
  this.updateDocAssetMetadata(assetId, metadata);
1731
1967
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
1732
1968
  await this.persistMeta();
@@ -1747,13 +1983,11 @@ var AssetManager = class {
1747
1983
  const existing = this.assets.get(assetId);
1748
1984
  if (existing) {
1749
1985
  metadata = existing.metadata;
1750
- if (!existing.data) {
1751
- const clone = bytes.slice();
1752
- existing.data = clone;
1753
- if (this.storage) await this.storage.save({
1986
+ if (this.storage) {
1987
+ if (!await this.storage.loadAsset(assetId)) await this.storage.save({
1754
1988
  type: "asset",
1755
1989
  assetId,
1756
- data: clone.slice()
1990
+ data: bytes.slice()
1757
1991
  });
1758
1992
  }
1759
1993
  let nextMetadata = metadata;
@@ -1793,11 +2027,11 @@ var AssetManager = class {
1793
2027
  await this.persistMeta();
1794
2028
  this.eventBus.emit({
1795
2029
  kind: "asset-metadata",
1796
- asset: this.createAssetDownload(assetId, metadata, existing.data),
2030
+ asset: this.createAssetDownload(assetId, metadata, bytes),
1797
2031
  by: "local"
1798
2032
  });
1799
2033
  } else metadata = existing.metadata;
1800
- storedBytes = existing.data;
2034
+ storedBytes = bytes;
1801
2035
  this.rememberAsset(metadata);
1802
2036
  } else {
1803
2037
  metadata = {
@@ -1823,7 +2057,7 @@ var AssetManager = class {
1823
2057
  assetId,
1824
2058
  data: storedBytes.slice()
1825
2059
  });
1826
- this.rememberAsset(metadata, storedBytes);
2060
+ this.rememberAsset(metadata);
1827
2061
  this.updateDocAssetMetadata(assetId, metadata);
1828
2062
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
1829
2063
  created = true;
@@ -1831,9 +2065,7 @@ var AssetManager = class {
1831
2065
  const mapping = this.docAssets.get(docId) ?? /* @__PURE__ */ new Map();
1832
2066
  mapping.set(assetId, metadata);
1833
2067
  this.docAssets.set(docId, mapping);
1834
- const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
1835
- refs.add(docId);
1836
- this.assetToDocRefs.set(assetId, refs);
2068
+ this.addDocReference(assetId, docId);
1837
2069
  this.metaFlock.put([
1838
2070
  "ld",
1839
2071
  docId,
@@ -1863,20 +2095,7 @@ var AssetManager = class {
1863
2095
  docId,
1864
2096
  assetId
1865
2097
  ]);
1866
- const refs = this.assetToDocRefs.get(assetId);
1867
- if (refs) {
1868
- refs.delete(docId);
1869
- if (refs.size === 0) {
1870
- this.assetToDocRefs.delete(assetId);
1871
- const record = this.assets.get(assetId);
1872
- if (record) this.orphanedAssets.set(assetId, {
1873
- metadata: record.metadata,
1874
- deletedAt: Date.now()
1875
- });
1876
- this.metaFlock.delete(["a", assetId]);
1877
- this.assets.delete(assetId);
1878
- }
1879
- }
2098
+ this.removeDocAssetReference(assetId, docId);
1880
2099
  await this.persistMeta();
1881
2100
  this.eventBus.emit({
1882
2101
  kind: "asset-unlink",
@@ -1963,12 +2182,11 @@ var AssetManager = class {
1963
2182
  this.handleAssetRemoval(assetId, by);
1964
2183
  return;
1965
2184
  }
1966
- const existingData = previous?.data;
1967
- this.rememberAsset(metadata, existingData);
2185
+ this.rememberAsset(metadata);
1968
2186
  this.updateDocAssetMetadata(assetId, cloneRepoAssetMetadata(metadata));
1969
2187
  if (!previous || !assetMetadataEqual(previous.metadata, metadata)) this.eventBus.emit({
1970
2188
  kind: "asset-metadata",
1971
- asset: this.createAssetDownload(assetId, metadata, existingData),
2189
+ asset: this.createAssetDownload(assetId, metadata),
1972
2190
  by
1973
2191
  });
1974
2192
  }
@@ -1983,11 +2201,7 @@ var AssetManager = class {
1983
2201
  if (typeof assetId !== "string") continue;
1984
2202
  const metadata = assetMetaFromJson(row.value);
1985
2203
  if (!metadata) continue;
1986
- const existing = this.assets.get(assetId);
1987
- nextAssets.set(assetId, {
1988
- metadata,
1989
- data: existing?.data
1990
- });
2204
+ nextAssets.set(assetId, { metadata });
1991
2205
  }
1992
2206
  const nextDocAssets = /* @__PURE__ */ new Map();
1993
2207
  const linkRows = this.metaFlock.scan({ prefix: ["ld"] });
@@ -2023,12 +2237,18 @@ var AssetManager = class {
2023
2237
  this.assetToDocRefs.set(assetId, refs);
2024
2238
  }
2025
2239
  this.assets.clear();
2026
- for (const record of nextAssets.values()) this.rememberAsset(record.metadata, record.data);
2240
+ for (const record of nextAssets.values()) this.rememberAsset(record.metadata);
2241
+ for (const assetId of nextAssets.keys()) {
2242
+ const refs = this.assetToDocRefs.get(assetId);
2243
+ if (!refs || refs.size === 0) {
2244
+ if (!this.orphanedAssets.has(assetId)) this.markAssetAsOrphan(assetId, nextAssets.get(assetId).metadata);
2245
+ } else this.orphanedAssets.delete(assetId);
2246
+ }
2027
2247
  for (const [assetId, record] of nextAssets) {
2028
2248
  const previous = prevAssets.get(assetId)?.metadata;
2029
2249
  if (!assetMetadataEqual(previous, record.metadata)) this.eventBus.emit({
2030
2250
  kind: "asset-metadata",
2031
- asset: this.createAssetDownload(assetId, record.metadata, record.data),
2251
+ asset: this.createAssetDownload(assetId, record.metadata),
2032
2252
  by
2033
2253
  });
2034
2254
  }
@@ -2124,36 +2344,24 @@ var AssetManager = class {
2124
2344
  };
2125
2345
  }
2126
2346
  async materializeAsset(assetId) {
2127
- let record = this.assets.get(assetId);
2128
- if (record?.data) return {
2129
- metadata: record.metadata,
2130
- bytes: record.data.slice()
2131
- };
2347
+ const record = this.assets.get(assetId);
2132
2348
  if (record && this.storage) {
2133
2349
  const stored = await this.storage.loadAsset(assetId);
2134
- if (stored) {
2135
- const clone = stored.slice();
2136
- record.data = clone.slice();
2137
- return {
2138
- metadata: record.metadata,
2139
- bytes: clone
2140
- };
2141
- }
2350
+ if (stored) return {
2351
+ metadata: record.metadata,
2352
+ bytes: stored
2353
+ };
2142
2354
  }
2143
2355
  if (!record && this.storage) {
2144
2356
  const stored = await this.storage.loadAsset(assetId);
2145
2357
  if (stored) {
2146
2358
  const metadata$1 = this.getAssetMetadata(assetId);
2147
2359
  if (!metadata$1) throw new Error(`Missing metadata for asset ${assetId}`);
2148
- const clone = stored.slice();
2149
- this.assets.set(assetId, {
2150
- metadata: metadata$1,
2151
- data: clone.slice()
2152
- });
2360
+ this.assets.set(assetId, { metadata: metadata$1 });
2153
2361
  this.updateDocAssetMetadata(assetId, metadata$1);
2154
2362
  return {
2155
2363
  metadata: metadata$1,
2156
- bytes: clone
2364
+ bytes: stored
2157
2365
  };
2158
2366
  }
2159
2367
  }
@@ -2169,10 +2377,7 @@ var AssetManager = class {
2169
2377
  ...remote.policy ? { policy: remote.policy } : {},
2170
2378
  ...remote.tag ? { tag: remote.tag } : {}
2171
2379
  };
2172
- this.assets.set(assetId, {
2173
- metadata,
2174
- data: remoteBytes.slice()
2175
- });
2380
+ this.assets.set(assetId, { metadata });
2176
2381
  this.updateDocAssetMetadata(assetId, metadata);
2177
2382
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
2178
2383
  await this.persistMeta();
@@ -2194,18 +2399,14 @@ var AssetManager = class {
2194
2399
  if (assets) assets.set(assetId, metadata);
2195
2400
  }
2196
2401
  }
2197
- rememberAsset(metadata, bytes) {
2198
- const data = bytes ? bytes.slice() : this.assets.get(metadata.assetId)?.data;
2199
- this.assets.set(metadata.assetId, {
2200
- metadata,
2201
- data
2202
- });
2203
- this.orphanedAssets.delete(metadata.assetId);
2402
+ rememberAsset(metadata) {
2403
+ this.assets.set(metadata.assetId, { metadata });
2204
2404
  }
2205
2405
  addDocReference(assetId, docId) {
2206
2406
  const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
2207
2407
  refs.add(docId);
2208
2408
  this.assetToDocRefs.set(assetId, refs);
2409
+ this.orphanedAssets.delete(assetId);
2209
2410
  }
2210
2411
  removeDocAssetReference(assetId, docId) {
2211
2412
  const refs = this.assetToDocRefs.get(assetId);
@@ -2328,6 +2529,9 @@ var FlockHydrator = class {
2328
2529
 
2329
2530
  //#endregion
2330
2531
  //#region src/internal/sync-runner.ts
2532
+ /**
2533
+ * Sync data between storage and transport layer
2534
+ */
2331
2535
  var SyncRunner = class {
2332
2536
  storage;
2333
2537
  transport;
@@ -2342,6 +2546,7 @@ var SyncRunner = class {
2342
2546
  readyPromise;
2343
2547
  metaRoomSubscription;
2344
2548
  unsubscribeMetaFlock;
2549
+ docSubscriptions = /* @__PURE__ */ new Map();
2345
2550
  constructor(options) {
2346
2551
  this.storage = options.storage;
2347
2552
  this.transport = options.transport;
@@ -2351,7 +2556,7 @@ var SyncRunner = class {
2351
2556
  this.assetManager = options.assetManager;
2352
2557
  this.flockHydrator = options.flockHydrator;
2353
2558
  this.getMetaFlock = options.getMetaFlock;
2354
- this.replaceMetaFlock = options.replaceMetaFlock;
2559
+ this.replaceMetaFlock = options.mergeFlock;
2355
2560
  this.persistMeta = options.persistMeta;
2356
2561
  }
2357
2562
  async ready() {
@@ -2428,15 +2633,30 @@ var SyncRunner = class {
2428
2633
  await this.ready();
2429
2634
  if (!this.transport) throw new Error("Transport adapter not configured");
2430
2635
  if (!this.transport.isConnected()) await this.transport.connect();
2636
+ const existing = this.docSubscriptions.get(docId);
2637
+ if (existing) return existing;
2431
2638
  const doc = await this.docManager.ensureDoc(docId);
2432
2639
  const subscription = this.transport.joinDocRoom(docId, doc, params);
2640
+ const wrapped = {
2641
+ unsubscribe: () => {
2642
+ subscription.unsubscribe();
2643
+ if (this.docSubscriptions.get(docId) === wrapped) this.docSubscriptions.delete(docId);
2644
+ },
2645
+ firstSyncedWithRemote: subscription.firstSyncedWithRemote,
2646
+ get connected() {
2647
+ return subscription.connected;
2648
+ }
2649
+ };
2650
+ this.docSubscriptions.set(docId, wrapped);
2433
2651
  subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
2434
- return subscription;
2652
+ return wrapped;
2435
2653
  }
2436
- async close() {
2654
+ async destroy() {
2437
2655
  await this.docManager.close();
2438
2656
  this.metaRoomSubscription?.unsubscribe();
2439
2657
  this.metaRoomSubscription = void 0;
2658
+ for (const sub of this.docSubscriptions.values()) sub.unsubscribe();
2659
+ this.docSubscriptions.clear();
2440
2660
  if (this.unsubscribeMetaFlock) {
2441
2661
  this.unsubscribeMetaFlock();
2442
2662
  this.unsubscribeMetaFlock = void 0;
@@ -2487,16 +2707,17 @@ function createRepoState() {
2487
2707
  //#region src/index.ts
2488
2708
  const textEncoder = new TextEncoder();
2489
2709
  const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
2490
- var LoroRepo = class {
2710
+ var LoroRepo = class LoroRepo {
2491
2711
  options;
2712
+ _destroyed = false;
2492
2713
  transport;
2493
2714
  storage;
2494
- docFactory;
2495
2715
  metaFlock = new __loro_dev_flock.Flock();
2496
2716
  eventBus;
2497
2717
  docManager;
2498
2718
  metadataManager;
2499
2719
  assetManager;
2720
+ assetTransport;
2500
2721
  flockHydrator;
2501
2722
  state;
2502
2723
  syncRunner;
@@ -2504,14 +2725,13 @@ var LoroRepo = class {
2504
2725
  this.options = options;
2505
2726
  this.transport = options.transportAdapter;
2506
2727
  this.storage = options.storageAdapter;
2507
- this.docFactory = options.docFactory ?? (async () => new loro_crdt.LoroDoc());
2728
+ this.assetTransport = options.assetTransportAdapter;
2508
2729
  this.eventBus = new RepoEventBus();
2509
2730
  this.state = createRepoState();
2510
2731
  const configuredDebounce = options.docFrontierDebounceMs;
2511
2732
  const docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
2512
2733
  this.docManager = new DocManager({
2513
2734
  storage: this.storage,
2514
- docFactory: this.docFactory,
2515
2735
  docFrontierDebounceMs,
2516
2736
  getMetaFlock: () => this.metaFlock,
2517
2737
  eventBus: this.eventBus,
@@ -2526,7 +2746,7 @@ var LoroRepo = class {
2526
2746
  });
2527
2747
  this.assetManager = new AssetManager({
2528
2748
  storage: this.storage,
2529
- assetTransport: options.assetTransportAdapter,
2749
+ assetTransport: this.assetTransport,
2530
2750
  getMetaFlock: () => this.metaFlock,
2531
2751
  eventBus: this.eventBus,
2532
2752
  persistMeta: () => this.persistMeta(),
@@ -2547,96 +2767,133 @@ var LoroRepo = class {
2547
2767
  assetManager: this.assetManager,
2548
2768
  flockHydrator: this.flockHydrator,
2549
2769
  getMetaFlock: () => this.metaFlock,
2550
- replaceMetaFlock: (snapshot) => {
2551
- this.metaFlock = snapshot;
2770
+ mergeFlock: (snapshot) => {
2771
+ this.metaFlock.merge(snapshot);
2552
2772
  },
2553
2773
  persistMeta: () => this.persistMeta()
2554
2774
  });
2555
2775
  }
2776
+ static async create(options) {
2777
+ const repo = new LoroRepo(options);
2778
+ await repo.storage?.init?.();
2779
+ await repo.ready();
2780
+ return repo;
2781
+ }
2782
+ /**
2783
+ * Load meta from storage.
2784
+ *
2785
+ * You need to call this before all other operations to make the app functioning correctly.
2786
+ * Though we do that implicitly already
2787
+ */
2556
2788
  async ready() {
2557
2789
  await this.syncRunner.ready();
2558
2790
  }
2791
+ /**
2792
+ * Sync selected data via the transport adaptor
2793
+ * @param options
2794
+ */
2559
2795
  async sync(options = {}) {
2560
2796
  await this.syncRunner.sync(options);
2561
2797
  }
2798
+ /**
2799
+ * Start syncing the metadata (Flock) room. It will establish a realtime connection to the transport adaptor.
2800
+ * All changes on the room will be synced to the Flock, and all changes on the Flock will be synced to the room.
2801
+ * @param params
2802
+ * @returns
2803
+ */
2562
2804
  async joinMetaRoom(params) {
2563
2805
  return this.syncRunner.joinMetaRoom(params);
2564
2806
  }
2807
+ /**
2808
+ * Start syncing the given doc. It will establish a realtime connection to the transport adaptor.
2809
+ * All changes on the doc will be synced to the transport, and all changes on the transport will be synced to the doc.
2810
+ *
2811
+ * All the changes on the room will be reflected on the same doc you get from `repo.openCollaborativeDoc(docId)`
2812
+ * @param docId
2813
+ * @param params
2814
+ * @returns
2815
+ */
2565
2816
  async joinDocRoom(docId, params) {
2566
2817
  return this.syncRunner.joinDocRoom(docId, params);
2567
2818
  }
2568
- async close() {
2569
- await this.syncRunner.close();
2819
+ /**
2820
+ * Opens a document that is automatically persisted to the configured storage adapter.
2821
+ *
2822
+ * - Edits are saved to storage (debounced).
2823
+ * - Frontiers are synced to the metadata (Flock).
2824
+ * - Realtime collaboration is NOT enabled by default; use `joinDocRoom` to connect.
2825
+ */
2826
+ async openPersistedDoc(docId) {
2827
+ return {
2828
+ doc: await this.docManager.openCollaborativeDoc(docId),
2829
+ syncOnce: () => {
2830
+ return this.sync({
2831
+ scope: "doc",
2832
+ docIds: [docId]
2833
+ });
2834
+ },
2835
+ joinRoom: (auth) => {
2836
+ return this.syncRunner.joinDocRoom(docId, { auth });
2837
+ }
2838
+ };
2570
2839
  }
2571
- async upsertDocMeta(docId, patch, _options = {}) {
2572
- await this.ready();
2840
+ async upsertDocMeta(docId, patch) {
2573
2841
  await this.metadataManager.upsert(docId, patch);
2574
2842
  }
2575
2843
  async getDocMeta(docId) {
2576
- await this.ready();
2577
2844
  return this.metadataManager.get(docId);
2578
2845
  }
2579
2846
  async listDoc(query) {
2580
- await this.ready();
2581
- return this.metadataManager.list(query);
2847
+ return this.metadataManager.listDoc(query);
2582
2848
  }
2583
- getMetaReplica() {
2849
+ getMeta() {
2584
2850
  return this.metaFlock;
2585
2851
  }
2586
2852
  watch(listener, filter = {}) {
2587
2853
  return this.eventBus.watch(listener, filter);
2588
2854
  }
2589
2855
  /**
2590
- * Opens the repo-managed collaborative document, registers it for persistence,
2591
- * and schedules a doc-level sync so `whenSyncedWithRemote` resolves after remote backfills.
2856
+ * Opens a detached `LoroDoc` snapshot.
2857
+ *
2858
+ * - **No Persistence**: Edits to this document are NOT saved to storage.
2859
+ * - **No Sync**: This document does not participate in realtime updates.
2860
+ * - **Use Case**: Ideal for read-only history inspection, temporary drafts, or conflict resolution without affecting the main state.
2592
2861
  */
2593
- async openCollaborativeDoc(docId) {
2594
- await this.ready();
2595
- const whenSyncedWithRemote = this.whenDocInSyncWithRemote(docId);
2596
- return this.docManager.openCollaborativeDoc(docId, whenSyncedWithRemote);
2862
+ async openDetachedDoc(docId) {
2863
+ return this.docManager.openDetachedDoc(docId);
2597
2864
  }
2598
2865
  /**
2599
- * Opens a detached `LoroDoc` snapshot that never registers with the repo, meaning
2600
- * it neither participates in remote subscriptions nor persists edits back to storage.
2866
+ * Explicitly unloads a document from memory.
2867
+ *
2868
+ * - **Persists Immediately**: Forces a save of the document's current state to storage.
2869
+ * - **Frees Memory**: Removes the document from the internal cache.
2870
+ * - **Note**: If the document is currently being synced (via `joinDocRoom`), you should also unsubscribe from the room to fully release resources.
2601
2871
  */
2602
- async openDetachedDoc(docId) {
2603
- await this.ready();
2604
- return this.docManager.openDetachedDoc(docId);
2872
+ async unloadDoc(docId) {
2873
+ await this.docManager.unloadDoc(docId);
2874
+ }
2875
+ async flush() {
2876
+ await this.docManager.flush();
2605
2877
  }
2606
2878
  async uploadAsset(params) {
2607
- await this.ready();
2608
2879
  return this.assetManager.uploadAsset(params);
2609
2880
  }
2610
- async whenDocInSyncWithRemote(docId) {
2611
- await this.ready();
2612
- await this.docManager.ensureDoc(docId);
2613
- await this.sync({
2614
- scope: "doc",
2615
- docIds: [docId]
2616
- });
2617
- }
2618
2881
  async linkAsset(docId, params) {
2619
- await this.ready();
2620
2882
  return this.assetManager.linkAsset(docId, params);
2621
2883
  }
2622
2884
  async fetchAsset(assetId) {
2623
- await this.ready();
2624
2885
  return this.assetManager.fetchAsset(assetId);
2625
2886
  }
2626
2887
  async unlinkAsset(docId, assetId) {
2627
- await this.ready();
2628
2888
  await this.assetManager.unlinkAsset(docId, assetId);
2629
2889
  }
2630
2890
  async listAssets(docId) {
2631
- await this.ready();
2632
2891
  return this.assetManager.listAssets(docId);
2633
2892
  }
2634
2893
  async ensureAsset(assetId) {
2635
- await this.ready();
2636
2894
  return this.assetManager.ensureAsset(assetId);
2637
2895
  }
2638
2896
  async gcAssets(options = {}) {
2639
- await this.ready();
2640
2897
  return this.assetManager.gcAssets(options);
2641
2898
  }
2642
2899
  async persistMeta() {
@@ -2648,6 +2905,17 @@ var LoroRepo = class {
2648
2905
  update: encoded
2649
2906
  });
2650
2907
  }
2908
+ get destroyed() {
2909
+ return this._destroyed;
2910
+ }
2911
+ async destroy() {
2912
+ if (this._destroyed) return;
2913
+ this._destroyed = true;
2914
+ await this.syncRunner.destroy();
2915
+ this.assetTransport?.close?.();
2916
+ this.storage?.close?.();
2917
+ await this.transport?.close();
2918
+ }
2651
2919
  };
2652
2920
 
2653
2921
  //#endregion