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.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Flock } from "@loro-dev/flock";
2
- import { LoroDoc } from "loro-crdt";
3
- import { FlockAdaptor, LoroAdaptor } from "loro-adaptors";
2
+ import { LoroAdaptor } from "loro-adaptors/loro";
4
3
  import { CrdtType, bytesToHex } from "loro-protocol";
5
4
  import { LoroWebsocketClient } from "loro-websocket";
5
+ import { LoroDoc } from "loro-crdt";
6
+ import { FlockAdaptor } from "loro-adaptors/flock";
6
7
  import { promises } from "node:fs";
7
8
  import * as path from "node:path";
8
9
  import { randomUUID } from "node:crypto";
@@ -12,8 +13,47 @@ function createRepoFlockAdaptorFromDoc(flock, config = {}) {
12
13
  return new FlockAdaptor(flock, config);
13
14
  }
14
15
 
16
+ //#endregion
17
+ //#region src/internal/debug.ts
18
+ const getEnv = () => {
19
+ if (typeof globalThis !== "object" || globalThis === null) return;
20
+ return globalThis.process?.env;
21
+ };
22
+ const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
23
+ const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
24
+ const wildcardTokens = new Set([
25
+ "*",
26
+ "1",
27
+ "true",
28
+ "all"
29
+ ]);
30
+ const namespaceSet = new Set(normalizedNamespaces);
31
+ const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
32
+ const isDebugEnabled = (namespace) => {
33
+ if (!namespaceSet.size) return false;
34
+ if (!namespace) return hasWildcard;
35
+ const normalized = namespace.toLowerCase();
36
+ if (hasWildcard) return true;
37
+ if (namespaceSet.has(normalized)) return true;
38
+ const [root] = normalized.split(":");
39
+ return namespaceSet.has(root);
40
+ };
41
+ const createDebugLogger = (namespace) => {
42
+ const normalized = namespace.toLowerCase();
43
+ return (...args) => {
44
+ if (!isDebugEnabled(normalized)) return;
45
+ const prefix = `[loro-repo:${namespace}]`;
46
+ if (args.length === 0) {
47
+ console.info(prefix);
48
+ return;
49
+ }
50
+ console.info(prefix, ...args);
51
+ };
52
+ };
53
+
15
54
  //#endregion
16
55
  //#region src/transport/websocket.ts
56
+ const debug = createDebugLogger("transport:websocket");
17
57
  function withTimeout(promise, timeoutMs) {
18
58
  if (!timeoutMs || timeoutMs <= 0) return promise;
19
59
  return new Promise((resolve, reject) => {
@@ -58,30 +98,51 @@ var WebSocketTransportAdapter = class {
58
98
  }
59
99
  async connect(_options) {
60
100
  const client = this.ensureClient();
61
- await client.connect();
62
- await client.waitConnected();
101
+ debug("connect requested", { status: client.getStatus() });
102
+ try {
103
+ await client.connect();
104
+ debug("client.connect resolved");
105
+ await client.waitConnected();
106
+ debug("client.waitConnected resolved", { status: client.getStatus() });
107
+ } catch (error) {
108
+ debug("connect failed", error);
109
+ throw error;
110
+ }
63
111
  }
64
112
  async close() {
113
+ debug("close requested", {
114
+ docSessions: this.docSessions.size,
115
+ metadataSession: Boolean(this.metadataSession)
116
+ });
65
117
  for (const [docId] of this.docSessions) await this.leaveDocSession(docId).catch(() => {});
66
118
  this.docSessions.clear();
67
119
  await this.teardownMetadataSession().catch(() => {});
68
120
  if (this.client) {
69
- this.client.destroy();
121
+ const client = this.client;
70
122
  this.client = void 0;
123
+ client.destroy();
124
+ debug("websocket client destroyed");
71
125
  }
126
+ debug("close completed");
72
127
  }
73
128
  isConnected() {
74
129
  return this.client?.getStatus() === "connected";
75
130
  }
76
131
  async syncMeta(flock, options) {
77
- if (!this.options.metadataRoomId) return { ok: true };
132
+ if (!this.options.metadataRoomId) {
133
+ debug("syncMeta skipped; metadata room not configured");
134
+ return { ok: true };
135
+ }
136
+ debug("syncMeta requested", { roomId: this.options.metadataRoomId });
78
137
  try {
79
138
  await withTimeout((await this.ensureMetadataSession(flock, {
80
139
  roomId: this.options.metadataRoomId,
81
140
  auth: this.options.metadataAuth
82
141
  })).firstSynced, options?.timeout);
142
+ debug("syncMeta completed", { roomId: this.options.metadataRoomId });
83
143
  return { ok: true };
84
- } catch {
144
+ } catch (error) {
145
+ debug("syncMeta failed", error);
85
146
  return { ok: false };
86
147
  }
87
148
  }
@@ -90,6 +151,10 @@ var WebSocketTransportAdapter = class {
90
151
  const roomId = normalizeRoomId(params?.roomId, fallback);
91
152
  if (!roomId) throw new Error("Metadata room id not configured");
92
153
  const auth = params?.auth ?? this.options.metadataAuth;
154
+ debug("joinMetaRoom requested", {
155
+ roomId,
156
+ hasAuth: Boolean(auth && auth.length)
157
+ });
93
158
  const ensure = this.ensureMetadataSession(flock, {
94
159
  roomId,
95
160
  auth
@@ -100,7 +165,14 @@ var WebSocketTransportAdapter = class {
100
165
  unsubscribe: () => {
101
166
  ensure.then((session) => {
102
167
  session.refCount = Math.max(0, session.refCount - 1);
103
- if (session.refCount === 0) this.teardownMetadataSession(session).catch(() => {});
168
+ debug("metadata session refCount decremented", {
169
+ roomId: session.roomId,
170
+ refCount: session.refCount
171
+ });
172
+ if (session.refCount === 0) {
173
+ debug("tearing down metadata session due to refCount=0", { roomId: session.roomId });
174
+ this.teardownMetadataSession(session).catch(() => {});
175
+ }
104
176
  });
105
177
  },
106
178
  firstSyncedWithRemote: firstSynced,
@@ -110,18 +182,37 @@ var WebSocketTransportAdapter = class {
110
182
  };
111
183
  ensure.then((session) => {
112
184
  session.refCount += 1;
185
+ debug("metadata session refCount incremented", {
186
+ roomId: session.roomId,
187
+ refCount: session.refCount
188
+ });
113
189
  });
114
190
  return subscription;
115
191
  }
116
192
  async syncDoc(docId, doc, options) {
193
+ debug("syncDoc requested", { docId });
117
194
  try {
118
- await withTimeout((await this.ensureDocSession(docId, doc, {})).firstSynced, options?.timeout);
195
+ const session = await this.ensureDocSession(docId, doc, {});
196
+ await withTimeout(session.firstSynced, options?.timeout);
197
+ debug("syncDoc completed", {
198
+ docId,
199
+ roomId: session.roomId
200
+ });
119
201
  return { ok: true };
120
- } catch {
202
+ } catch (error) {
203
+ debug("syncDoc failed", {
204
+ docId,
205
+ error
206
+ });
121
207
  return { ok: false };
122
208
  }
123
209
  }
124
210
  joinDocRoom(docId, doc, params) {
211
+ debug("joinDocRoom requested", {
212
+ docId,
213
+ roomParamType: params?.roomId ? typeof params.roomId === "string" ? "string" : "uint8array" : void 0,
214
+ hasAuthOverride: Boolean(params?.auth && params.auth.length)
215
+ });
125
216
  const ensure = this.ensureDocSession(docId, doc, params ?? {});
126
217
  const firstSynced = ensure.then((session) => session.firstSynced);
127
218
  const getConnected = () => this.isConnected();
@@ -129,6 +220,11 @@ var WebSocketTransportAdapter = class {
129
220
  unsubscribe: () => {
130
221
  ensure.then((session) => {
131
222
  session.refCount = Math.max(0, session.refCount - 1);
223
+ debug("doc session refCount decremented", {
224
+ docId,
225
+ roomId: session.roomId,
226
+ refCount: session.refCount
227
+ });
132
228
  if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
133
229
  });
134
230
  },
@@ -139,12 +235,24 @@ var WebSocketTransportAdapter = class {
139
235
  };
140
236
  ensure.then((session) => {
141
237
  session.refCount += 1;
238
+ debug("doc session refCount incremented", {
239
+ docId,
240
+ roomId: session.roomId,
241
+ refCount: session.refCount
242
+ });
142
243
  });
143
244
  return subscription;
144
245
  }
145
246
  ensureClient() {
146
- if (this.client) return this.client;
247
+ if (this.client) {
248
+ debug("reusing websocket client", { status: this.client.getStatus() });
249
+ return this.client;
250
+ }
147
251
  const { url, client: clientOptions } = this.options;
252
+ debug("creating websocket client", {
253
+ url,
254
+ clientOptionsKeys: clientOptions ? Object.keys(clientOptions) : []
255
+ });
148
256
  const client = new LoroWebsocketClient({
149
257
  url,
150
258
  ...clientOptions
@@ -153,22 +261,49 @@ var WebSocketTransportAdapter = class {
153
261
  return client;
154
262
  }
155
263
  async ensureMetadataSession(flock, params) {
264
+ debug("ensureMetadataSession invoked", {
265
+ roomId: params.roomId,
266
+ hasAuth: Boolean(params.auth && params.auth.length)
267
+ });
156
268
  const client = this.ensureClient();
157
269
  await client.waitConnected();
158
- if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) return this.metadataSession;
159
- if (this.metadataSession) await this.teardownMetadataSession(this.metadataSession).catch(() => {});
270
+ debug("websocket client ready for metadata session", { status: client.getStatus() });
271
+ if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) {
272
+ debug("reusing metadata session", {
273
+ roomId: this.metadataSession.roomId,
274
+ refCount: this.metadataSession.refCount
275
+ });
276
+ return this.metadataSession;
277
+ }
278
+ if (this.metadataSession) {
279
+ debug("tearing down previous metadata session", { roomId: this.metadataSession.roomId });
280
+ await this.teardownMetadataSession(this.metadataSession).catch(() => {});
281
+ }
160
282
  const configuredType = this.options.metadataCrdtType;
161
283
  if (configuredType && configuredType !== CrdtType.Flock) throw new Error(`metadataCrdtType must be ${CrdtType.Flock} when syncing Flock metadata`);
162
284
  const adaptor = createRepoFlockAdaptorFromDoc(flock, this.options.metadataAdaptorConfig ?? {});
285
+ debug("joining metadata room", {
286
+ roomId: params.roomId,
287
+ hasAuth: Boolean(params.auth && params.auth.length)
288
+ });
163
289
  const room = await client.join({
164
290
  roomId: params.roomId,
165
291
  crdtAdaptor: adaptor,
166
292
  auth: params.auth
167
293
  });
294
+ const firstSynced = room.waitForReachingServerVersion();
295
+ firstSynced.then(() => {
296
+ debug("metadata session firstSynced resolved", { roomId: params.roomId });
297
+ }, (error) => {
298
+ debug("metadata session firstSynced rejected", {
299
+ roomId: params.roomId,
300
+ error
301
+ });
302
+ });
168
303
  const session = {
169
304
  adaptor,
170
305
  room,
171
- firstSynced: room.waitForReachingServerVersion(),
306
+ firstSynced,
172
307
  flock,
173
308
  roomId: params.roomId,
174
309
  auth: params.auth,
@@ -180,34 +315,83 @@ var WebSocketTransportAdapter = class {
180
315
  async teardownMetadataSession(session) {
181
316
  const target = session ?? this.metadataSession;
182
317
  if (!target) return;
318
+ debug("teardownMetadataSession invoked", { roomId: target.roomId });
183
319
  if (this.metadataSession === target) this.metadataSession = void 0;
184
320
  const { adaptor, room } = target;
185
321
  try {
186
322
  await room.leave();
187
- } catch {
323
+ debug("metadata room left", { roomId: target.roomId });
324
+ } catch (error) {
325
+ debug("metadata room leave failed; destroying", {
326
+ roomId: target.roomId,
327
+ error
328
+ });
188
329
  await room.destroy().catch(() => {});
189
330
  }
190
331
  adaptor.destroy();
332
+ debug("metadata session destroyed", { roomId: target.roomId });
191
333
  }
192
334
  async ensureDocSession(docId, doc, params) {
335
+ debug("ensureDocSession invoked", { docId });
193
336
  const client = this.ensureClient();
194
337
  await client.waitConnected();
338
+ debug("websocket client ready for doc session", {
339
+ docId,
340
+ status: client.getStatus()
341
+ });
195
342
  const existing = this.docSessions.get(docId);
196
343
  const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
197
344
  const roomId = normalizeRoomId(params.roomId, derivedRoomId);
198
345
  const auth = params.auth ?? this.options.docAuth?.(docId);
199
- if (existing && existing.doc === doc && existing.roomId === roomId) return existing;
200
- if (existing) await this.leaveDocSession(docId).catch(() => {});
346
+ debug("doc session params resolved", {
347
+ docId,
348
+ roomId,
349
+ hasAuth: Boolean(auth && auth.length)
350
+ });
351
+ if (existing && existing.doc === doc && existing.roomId === roomId) {
352
+ debug("reusing doc session", {
353
+ docId,
354
+ roomId,
355
+ refCount: existing.refCount
356
+ });
357
+ return existing;
358
+ }
359
+ if (existing) {
360
+ debug("doc session mismatch; leaving existing session", {
361
+ docId,
362
+ previousRoomId: existing.roomId,
363
+ nextRoomId: roomId
364
+ });
365
+ await this.leaveDocSession(docId).catch(() => {});
366
+ }
201
367
  const adaptor = new LoroAdaptor(doc);
368
+ debug("joining doc room", {
369
+ docId,
370
+ roomId,
371
+ hasAuth: Boolean(auth && auth.length)
372
+ });
202
373
  const room = await client.join({
203
374
  roomId,
204
375
  crdtAdaptor: adaptor,
205
376
  auth
206
377
  });
378
+ const firstSynced = room.waitForReachingServerVersion();
379
+ firstSynced.then(() => {
380
+ debug("doc session firstSynced resolved", {
381
+ docId,
382
+ roomId
383
+ });
384
+ }, (error) => {
385
+ debug("doc session firstSynced rejected", {
386
+ docId,
387
+ roomId,
388
+ error
389
+ });
390
+ });
207
391
  const session = {
208
392
  adaptor,
209
393
  room,
210
- firstSynced: room.waitForReachingServerVersion(),
394
+ firstSynced,
211
395
  doc,
212
396
  roomId,
213
397
  refCount: 0
@@ -217,14 +401,34 @@ var WebSocketTransportAdapter = class {
217
401
  }
218
402
  async leaveDocSession(docId) {
219
403
  const session = this.docSessions.get(docId);
220
- if (!session) return;
404
+ if (!session) {
405
+ debug("leaveDocSession invoked but no session found", { docId });
406
+ return;
407
+ }
221
408
  this.docSessions.delete(docId);
409
+ debug("leaving doc session", {
410
+ docId,
411
+ roomId: session.roomId
412
+ });
222
413
  try {
223
414
  await session.room.leave();
224
- } catch {
415
+ debug("doc room left", {
416
+ docId,
417
+ roomId: session.roomId
418
+ });
419
+ } catch (error) {
420
+ debug("doc room leave failed; destroying", {
421
+ docId,
422
+ roomId: session.roomId,
423
+ error
424
+ });
225
425
  await session.room.destroy().catch(() => {});
226
426
  }
227
427
  session.adaptor.destroy();
428
+ debug("doc session destroyed", {
429
+ docId,
430
+ roomId: session.roomId
431
+ });
228
432
  }
229
433
  };
230
434
 
@@ -929,24 +1133,6 @@ var RepoEventBus = class {
929
1133
  }
930
1134
  };
931
1135
 
932
- //#endregion
933
- //#region src/internal/doc-handle.ts
934
- var RepoDocHandleImpl = class {
935
- doc;
936
- whenSyncedWithRemote;
937
- docId;
938
- onClose;
939
- constructor(docId, doc, whenSyncedWithRemote, onClose) {
940
- this.docId = docId;
941
- this.doc = doc;
942
- this.whenSyncedWithRemote = whenSyncedWithRemote;
943
- this.onClose = onClose;
944
- }
945
- async close() {
946
- await this.onClose(this.docId, this.doc);
947
- }
948
- };
949
-
950
1136
  //#endregion
951
1137
  //#region src/utils.ts
952
1138
  async function streamToUint8Array(stream) {
@@ -1109,37 +1295,24 @@ function toReadableStream(bytes) {
1109
1295
  controller.close();
1110
1296
  } });
1111
1297
  }
1112
- function emptyFrontiers() {
1113
- return [];
1114
- }
1115
- function getDocFrontiers(doc) {
1116
- const candidate = doc;
1117
- if (typeof candidate.frontiers === "function") {
1118
- const result = candidate.frontiers();
1119
- if (result) return result;
1120
- }
1121
- return emptyFrontiers();
1122
- }
1123
- function versionVectorToJson(vv) {
1124
- const map = vv.toJSON();
1125
- const record = {};
1126
- if (map instanceof Map) {
1127
- const entries = Array.from(map.entries()).sort(([a], [b]) => String(a).localeCompare(String(b)));
1128
- for (const [peer, counter] of entries) {
1129
- if (typeof counter !== "number" || !Number.isFinite(counter)) continue;
1130
- const key = typeof peer === "string" ? peer : JSON.stringify(peer);
1131
- record[key] = counter;
1132
- }
1133
- }
1134
- return record;
1135
- }
1136
- function canonicalizeVersionVector(vv) {
1137
- const json = versionVectorToJson(vv);
1298
+ function canonicalizeFrontiers(frontiers) {
1299
+ const json = [...frontiers].sort((a, b) => {
1300
+ if (a.peer < b.peer) return -1;
1301
+ if (a.peer > b.peer) return 1;
1302
+ return a.counter - b.counter;
1303
+ }).map((f) => ({
1304
+ peer: f.peer,
1305
+ counter: f.counter
1306
+ }));
1138
1307
  return {
1139
1308
  json,
1140
1309
  key: stableStringify(json)
1141
1310
  };
1142
1311
  }
1312
+ function includesFrontiers(vv, frontiers) {
1313
+ for (const { peer, counter } of frontiers) if ((vv.get(peer) ?? 0) <= counter) return false;
1314
+ return true;
1315
+ }
1143
1316
  function matchesQuery(docId, _metadata, query) {
1144
1317
  if (!query) return true;
1145
1318
  if (query.prefix && !docId.startsWith(query.prefix)) return false;
@@ -1161,14 +1334,12 @@ function logAsyncError(context) {
1161
1334
  //#region src/internal/doc-manager.ts
1162
1335
  var DocManager = class {
1163
1336
  storage;
1164
- docFactory;
1165
1337
  docFrontierDebounceMs;
1166
1338
  getMetaFlock;
1167
1339
  eventBus;
1168
1340
  persistMeta;
1169
1341
  state;
1170
1342
  docs = /* @__PURE__ */ new Map();
1171
- docRefs = /* @__PURE__ */ new Map();
1172
1343
  docSubscriptions = /* @__PURE__ */ new Map();
1173
1344
  docFrontierUpdates = /* @__PURE__ */ new Map();
1174
1345
  docPersistedVersions = /* @__PURE__ */ new Map();
@@ -1177,21 +1348,17 @@ var DocManager = class {
1177
1348
  }
1178
1349
  constructor(options) {
1179
1350
  this.storage = options.storage;
1180
- this.docFactory = options.docFactory;
1181
1351
  this.docFrontierDebounceMs = options.docFrontierDebounceMs;
1182
1352
  this.getMetaFlock = options.getMetaFlock;
1183
1353
  this.eventBus = options.eventBus;
1184
1354
  this.persistMeta = options.persistMeta;
1185
1355
  this.state = options.state;
1186
1356
  }
1187
- async openCollaborativeDoc(docId, whenSyncedWithRemote) {
1188
- const doc = await this.ensureDoc(docId);
1189
- const refs = this.docRefs.get(docId) ?? 0;
1190
- this.docRefs.set(docId, refs + 1);
1191
- return new RepoDocHandleImpl(docId, doc, whenSyncedWithRemote, async () => this.onDocHandleClose(docId, doc));
1357
+ async openCollaborativeDoc(docId) {
1358
+ return await this.ensureDoc(docId);
1192
1359
  }
1193
1360
  async openDetachedDoc(docId) {
1194
- return new RepoDocHandleImpl(docId, await this.materializeDetachedDoc(docId), Promise.resolve(), async () => {});
1361
+ return await this.materializeDetachedDoc(docId);
1195
1362
  }
1196
1363
  async ensureDoc(docId) {
1197
1364
  const cached = this.docs.get(docId);
@@ -1207,7 +1374,7 @@ var DocManager = class {
1207
1374
  return stored;
1208
1375
  }
1209
1376
  }
1210
- const created = await this.docFactory(docId);
1377
+ const created = new LoroDoc();
1211
1378
  this.registerDoc(docId, created);
1212
1379
  return created;
1213
1380
  }
@@ -1233,27 +1400,42 @@ var DocManager = class {
1233
1400
  }
1234
1401
  }
1235
1402
  async updateDocFrontiers(docId, doc, defaultBy) {
1236
- const { json, key } = canonicalizeVersionVector(doc.version());
1403
+ const frontiers = doc.oplogFrontiers();
1404
+ const { json, key } = canonicalizeFrontiers(frontiers);
1237
1405
  const existingKeys = this.docFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
1238
1406
  let mutated = false;
1239
- if (existingKeys.size !== 1 || !existingKeys.has(key)) {
1240
- const metaFlock = this.metaFlock;
1241
- for (const entry of existingKeys) metaFlock.delete([
1242
- "f",
1243
- docId,
1244
- entry
1245
- ]);
1407
+ const metaFlock = this.metaFlock;
1408
+ const vv = doc.version();
1409
+ for (const entry of existingKeys) {
1410
+ if (entry === key) continue;
1411
+ let oldFrontiers;
1412
+ try {
1413
+ oldFrontiers = JSON.parse(entry);
1414
+ } catch {
1415
+ continue;
1416
+ }
1417
+ if (includesFrontiers(vv, oldFrontiers)) {
1418
+ metaFlock.delete([
1419
+ "f",
1420
+ docId,
1421
+ entry
1422
+ ]);
1423
+ mutated = true;
1424
+ }
1425
+ }
1426
+ if (!existingKeys.has(key)) {
1246
1427
  metaFlock.put([
1247
1428
  "f",
1248
1429
  docId,
1249
1430
  key
1250
1431
  ], json);
1251
- this.docFrontierKeys.set(docId, new Set([key]));
1252
1432
  mutated = true;
1253
1433
  }
1254
- if (mutated) await this.persistMeta();
1434
+ if (mutated) {
1435
+ this.refreshDocFrontierKeys(docId);
1436
+ await this.persistMeta();
1437
+ }
1255
1438
  const by = this.eventBus.resolveEventBy(defaultBy);
1256
- const frontiers = getDocFrontiers(doc);
1257
1439
  this.eventBus.emit({
1258
1440
  kind: "doc-frontiers",
1259
1441
  docId,
@@ -1274,20 +1456,33 @@ var DocManager = class {
1274
1456
  }
1275
1457
  return true;
1276
1458
  }
1459
+ async unloadDoc(docId) {
1460
+ const doc = this.docs.get(docId);
1461
+ if (!doc) return;
1462
+ await this.flushScheduledDocFrontierUpdate(docId);
1463
+ await this.persistDocUpdate(docId, doc);
1464
+ await this.updateDocFrontiers(docId, doc, "local");
1465
+ this.docSubscriptions.get(docId)?.();
1466
+ this.docSubscriptions.delete(docId);
1467
+ this.docs.delete(docId);
1468
+ this.docPersistedVersions.delete(docId);
1469
+ }
1470
+ async flush() {
1471
+ const promises$1 = [];
1472
+ for (const [docId, doc] of this.docs) promises$1.push((async () => {
1473
+ await this.persistDocUpdate(docId, doc);
1474
+ await this.flushScheduledDocFrontierUpdate(docId);
1475
+ })());
1476
+ await Promise.all(promises$1);
1477
+ }
1277
1478
  async close() {
1479
+ await this.flush();
1278
1480
  for (const unsubscribe of this.docSubscriptions.values()) try {
1279
1481
  unsubscribe();
1280
1482
  } catch {}
1281
1483
  this.docSubscriptions.clear();
1282
- const pendingDocIds = Array.from(this.docFrontierUpdates.keys());
1283
- for (const docId of pendingDocIds) try {
1284
- await this.flushScheduledDocFrontierUpdate(docId);
1285
- } catch (error) {
1286
- logAsyncError(`doc ${docId} frontier flush on close`)(error);
1287
- }
1288
1484
  this.docFrontierUpdates.clear();
1289
1485
  this.docs.clear();
1290
- this.docRefs.clear();
1291
1486
  this.docPersistedVersions.clear();
1292
1487
  this.docFrontierKeys.clear();
1293
1488
  }
@@ -1311,6 +1506,7 @@ var DocManager = class {
1311
1506
  const keys = /* @__PURE__ */ new Set();
1312
1507
  for (const row of rows) {
1313
1508
  if (!Array.isArray(row.key) || row.key.length < 3) continue;
1509
+ if (row.value === void 0 || row.value === null) continue;
1314
1510
  const frontierKey = row.key[2];
1315
1511
  if (typeof frontierKey === "string") keys.add(frontierKey);
1316
1512
  }
@@ -1365,22 +1561,10 @@ var DocManager = class {
1365
1561
  }
1366
1562
  })().catch(logAsyncError(`doc ${docId} frontier debounce`));
1367
1563
  }
1368
- async onDocHandleClose(docId, doc) {
1369
- const refs = this.docRefs.get(docId) ?? 0;
1370
- if (refs <= 1) {
1371
- this.docRefs.delete(docId);
1372
- this.docSubscriptions.get(docId)?.();
1373
- this.docSubscriptions.delete(docId);
1374
- this.docs.delete(docId);
1375
- this.docPersistedVersions.delete(docId);
1376
- } else this.docRefs.set(docId, refs - 1);
1377
- await this.persistDocUpdate(docId, doc);
1378
- if (!await this.flushScheduledDocFrontierUpdate(docId)) await this.updateDocFrontiers(docId, doc, "local");
1379
- }
1380
1564
  async materializeDetachedDoc(docId) {
1381
1565
  const snapshot = await this.exportDocSnapshot(docId);
1382
1566
  if (snapshot) return LoroDoc.fromSnapshot(snapshot);
1383
- return this.docFactory(docId);
1567
+ return new LoroDoc();
1384
1568
  }
1385
1569
  async exportDocSnapshot(docId) {
1386
1570
  const cached = this.docs.get(docId);
@@ -1390,7 +1574,7 @@ var DocManager = class {
1390
1574
  }
1391
1575
  async persistDocUpdate(docId, doc) {
1392
1576
  const previousVersion = this.docPersistedVersions.get(docId);
1393
- const nextVersion = doc.version();
1577
+ const nextVersion = doc.oplogVersion();
1394
1578
  if (!this.storage) {
1395
1579
  this.docPersistedVersions.set(docId, nextVersion);
1396
1580
  return;
@@ -1400,14 +1584,11 @@ var DocManager = class {
1400
1584
  this.docPersistedVersions.set(docId, nextVersion);
1401
1585
  return;
1402
1586
  }
1587
+ if (previousVersion.compare(nextVersion) === 0) return;
1403
1588
  const update = doc.export({
1404
1589
  mode: "update",
1405
1590
  from: previousVersion
1406
1591
  });
1407
- if (!update.length) {
1408
- this.docPersistedVersions.set(docId, nextVersion);
1409
- return;
1410
- }
1411
1592
  this.docPersistedVersions.set(docId, nextVersion);
1412
1593
  try {
1413
1594
  await this.storage.save({
@@ -1429,7 +1610,14 @@ var DocManager = class {
1429
1610
  return;
1430
1611
  }
1431
1612
  const flushed = this.flushScheduledDocFrontierUpdate(docId);
1432
- const updated = this.updateDocFrontiers(docId, doc, by);
1613
+ const updated = (async () => {
1614
+ this.eventBus.pushEventBy(by);
1615
+ try {
1616
+ await this.updateDocFrontiers(docId, doc, by);
1617
+ } finally {
1618
+ this.eventBus.popEventBy();
1619
+ }
1620
+ })();
1433
1621
  await Promise.all([
1434
1622
  persist,
1435
1623
  flushed,
@@ -1462,17 +1650,38 @@ var MetadataManager = class {
1462
1650
  const metadata = this.state.metadata.get(docId);
1463
1651
  return metadata ? cloneJsonObject(metadata) : void 0;
1464
1652
  }
1465
- list(query) {
1653
+ listDoc(query) {
1654
+ if (query?.limit !== void 0 && query.limit <= 0) return [];
1655
+ const { startKey, endKey } = this.computeDocRangeKeys(query);
1656
+ if (startKey && endKey && startKey >= endKey) return [];
1657
+ const scanOptions = { prefix: ["m"] };
1658
+ if (startKey) scanOptions.start = {
1659
+ kind: "inclusive",
1660
+ key: ["m", startKey]
1661
+ };
1662
+ if (endKey) scanOptions.end = {
1663
+ kind: "exclusive",
1664
+ key: ["m", endKey]
1665
+ };
1666
+ const rows = this.metaFlock.scan(scanOptions);
1667
+ const seen = /* @__PURE__ */ new Set();
1466
1668
  const entries = [];
1467
- for (const [docId, metadata] of this.state.metadata.entries()) {
1669
+ for (const row of rows) {
1670
+ if (query?.limit !== void 0 && entries.length >= query.limit) break;
1671
+ if (!Array.isArray(row.key) || row.key.length < 2) continue;
1672
+ const docId = row.key[1];
1673
+ if (typeof docId !== "string") continue;
1674
+ if (seen.has(docId)) continue;
1675
+ seen.add(docId);
1676
+ const metadata = this.state.metadata.get(docId);
1677
+ if (!metadata) continue;
1468
1678
  if (!matchesQuery(docId, metadata, query)) continue;
1469
1679
  entries.push({
1470
1680
  docId,
1471
1681
  meta: cloneJsonObject(metadata)
1472
1682
  });
1683
+ if (query?.limit !== void 0 && entries.length >= query.limit) break;
1473
1684
  }
1474
- entries.sort((a, b) => a.docId < b.docId ? -1 : a.docId > b.docId ? 1 : 0);
1475
- if (query?.limit !== void 0) return entries.slice(0, query.limit);
1476
1685
  return entries;
1477
1686
  }
1478
1687
  async upsert(docId, patch) {
@@ -1566,6 +1775,26 @@ var MetadataManager = class {
1566
1775
  clear() {
1567
1776
  this.state.metadata.clear();
1568
1777
  }
1778
+ computeDocRangeKeys(query) {
1779
+ if (!query) return {};
1780
+ const prefix = query.prefix && query.prefix.length > 0 ? query.prefix : void 0;
1781
+ let startKey = query.start;
1782
+ if (prefix) startKey = !startKey || prefix > startKey ? prefix : startKey;
1783
+ let endKey = query.end;
1784
+ const prefixEnd = this.nextLexicographicString(prefix);
1785
+ if (prefixEnd) endKey = !endKey || prefixEnd < endKey ? prefixEnd : endKey;
1786
+ return {
1787
+ startKey,
1788
+ endKey
1789
+ };
1790
+ }
1791
+ nextLexicographicString(value) {
1792
+ if (!value) return void 0;
1793
+ for (let i = value.length - 1; i >= 0; i -= 1) {
1794
+ const code = value.charCodeAt(i);
1795
+ if (code < 65535) return `${value.slice(0, i)}${String.fromCharCode(code + 1)}`;
1796
+ }
1797
+ }
1569
1798
  readDocMetadataFromFlock(docId) {
1570
1799
  const rows = this.metaFlock.scan({ prefix: ["m", docId] });
1571
1800
  if (!rows.length) return void 0;
@@ -1639,13 +1868,11 @@ var AssetManager = class {
1639
1868
  if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
1640
1869
  const existing = this.assets.get(assetId);
1641
1870
  if (existing) {
1642
- if (!existing.data) {
1643
- const clone = bytes.slice();
1644
- existing.data = clone;
1645
- if (this.storage) await this.storage.save({
1871
+ if (this.storage) {
1872
+ if (!await this.storage.loadAsset(assetId)) await this.storage.save({
1646
1873
  type: "asset",
1647
1874
  assetId,
1648
- data: clone.slice()
1875
+ data: bytes.slice()
1649
1876
  });
1650
1877
  }
1651
1878
  let metadataMutated = false;
@@ -1672,7 +1899,7 @@ var AssetManager = class {
1672
1899
  await this.persistMeta();
1673
1900
  this.eventBus.emit({
1674
1901
  kind: "asset-metadata",
1675
- asset: this.createAssetDownload(assetId, metadata$1, existing.data),
1902
+ asset: this.createAssetDownload(assetId, metadata$1, bytes),
1676
1903
  by: "local"
1677
1904
  });
1678
1905
  }
@@ -1702,7 +1929,8 @@ var AssetManager = class {
1702
1929
  assetId,
1703
1930
  data: storedBytes.slice()
1704
1931
  });
1705
- this.rememberAsset(metadata, storedBytes);
1932
+ this.rememberAsset(metadata);
1933
+ this.markAssetAsOrphan(assetId, metadata);
1706
1934
  this.updateDocAssetMetadata(assetId, metadata);
1707
1935
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
1708
1936
  await this.persistMeta();
@@ -1723,13 +1951,11 @@ var AssetManager = class {
1723
1951
  const existing = this.assets.get(assetId);
1724
1952
  if (existing) {
1725
1953
  metadata = existing.metadata;
1726
- if (!existing.data) {
1727
- const clone = bytes.slice();
1728
- existing.data = clone;
1729
- if (this.storage) await this.storage.save({
1954
+ if (this.storage) {
1955
+ if (!await this.storage.loadAsset(assetId)) await this.storage.save({
1730
1956
  type: "asset",
1731
1957
  assetId,
1732
- data: clone.slice()
1958
+ data: bytes.slice()
1733
1959
  });
1734
1960
  }
1735
1961
  let nextMetadata = metadata;
@@ -1769,11 +1995,11 @@ var AssetManager = class {
1769
1995
  await this.persistMeta();
1770
1996
  this.eventBus.emit({
1771
1997
  kind: "asset-metadata",
1772
- asset: this.createAssetDownload(assetId, metadata, existing.data),
1998
+ asset: this.createAssetDownload(assetId, metadata, bytes),
1773
1999
  by: "local"
1774
2000
  });
1775
2001
  } else metadata = existing.metadata;
1776
- storedBytes = existing.data;
2002
+ storedBytes = bytes;
1777
2003
  this.rememberAsset(metadata);
1778
2004
  } else {
1779
2005
  metadata = {
@@ -1799,7 +2025,7 @@ var AssetManager = class {
1799
2025
  assetId,
1800
2026
  data: storedBytes.slice()
1801
2027
  });
1802
- this.rememberAsset(metadata, storedBytes);
2028
+ this.rememberAsset(metadata);
1803
2029
  this.updateDocAssetMetadata(assetId, metadata);
1804
2030
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
1805
2031
  created = true;
@@ -1807,9 +2033,7 @@ var AssetManager = class {
1807
2033
  const mapping = this.docAssets.get(docId) ?? /* @__PURE__ */ new Map();
1808
2034
  mapping.set(assetId, metadata);
1809
2035
  this.docAssets.set(docId, mapping);
1810
- const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
1811
- refs.add(docId);
1812
- this.assetToDocRefs.set(assetId, refs);
2036
+ this.addDocReference(assetId, docId);
1813
2037
  this.metaFlock.put([
1814
2038
  "ld",
1815
2039
  docId,
@@ -1839,20 +2063,7 @@ var AssetManager = class {
1839
2063
  docId,
1840
2064
  assetId
1841
2065
  ]);
1842
- const refs = this.assetToDocRefs.get(assetId);
1843
- if (refs) {
1844
- refs.delete(docId);
1845
- if (refs.size === 0) {
1846
- this.assetToDocRefs.delete(assetId);
1847
- const record = this.assets.get(assetId);
1848
- if (record) this.orphanedAssets.set(assetId, {
1849
- metadata: record.metadata,
1850
- deletedAt: Date.now()
1851
- });
1852
- this.metaFlock.delete(["a", assetId]);
1853
- this.assets.delete(assetId);
1854
- }
1855
- }
2066
+ this.removeDocAssetReference(assetId, docId);
1856
2067
  await this.persistMeta();
1857
2068
  this.eventBus.emit({
1858
2069
  kind: "asset-unlink",
@@ -1939,12 +2150,11 @@ var AssetManager = class {
1939
2150
  this.handleAssetRemoval(assetId, by);
1940
2151
  return;
1941
2152
  }
1942
- const existingData = previous?.data;
1943
- this.rememberAsset(metadata, existingData);
2153
+ this.rememberAsset(metadata);
1944
2154
  this.updateDocAssetMetadata(assetId, cloneRepoAssetMetadata(metadata));
1945
2155
  if (!previous || !assetMetadataEqual(previous.metadata, metadata)) this.eventBus.emit({
1946
2156
  kind: "asset-metadata",
1947
- asset: this.createAssetDownload(assetId, metadata, existingData),
2157
+ asset: this.createAssetDownload(assetId, metadata),
1948
2158
  by
1949
2159
  });
1950
2160
  }
@@ -1959,11 +2169,7 @@ var AssetManager = class {
1959
2169
  if (typeof assetId !== "string") continue;
1960
2170
  const metadata = assetMetaFromJson(row.value);
1961
2171
  if (!metadata) continue;
1962
- const existing = this.assets.get(assetId);
1963
- nextAssets.set(assetId, {
1964
- metadata,
1965
- data: existing?.data
1966
- });
2172
+ nextAssets.set(assetId, { metadata });
1967
2173
  }
1968
2174
  const nextDocAssets = /* @__PURE__ */ new Map();
1969
2175
  const linkRows = this.metaFlock.scan({ prefix: ["ld"] });
@@ -1999,12 +2205,18 @@ var AssetManager = class {
1999
2205
  this.assetToDocRefs.set(assetId, refs);
2000
2206
  }
2001
2207
  this.assets.clear();
2002
- for (const record of nextAssets.values()) this.rememberAsset(record.metadata, record.data);
2208
+ for (const record of nextAssets.values()) this.rememberAsset(record.metadata);
2209
+ for (const assetId of nextAssets.keys()) {
2210
+ const refs = this.assetToDocRefs.get(assetId);
2211
+ if (!refs || refs.size === 0) {
2212
+ if (!this.orphanedAssets.has(assetId)) this.markAssetAsOrphan(assetId, nextAssets.get(assetId).metadata);
2213
+ } else this.orphanedAssets.delete(assetId);
2214
+ }
2003
2215
  for (const [assetId, record] of nextAssets) {
2004
2216
  const previous = prevAssets.get(assetId)?.metadata;
2005
2217
  if (!assetMetadataEqual(previous, record.metadata)) this.eventBus.emit({
2006
2218
  kind: "asset-metadata",
2007
- asset: this.createAssetDownload(assetId, record.metadata, record.data),
2219
+ asset: this.createAssetDownload(assetId, record.metadata),
2008
2220
  by
2009
2221
  });
2010
2222
  }
@@ -2100,36 +2312,24 @@ var AssetManager = class {
2100
2312
  };
2101
2313
  }
2102
2314
  async materializeAsset(assetId) {
2103
- let record = this.assets.get(assetId);
2104
- if (record?.data) return {
2105
- metadata: record.metadata,
2106
- bytes: record.data.slice()
2107
- };
2315
+ const record = this.assets.get(assetId);
2108
2316
  if (record && this.storage) {
2109
2317
  const stored = await this.storage.loadAsset(assetId);
2110
- if (stored) {
2111
- const clone = stored.slice();
2112
- record.data = clone.slice();
2113
- return {
2114
- metadata: record.metadata,
2115
- bytes: clone
2116
- };
2117
- }
2318
+ if (stored) return {
2319
+ metadata: record.metadata,
2320
+ bytes: stored
2321
+ };
2118
2322
  }
2119
2323
  if (!record && this.storage) {
2120
2324
  const stored = await this.storage.loadAsset(assetId);
2121
2325
  if (stored) {
2122
2326
  const metadata$1 = this.getAssetMetadata(assetId);
2123
2327
  if (!metadata$1) throw new Error(`Missing metadata for asset ${assetId}`);
2124
- const clone = stored.slice();
2125
- this.assets.set(assetId, {
2126
- metadata: metadata$1,
2127
- data: clone.slice()
2128
- });
2328
+ this.assets.set(assetId, { metadata: metadata$1 });
2129
2329
  this.updateDocAssetMetadata(assetId, metadata$1);
2130
2330
  return {
2131
2331
  metadata: metadata$1,
2132
- bytes: clone
2332
+ bytes: stored
2133
2333
  };
2134
2334
  }
2135
2335
  }
@@ -2145,10 +2345,7 @@ var AssetManager = class {
2145
2345
  ...remote.policy ? { policy: remote.policy } : {},
2146
2346
  ...remote.tag ? { tag: remote.tag } : {}
2147
2347
  };
2148
- this.assets.set(assetId, {
2149
- metadata,
2150
- data: remoteBytes.slice()
2151
- });
2348
+ this.assets.set(assetId, { metadata });
2152
2349
  this.updateDocAssetMetadata(assetId, metadata);
2153
2350
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
2154
2351
  await this.persistMeta();
@@ -2170,18 +2367,14 @@ var AssetManager = class {
2170
2367
  if (assets) assets.set(assetId, metadata);
2171
2368
  }
2172
2369
  }
2173
- rememberAsset(metadata, bytes) {
2174
- const data = bytes ? bytes.slice() : this.assets.get(metadata.assetId)?.data;
2175
- this.assets.set(metadata.assetId, {
2176
- metadata,
2177
- data
2178
- });
2179
- this.orphanedAssets.delete(metadata.assetId);
2370
+ rememberAsset(metadata) {
2371
+ this.assets.set(metadata.assetId, { metadata });
2180
2372
  }
2181
2373
  addDocReference(assetId, docId) {
2182
2374
  const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
2183
2375
  refs.add(docId);
2184
2376
  this.assetToDocRefs.set(assetId, refs);
2377
+ this.orphanedAssets.delete(assetId);
2185
2378
  }
2186
2379
  removeDocAssetReference(assetId, docId) {
2187
2380
  const refs = this.assetToDocRefs.get(assetId);
@@ -2304,6 +2497,9 @@ var FlockHydrator = class {
2304
2497
 
2305
2498
  //#endregion
2306
2499
  //#region src/internal/sync-runner.ts
2500
+ /**
2501
+ * Sync data between storage and transport layer
2502
+ */
2307
2503
  var SyncRunner = class {
2308
2504
  storage;
2309
2505
  transport;
@@ -2318,6 +2514,7 @@ var SyncRunner = class {
2318
2514
  readyPromise;
2319
2515
  metaRoomSubscription;
2320
2516
  unsubscribeMetaFlock;
2517
+ docSubscriptions = /* @__PURE__ */ new Map();
2321
2518
  constructor(options) {
2322
2519
  this.storage = options.storage;
2323
2520
  this.transport = options.transport;
@@ -2327,7 +2524,7 @@ var SyncRunner = class {
2327
2524
  this.assetManager = options.assetManager;
2328
2525
  this.flockHydrator = options.flockHydrator;
2329
2526
  this.getMetaFlock = options.getMetaFlock;
2330
- this.replaceMetaFlock = options.replaceMetaFlock;
2527
+ this.replaceMetaFlock = options.mergeFlock;
2331
2528
  this.persistMeta = options.persistMeta;
2332
2529
  }
2333
2530
  async ready() {
@@ -2404,15 +2601,30 @@ var SyncRunner = class {
2404
2601
  await this.ready();
2405
2602
  if (!this.transport) throw new Error("Transport adapter not configured");
2406
2603
  if (!this.transport.isConnected()) await this.transport.connect();
2604
+ const existing = this.docSubscriptions.get(docId);
2605
+ if (existing) return existing;
2407
2606
  const doc = await this.docManager.ensureDoc(docId);
2408
2607
  const subscription = this.transport.joinDocRoom(docId, doc, params);
2608
+ const wrapped = {
2609
+ unsubscribe: () => {
2610
+ subscription.unsubscribe();
2611
+ if (this.docSubscriptions.get(docId) === wrapped) this.docSubscriptions.delete(docId);
2612
+ },
2613
+ firstSyncedWithRemote: subscription.firstSyncedWithRemote,
2614
+ get connected() {
2615
+ return subscription.connected;
2616
+ }
2617
+ };
2618
+ this.docSubscriptions.set(docId, wrapped);
2409
2619
  subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
2410
- return subscription;
2620
+ return wrapped;
2411
2621
  }
2412
- async close() {
2622
+ async destroy() {
2413
2623
  await this.docManager.close();
2414
2624
  this.metaRoomSubscription?.unsubscribe();
2415
2625
  this.metaRoomSubscription = void 0;
2626
+ for (const sub of this.docSubscriptions.values()) sub.unsubscribe();
2627
+ this.docSubscriptions.clear();
2416
2628
  if (this.unsubscribeMetaFlock) {
2417
2629
  this.unsubscribeMetaFlock();
2418
2630
  this.unsubscribeMetaFlock = void 0;
@@ -2463,16 +2675,17 @@ function createRepoState() {
2463
2675
  //#region src/index.ts
2464
2676
  const textEncoder = new TextEncoder();
2465
2677
  const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
2466
- var LoroRepo = class {
2678
+ var LoroRepo = class LoroRepo {
2467
2679
  options;
2680
+ _destroyed = false;
2468
2681
  transport;
2469
2682
  storage;
2470
- docFactory;
2471
2683
  metaFlock = new Flock();
2472
2684
  eventBus;
2473
2685
  docManager;
2474
2686
  metadataManager;
2475
2687
  assetManager;
2688
+ assetTransport;
2476
2689
  flockHydrator;
2477
2690
  state;
2478
2691
  syncRunner;
@@ -2480,14 +2693,13 @@ var LoroRepo = class {
2480
2693
  this.options = options;
2481
2694
  this.transport = options.transportAdapter;
2482
2695
  this.storage = options.storageAdapter;
2483
- this.docFactory = options.docFactory ?? (async () => new LoroDoc());
2696
+ this.assetTransport = options.assetTransportAdapter;
2484
2697
  this.eventBus = new RepoEventBus();
2485
2698
  this.state = createRepoState();
2486
2699
  const configuredDebounce = options.docFrontierDebounceMs;
2487
2700
  const docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
2488
2701
  this.docManager = new DocManager({
2489
2702
  storage: this.storage,
2490
- docFactory: this.docFactory,
2491
2703
  docFrontierDebounceMs,
2492
2704
  getMetaFlock: () => this.metaFlock,
2493
2705
  eventBus: this.eventBus,
@@ -2502,7 +2714,7 @@ var LoroRepo = class {
2502
2714
  });
2503
2715
  this.assetManager = new AssetManager({
2504
2716
  storage: this.storage,
2505
- assetTransport: options.assetTransportAdapter,
2717
+ assetTransport: this.assetTransport,
2506
2718
  getMetaFlock: () => this.metaFlock,
2507
2719
  eventBus: this.eventBus,
2508
2720
  persistMeta: () => this.persistMeta(),
@@ -2523,96 +2735,133 @@ var LoroRepo = class {
2523
2735
  assetManager: this.assetManager,
2524
2736
  flockHydrator: this.flockHydrator,
2525
2737
  getMetaFlock: () => this.metaFlock,
2526
- replaceMetaFlock: (snapshot) => {
2527
- this.metaFlock = snapshot;
2738
+ mergeFlock: (snapshot) => {
2739
+ this.metaFlock.merge(snapshot);
2528
2740
  },
2529
2741
  persistMeta: () => this.persistMeta()
2530
2742
  });
2531
2743
  }
2744
+ static async create(options) {
2745
+ const repo = new LoroRepo(options);
2746
+ await repo.storage?.init?.();
2747
+ await repo.ready();
2748
+ return repo;
2749
+ }
2750
+ /**
2751
+ * Load meta from storage.
2752
+ *
2753
+ * You need to call this before all other operations to make the app functioning correctly.
2754
+ * Though we do that implicitly already
2755
+ */
2532
2756
  async ready() {
2533
2757
  await this.syncRunner.ready();
2534
2758
  }
2759
+ /**
2760
+ * Sync selected data via the transport adaptor
2761
+ * @param options
2762
+ */
2535
2763
  async sync(options = {}) {
2536
2764
  await this.syncRunner.sync(options);
2537
2765
  }
2766
+ /**
2767
+ * Start syncing the metadata (Flock) room. It will establish a realtime connection to the transport adaptor.
2768
+ * All changes on the room will be synced to the Flock, and all changes on the Flock will be synced to the room.
2769
+ * @param params
2770
+ * @returns
2771
+ */
2538
2772
  async joinMetaRoom(params) {
2539
2773
  return this.syncRunner.joinMetaRoom(params);
2540
2774
  }
2775
+ /**
2776
+ * Start syncing the given doc. It will establish a realtime connection to the transport adaptor.
2777
+ * All changes on the doc will be synced to the transport, and all changes on the transport will be synced to the doc.
2778
+ *
2779
+ * All the changes on the room will be reflected on the same doc you get from `repo.openCollaborativeDoc(docId)`
2780
+ * @param docId
2781
+ * @param params
2782
+ * @returns
2783
+ */
2541
2784
  async joinDocRoom(docId, params) {
2542
2785
  return this.syncRunner.joinDocRoom(docId, params);
2543
2786
  }
2544
- async close() {
2545
- await this.syncRunner.close();
2787
+ /**
2788
+ * Opens a document that is automatically persisted to the configured storage adapter.
2789
+ *
2790
+ * - Edits are saved to storage (debounced).
2791
+ * - Frontiers are synced to the metadata (Flock).
2792
+ * - Realtime collaboration is NOT enabled by default; use `joinDocRoom` to connect.
2793
+ */
2794
+ async openPersistedDoc(docId) {
2795
+ return {
2796
+ doc: await this.docManager.openCollaborativeDoc(docId),
2797
+ syncOnce: () => {
2798
+ return this.sync({
2799
+ scope: "doc",
2800
+ docIds: [docId]
2801
+ });
2802
+ },
2803
+ joinRoom: (auth) => {
2804
+ return this.syncRunner.joinDocRoom(docId, { auth });
2805
+ }
2806
+ };
2546
2807
  }
2547
- async upsertDocMeta(docId, patch, _options = {}) {
2548
- await this.ready();
2808
+ async upsertDocMeta(docId, patch) {
2549
2809
  await this.metadataManager.upsert(docId, patch);
2550
2810
  }
2551
2811
  async getDocMeta(docId) {
2552
- await this.ready();
2553
2812
  return this.metadataManager.get(docId);
2554
2813
  }
2555
2814
  async listDoc(query) {
2556
- await this.ready();
2557
- return this.metadataManager.list(query);
2815
+ return this.metadataManager.listDoc(query);
2558
2816
  }
2559
- getMetaReplica() {
2817
+ getMeta() {
2560
2818
  return this.metaFlock;
2561
2819
  }
2562
2820
  watch(listener, filter = {}) {
2563
2821
  return this.eventBus.watch(listener, filter);
2564
2822
  }
2565
2823
  /**
2566
- * Opens the repo-managed collaborative document, registers it for persistence,
2567
- * and schedules a doc-level sync so `whenSyncedWithRemote` resolves after remote backfills.
2824
+ * Opens a detached `LoroDoc` snapshot.
2825
+ *
2826
+ * - **No Persistence**: Edits to this document are NOT saved to storage.
2827
+ * - **No Sync**: This document does not participate in realtime updates.
2828
+ * - **Use Case**: Ideal for read-only history inspection, temporary drafts, or conflict resolution without affecting the main state.
2568
2829
  */
2569
- async openCollaborativeDoc(docId) {
2570
- await this.ready();
2571
- const whenSyncedWithRemote = this.whenDocInSyncWithRemote(docId);
2572
- return this.docManager.openCollaborativeDoc(docId, whenSyncedWithRemote);
2830
+ async openDetachedDoc(docId) {
2831
+ return this.docManager.openDetachedDoc(docId);
2573
2832
  }
2574
2833
  /**
2575
- * Opens a detached `LoroDoc` snapshot that never registers with the repo, meaning
2576
- * it neither participates in remote subscriptions nor persists edits back to storage.
2834
+ * Explicitly unloads a document from memory.
2835
+ *
2836
+ * - **Persists Immediately**: Forces a save of the document's current state to storage.
2837
+ * - **Frees Memory**: Removes the document from the internal cache.
2838
+ * - **Note**: If the document is currently being synced (via `joinDocRoom`), you should also unsubscribe from the room to fully release resources.
2577
2839
  */
2578
- async openDetachedDoc(docId) {
2579
- await this.ready();
2580
- return this.docManager.openDetachedDoc(docId);
2840
+ async unloadDoc(docId) {
2841
+ await this.docManager.unloadDoc(docId);
2842
+ }
2843
+ async flush() {
2844
+ await this.docManager.flush();
2581
2845
  }
2582
2846
  async uploadAsset(params) {
2583
- await this.ready();
2584
2847
  return this.assetManager.uploadAsset(params);
2585
2848
  }
2586
- async whenDocInSyncWithRemote(docId) {
2587
- await this.ready();
2588
- await this.docManager.ensureDoc(docId);
2589
- await this.sync({
2590
- scope: "doc",
2591
- docIds: [docId]
2592
- });
2593
- }
2594
2849
  async linkAsset(docId, params) {
2595
- await this.ready();
2596
2850
  return this.assetManager.linkAsset(docId, params);
2597
2851
  }
2598
2852
  async fetchAsset(assetId) {
2599
- await this.ready();
2600
2853
  return this.assetManager.fetchAsset(assetId);
2601
2854
  }
2602
2855
  async unlinkAsset(docId, assetId) {
2603
- await this.ready();
2604
2856
  await this.assetManager.unlinkAsset(docId, assetId);
2605
2857
  }
2606
2858
  async listAssets(docId) {
2607
- await this.ready();
2608
2859
  return this.assetManager.listAssets(docId);
2609
2860
  }
2610
2861
  async ensureAsset(assetId) {
2611
- await this.ready();
2612
2862
  return this.assetManager.ensureAsset(assetId);
2613
2863
  }
2614
2864
  async gcAssets(options = {}) {
2615
- await this.ready();
2616
2865
  return this.assetManager.gcAssets(options);
2617
2866
  }
2618
2867
  async persistMeta() {
@@ -2624,6 +2873,17 @@ var LoroRepo = class {
2624
2873
  update: encoded
2625
2874
  });
2626
2875
  }
2876
+ get destroyed() {
2877
+ return this._destroyed;
2878
+ }
2879
+ async destroy() {
2880
+ if (this._destroyed) return;
2881
+ this._destroyed = true;
2882
+ await this.syncRunner.destroy();
2883
+ this.assetTransport?.close?.();
2884
+ this.storage?.close?.();
2885
+ await this.transport?.close();
2886
+ }
2627
2887
  };
2628
2888
 
2629
2889
  //#endregion