loro-repo 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index.d.cts → index-DsCaL9JX.d.cts} +101 -49
- package/dist/{index.d.ts → index-tq65q3qY.d.ts} +102 -50
- package/dist/index.cjs +463 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +453 -158
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/dist/index.cjs
CHANGED
|
@@ -6,12 +6,16 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
8
|
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
15
19
|
}
|
|
16
20
|
return to;
|
|
17
21
|
};
|
|
@@ -22,10 +26,10 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
26
|
|
|
23
27
|
//#endregion
|
|
24
28
|
let __loro_dev_flock = require("@loro-dev/flock");
|
|
25
|
-
let loro_crdt = require("loro-crdt");
|
|
26
29
|
let loro_adaptors = require("loro-adaptors");
|
|
27
30
|
let loro_protocol = require("loro-protocol");
|
|
28
31
|
let loro_websocket = require("loro-websocket");
|
|
32
|
+
let loro_crdt = require("loro-crdt");
|
|
29
33
|
let node_fs = require("node:fs");
|
|
30
34
|
let node_path = require("node:path");
|
|
31
35
|
node_path = __toESM(node_path);
|
|
@@ -36,8 +40,47 @@ function createRepoFlockAdaptorFromDoc(flock, config = {}) {
|
|
|
36
40
|
return new loro_adaptors.FlockAdaptor(flock, config);
|
|
37
41
|
}
|
|
38
42
|
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/internal/debug.ts
|
|
45
|
+
const getEnv = () => {
|
|
46
|
+
if (typeof globalThis !== "object" || globalThis === null) return;
|
|
47
|
+
return globalThis.process?.env;
|
|
48
|
+
};
|
|
49
|
+
const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
|
|
50
|
+
const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
|
|
51
|
+
const wildcardTokens = new Set([
|
|
52
|
+
"*",
|
|
53
|
+
"1",
|
|
54
|
+
"true",
|
|
55
|
+
"all"
|
|
56
|
+
]);
|
|
57
|
+
const namespaceSet = new Set(normalizedNamespaces);
|
|
58
|
+
const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
|
|
59
|
+
const isDebugEnabled = (namespace) => {
|
|
60
|
+
if (!namespaceSet.size) return false;
|
|
61
|
+
if (!namespace) return hasWildcard;
|
|
62
|
+
const normalized = namespace.toLowerCase();
|
|
63
|
+
if (hasWildcard) return true;
|
|
64
|
+
if (namespaceSet.has(normalized)) return true;
|
|
65
|
+
const [root] = normalized.split(":");
|
|
66
|
+
return namespaceSet.has(root);
|
|
67
|
+
};
|
|
68
|
+
const createDebugLogger = (namespace) => {
|
|
69
|
+
const normalized = namespace.toLowerCase();
|
|
70
|
+
return (...args) => {
|
|
71
|
+
if (!isDebugEnabled(normalized)) return;
|
|
72
|
+
const prefix = `[loro-repo:${namespace}]`;
|
|
73
|
+
if (args.length === 0) {
|
|
74
|
+
console.info(prefix);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.info(prefix, ...args);
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
39
81
|
//#endregion
|
|
40
82
|
//#region src/transport/websocket.ts
|
|
83
|
+
const debug = createDebugLogger("transport:websocket");
|
|
41
84
|
function withTimeout(promise, timeoutMs) {
|
|
42
85
|
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
43
86
|
return new Promise((resolve, reject) => {
|
|
@@ -82,30 +125,51 @@ var WebSocketTransportAdapter = class {
|
|
|
82
125
|
}
|
|
83
126
|
async connect(_options) {
|
|
84
127
|
const client = this.ensureClient();
|
|
85
|
-
|
|
86
|
-
|
|
128
|
+
debug("connect requested", { status: client.getStatus() });
|
|
129
|
+
try {
|
|
130
|
+
await client.connect();
|
|
131
|
+
debug("client.connect resolved");
|
|
132
|
+
await client.waitConnected();
|
|
133
|
+
debug("client.waitConnected resolved", { status: client.getStatus() });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
debug("connect failed", error);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
87
138
|
}
|
|
88
139
|
async close() {
|
|
140
|
+
debug("close requested", {
|
|
141
|
+
docSessions: this.docSessions.size,
|
|
142
|
+
metadataSession: Boolean(this.metadataSession)
|
|
143
|
+
});
|
|
89
144
|
for (const [docId] of this.docSessions) await this.leaveDocSession(docId).catch(() => {});
|
|
90
145
|
this.docSessions.clear();
|
|
91
146
|
await this.teardownMetadataSession().catch(() => {});
|
|
92
147
|
if (this.client) {
|
|
93
|
-
this.client
|
|
148
|
+
const client = this.client;
|
|
94
149
|
this.client = void 0;
|
|
150
|
+
client.destroy();
|
|
151
|
+
debug("websocket client destroyed");
|
|
95
152
|
}
|
|
153
|
+
debug("close completed");
|
|
96
154
|
}
|
|
97
155
|
isConnected() {
|
|
98
156
|
return this.client?.getStatus() === "connected";
|
|
99
157
|
}
|
|
100
158
|
async syncMeta(flock, options) {
|
|
101
|
-
if (!this.options.metadataRoomId)
|
|
159
|
+
if (!this.options.metadataRoomId) {
|
|
160
|
+
debug("syncMeta skipped; metadata room not configured");
|
|
161
|
+
return { ok: true };
|
|
162
|
+
}
|
|
163
|
+
debug("syncMeta requested", { roomId: this.options.metadataRoomId });
|
|
102
164
|
try {
|
|
103
165
|
await withTimeout((await this.ensureMetadataSession(flock, {
|
|
104
166
|
roomId: this.options.metadataRoomId,
|
|
105
167
|
auth: this.options.metadataAuth
|
|
106
168
|
})).firstSynced, options?.timeout);
|
|
169
|
+
debug("syncMeta completed", { roomId: this.options.metadataRoomId });
|
|
107
170
|
return { ok: true };
|
|
108
|
-
} catch {
|
|
171
|
+
} catch (error) {
|
|
172
|
+
debug("syncMeta failed", error);
|
|
109
173
|
return { ok: false };
|
|
110
174
|
}
|
|
111
175
|
}
|
|
@@ -114,6 +178,10 @@ var WebSocketTransportAdapter = class {
|
|
|
114
178
|
const roomId = normalizeRoomId(params?.roomId, fallback);
|
|
115
179
|
if (!roomId) throw new Error("Metadata room id not configured");
|
|
116
180
|
const auth = params?.auth ?? this.options.metadataAuth;
|
|
181
|
+
debug("joinMetaRoom requested", {
|
|
182
|
+
roomId,
|
|
183
|
+
hasAuth: Boolean(auth && auth.length)
|
|
184
|
+
});
|
|
117
185
|
const ensure = this.ensureMetadataSession(flock, {
|
|
118
186
|
roomId,
|
|
119
187
|
auth
|
|
@@ -124,7 +192,14 @@ var WebSocketTransportAdapter = class {
|
|
|
124
192
|
unsubscribe: () => {
|
|
125
193
|
ensure.then((session) => {
|
|
126
194
|
session.refCount = Math.max(0, session.refCount - 1);
|
|
127
|
-
|
|
195
|
+
debug("metadata session refCount decremented", {
|
|
196
|
+
roomId: session.roomId,
|
|
197
|
+
refCount: session.refCount
|
|
198
|
+
});
|
|
199
|
+
if (session.refCount === 0) {
|
|
200
|
+
debug("tearing down metadata session due to refCount=0", { roomId: session.roomId });
|
|
201
|
+
this.teardownMetadataSession(session).catch(() => {});
|
|
202
|
+
}
|
|
128
203
|
});
|
|
129
204
|
},
|
|
130
205
|
firstSyncedWithRemote: firstSynced,
|
|
@@ -134,18 +209,37 @@ var WebSocketTransportAdapter = class {
|
|
|
134
209
|
};
|
|
135
210
|
ensure.then((session) => {
|
|
136
211
|
session.refCount += 1;
|
|
212
|
+
debug("metadata session refCount incremented", {
|
|
213
|
+
roomId: session.roomId,
|
|
214
|
+
refCount: session.refCount
|
|
215
|
+
});
|
|
137
216
|
});
|
|
138
217
|
return subscription;
|
|
139
218
|
}
|
|
140
219
|
async syncDoc(docId, doc, options) {
|
|
220
|
+
debug("syncDoc requested", { docId });
|
|
141
221
|
try {
|
|
142
|
-
|
|
222
|
+
const session = await this.ensureDocSession(docId, doc, {});
|
|
223
|
+
await withTimeout(session.firstSynced, options?.timeout);
|
|
224
|
+
debug("syncDoc completed", {
|
|
225
|
+
docId,
|
|
226
|
+
roomId: session.roomId
|
|
227
|
+
});
|
|
143
228
|
return { ok: true };
|
|
144
|
-
} catch {
|
|
229
|
+
} catch (error) {
|
|
230
|
+
debug("syncDoc failed", {
|
|
231
|
+
docId,
|
|
232
|
+
error
|
|
233
|
+
});
|
|
145
234
|
return { ok: false };
|
|
146
235
|
}
|
|
147
236
|
}
|
|
148
237
|
joinDocRoom(docId, doc, params) {
|
|
238
|
+
debug("joinDocRoom requested", {
|
|
239
|
+
docId,
|
|
240
|
+
roomParamType: params?.roomId ? typeof params.roomId === "string" ? "string" : "uint8array" : void 0,
|
|
241
|
+
hasAuthOverride: Boolean(params?.auth && params.auth.length)
|
|
242
|
+
});
|
|
149
243
|
const ensure = this.ensureDocSession(docId, doc, params ?? {});
|
|
150
244
|
const firstSynced = ensure.then((session) => session.firstSynced);
|
|
151
245
|
const getConnected = () => this.isConnected();
|
|
@@ -153,6 +247,11 @@ var WebSocketTransportAdapter = class {
|
|
|
153
247
|
unsubscribe: () => {
|
|
154
248
|
ensure.then((session) => {
|
|
155
249
|
session.refCount = Math.max(0, session.refCount - 1);
|
|
250
|
+
debug("doc session refCount decremented", {
|
|
251
|
+
docId,
|
|
252
|
+
roomId: session.roomId,
|
|
253
|
+
refCount: session.refCount
|
|
254
|
+
});
|
|
156
255
|
if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
|
|
157
256
|
});
|
|
158
257
|
},
|
|
@@ -163,12 +262,24 @@ var WebSocketTransportAdapter = class {
|
|
|
163
262
|
};
|
|
164
263
|
ensure.then((session) => {
|
|
165
264
|
session.refCount += 1;
|
|
265
|
+
debug("doc session refCount incremented", {
|
|
266
|
+
docId,
|
|
267
|
+
roomId: session.roomId,
|
|
268
|
+
refCount: session.refCount
|
|
269
|
+
});
|
|
166
270
|
});
|
|
167
271
|
return subscription;
|
|
168
272
|
}
|
|
169
273
|
ensureClient() {
|
|
170
|
-
if (this.client)
|
|
274
|
+
if (this.client) {
|
|
275
|
+
debug("reusing websocket client", { status: this.client.getStatus() });
|
|
276
|
+
return this.client;
|
|
277
|
+
}
|
|
171
278
|
const { url, client: clientOptions } = this.options;
|
|
279
|
+
debug("creating websocket client", {
|
|
280
|
+
url,
|
|
281
|
+
clientOptionsKeys: clientOptions ? Object.keys(clientOptions) : []
|
|
282
|
+
});
|
|
172
283
|
const client = new loro_websocket.LoroWebsocketClient({
|
|
173
284
|
url,
|
|
174
285
|
...clientOptions
|
|
@@ -177,22 +288,49 @@ var WebSocketTransportAdapter = class {
|
|
|
177
288
|
return client;
|
|
178
289
|
}
|
|
179
290
|
async ensureMetadataSession(flock, params) {
|
|
291
|
+
debug("ensureMetadataSession invoked", {
|
|
292
|
+
roomId: params.roomId,
|
|
293
|
+
hasAuth: Boolean(params.auth && params.auth.length)
|
|
294
|
+
});
|
|
180
295
|
const client = this.ensureClient();
|
|
181
296
|
await client.waitConnected();
|
|
182
|
-
|
|
183
|
-
if (this.metadataSession
|
|
297
|
+
debug("websocket client ready for metadata session", { status: client.getStatus() });
|
|
298
|
+
if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) {
|
|
299
|
+
debug("reusing metadata session", {
|
|
300
|
+
roomId: this.metadataSession.roomId,
|
|
301
|
+
refCount: this.metadataSession.refCount
|
|
302
|
+
});
|
|
303
|
+
return this.metadataSession;
|
|
304
|
+
}
|
|
305
|
+
if (this.metadataSession) {
|
|
306
|
+
debug("tearing down previous metadata session", { roomId: this.metadataSession.roomId });
|
|
307
|
+
await this.teardownMetadataSession(this.metadataSession).catch(() => {});
|
|
308
|
+
}
|
|
184
309
|
const configuredType = this.options.metadataCrdtType;
|
|
185
310
|
if (configuredType && configuredType !== loro_protocol.CrdtType.Flock) throw new Error(`metadataCrdtType must be ${loro_protocol.CrdtType.Flock} when syncing Flock metadata`);
|
|
186
311
|
const adaptor = createRepoFlockAdaptorFromDoc(flock, this.options.metadataAdaptorConfig ?? {});
|
|
312
|
+
debug("joining metadata room", {
|
|
313
|
+
roomId: params.roomId,
|
|
314
|
+
hasAuth: Boolean(params.auth && params.auth.length)
|
|
315
|
+
});
|
|
187
316
|
const room = await client.join({
|
|
188
317
|
roomId: params.roomId,
|
|
189
318
|
crdtAdaptor: adaptor,
|
|
190
319
|
auth: params.auth
|
|
191
320
|
});
|
|
321
|
+
const firstSynced = room.waitForReachingServerVersion();
|
|
322
|
+
firstSynced.then(() => {
|
|
323
|
+
debug("metadata session firstSynced resolved", { roomId: params.roomId });
|
|
324
|
+
}, (error) => {
|
|
325
|
+
debug("metadata session firstSynced rejected", {
|
|
326
|
+
roomId: params.roomId,
|
|
327
|
+
error
|
|
328
|
+
});
|
|
329
|
+
});
|
|
192
330
|
const session = {
|
|
193
331
|
adaptor,
|
|
194
332
|
room,
|
|
195
|
-
firstSynced
|
|
333
|
+
firstSynced,
|
|
196
334
|
flock,
|
|
197
335
|
roomId: params.roomId,
|
|
198
336
|
auth: params.auth,
|
|
@@ -204,34 +342,83 @@ var WebSocketTransportAdapter = class {
|
|
|
204
342
|
async teardownMetadataSession(session) {
|
|
205
343
|
const target = session ?? this.metadataSession;
|
|
206
344
|
if (!target) return;
|
|
345
|
+
debug("teardownMetadataSession invoked", { roomId: target.roomId });
|
|
207
346
|
if (this.metadataSession === target) this.metadataSession = void 0;
|
|
208
347
|
const { adaptor, room } = target;
|
|
209
348
|
try {
|
|
210
349
|
await room.leave();
|
|
211
|
-
|
|
350
|
+
debug("metadata room left", { roomId: target.roomId });
|
|
351
|
+
} catch (error) {
|
|
352
|
+
debug("metadata room leave failed; destroying", {
|
|
353
|
+
roomId: target.roomId,
|
|
354
|
+
error
|
|
355
|
+
});
|
|
212
356
|
await room.destroy().catch(() => {});
|
|
213
357
|
}
|
|
214
358
|
adaptor.destroy();
|
|
359
|
+
debug("metadata session destroyed", { roomId: target.roomId });
|
|
215
360
|
}
|
|
216
361
|
async ensureDocSession(docId, doc, params) {
|
|
362
|
+
debug("ensureDocSession invoked", { docId });
|
|
217
363
|
const client = this.ensureClient();
|
|
218
364
|
await client.waitConnected();
|
|
365
|
+
debug("websocket client ready for doc session", {
|
|
366
|
+
docId,
|
|
367
|
+
status: client.getStatus()
|
|
368
|
+
});
|
|
219
369
|
const existing = this.docSessions.get(docId);
|
|
220
370
|
const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
|
|
221
371
|
const roomId = normalizeRoomId(params.roomId, derivedRoomId);
|
|
222
372
|
const auth = params.auth ?? this.options.docAuth?.(docId);
|
|
223
|
-
|
|
224
|
-
|
|
373
|
+
debug("doc session params resolved", {
|
|
374
|
+
docId,
|
|
375
|
+
roomId,
|
|
376
|
+
hasAuth: Boolean(auth && auth.length)
|
|
377
|
+
});
|
|
378
|
+
if (existing && existing.doc === doc && existing.roomId === roomId) {
|
|
379
|
+
debug("reusing doc session", {
|
|
380
|
+
docId,
|
|
381
|
+
roomId,
|
|
382
|
+
refCount: existing.refCount
|
|
383
|
+
});
|
|
384
|
+
return existing;
|
|
385
|
+
}
|
|
386
|
+
if (existing) {
|
|
387
|
+
debug("doc session mismatch; leaving existing session", {
|
|
388
|
+
docId,
|
|
389
|
+
previousRoomId: existing.roomId,
|
|
390
|
+
nextRoomId: roomId
|
|
391
|
+
});
|
|
392
|
+
await this.leaveDocSession(docId).catch(() => {});
|
|
393
|
+
}
|
|
225
394
|
const adaptor = new loro_adaptors.LoroAdaptor(doc);
|
|
395
|
+
debug("joining doc room", {
|
|
396
|
+
docId,
|
|
397
|
+
roomId,
|
|
398
|
+
hasAuth: Boolean(auth && auth.length)
|
|
399
|
+
});
|
|
226
400
|
const room = await client.join({
|
|
227
401
|
roomId,
|
|
228
402
|
crdtAdaptor: adaptor,
|
|
229
403
|
auth
|
|
230
404
|
});
|
|
405
|
+
const firstSynced = room.waitForReachingServerVersion();
|
|
406
|
+
firstSynced.then(() => {
|
|
407
|
+
debug("doc session firstSynced resolved", {
|
|
408
|
+
docId,
|
|
409
|
+
roomId
|
|
410
|
+
});
|
|
411
|
+
}, (error) => {
|
|
412
|
+
debug("doc session firstSynced rejected", {
|
|
413
|
+
docId,
|
|
414
|
+
roomId,
|
|
415
|
+
error
|
|
416
|
+
});
|
|
417
|
+
});
|
|
231
418
|
const session = {
|
|
232
419
|
adaptor,
|
|
233
420
|
room,
|
|
234
|
-
firstSynced
|
|
421
|
+
firstSynced,
|
|
235
422
|
doc,
|
|
236
423
|
roomId,
|
|
237
424
|
refCount: 0
|
|
@@ -241,14 +428,34 @@ var WebSocketTransportAdapter = class {
|
|
|
241
428
|
}
|
|
242
429
|
async leaveDocSession(docId) {
|
|
243
430
|
const session = this.docSessions.get(docId);
|
|
244
|
-
if (!session)
|
|
431
|
+
if (!session) {
|
|
432
|
+
debug("leaveDocSession invoked but no session found", { docId });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
245
435
|
this.docSessions.delete(docId);
|
|
436
|
+
debug("leaving doc session", {
|
|
437
|
+
docId,
|
|
438
|
+
roomId: session.roomId
|
|
439
|
+
});
|
|
246
440
|
try {
|
|
247
441
|
await session.room.leave();
|
|
248
|
-
|
|
442
|
+
debug("doc room left", {
|
|
443
|
+
docId,
|
|
444
|
+
roomId: session.roomId
|
|
445
|
+
});
|
|
446
|
+
} catch (error) {
|
|
447
|
+
debug("doc room leave failed; destroying", {
|
|
448
|
+
docId,
|
|
449
|
+
roomId: session.roomId,
|
|
450
|
+
error
|
|
451
|
+
});
|
|
249
452
|
await session.room.destroy().catch(() => {});
|
|
250
453
|
}
|
|
251
454
|
session.adaptor.destroy();
|
|
455
|
+
debug("doc session destroyed", {
|
|
456
|
+
docId,
|
|
457
|
+
roomId: session.roomId
|
|
458
|
+
});
|
|
252
459
|
}
|
|
253
460
|
};
|
|
254
461
|
|
|
@@ -953,24 +1160,6 @@ var RepoEventBus = class {
|
|
|
953
1160
|
}
|
|
954
1161
|
};
|
|
955
1162
|
|
|
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
1163
|
//#endregion
|
|
975
1164
|
//#region src/utils.ts
|
|
976
1165
|
async function streamToUint8Array(stream) {
|
|
@@ -1133,37 +1322,24 @@ function toReadableStream(bytes) {
|
|
|
1133
1322
|
controller.close();
|
|
1134
1323
|
} });
|
|
1135
1324
|
}
|
|
1136
|
-
function
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
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);
|
|
1325
|
+
function canonicalizeFrontiers(frontiers) {
|
|
1326
|
+
const json = [...frontiers].sort((a, b) => {
|
|
1327
|
+
if (a.peer < b.peer) return -1;
|
|
1328
|
+
if (a.peer > b.peer) return 1;
|
|
1329
|
+
return a.counter - b.counter;
|
|
1330
|
+
}).map((f) => ({
|
|
1331
|
+
peer: f.peer,
|
|
1332
|
+
counter: f.counter
|
|
1333
|
+
}));
|
|
1162
1334
|
return {
|
|
1163
1335
|
json,
|
|
1164
1336
|
key: stableStringify(json)
|
|
1165
1337
|
};
|
|
1166
1338
|
}
|
|
1339
|
+
function includesFrontiers(vv, frontiers) {
|
|
1340
|
+
for (const { peer, counter } of frontiers) if ((vv.get(peer) ?? 0) <= counter) return false;
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
1167
1343
|
function matchesQuery(docId, _metadata, query) {
|
|
1168
1344
|
if (!query) return true;
|
|
1169
1345
|
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
@@ -1185,14 +1361,12 @@ function logAsyncError(context) {
|
|
|
1185
1361
|
//#region src/internal/doc-manager.ts
|
|
1186
1362
|
var DocManager = class {
|
|
1187
1363
|
storage;
|
|
1188
|
-
docFactory;
|
|
1189
1364
|
docFrontierDebounceMs;
|
|
1190
1365
|
getMetaFlock;
|
|
1191
1366
|
eventBus;
|
|
1192
1367
|
persistMeta;
|
|
1193
1368
|
state;
|
|
1194
1369
|
docs = /* @__PURE__ */ new Map();
|
|
1195
|
-
docRefs = /* @__PURE__ */ new Map();
|
|
1196
1370
|
docSubscriptions = /* @__PURE__ */ new Map();
|
|
1197
1371
|
docFrontierUpdates = /* @__PURE__ */ new Map();
|
|
1198
1372
|
docPersistedVersions = /* @__PURE__ */ new Map();
|
|
@@ -1201,21 +1375,17 @@ var DocManager = class {
|
|
|
1201
1375
|
}
|
|
1202
1376
|
constructor(options) {
|
|
1203
1377
|
this.storage = options.storage;
|
|
1204
|
-
this.docFactory = options.docFactory;
|
|
1205
1378
|
this.docFrontierDebounceMs = options.docFrontierDebounceMs;
|
|
1206
1379
|
this.getMetaFlock = options.getMetaFlock;
|
|
1207
1380
|
this.eventBus = options.eventBus;
|
|
1208
1381
|
this.persistMeta = options.persistMeta;
|
|
1209
1382
|
this.state = options.state;
|
|
1210
1383
|
}
|
|
1211
|
-
async openCollaborativeDoc(docId
|
|
1212
|
-
|
|
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));
|
|
1384
|
+
async openCollaborativeDoc(docId) {
|
|
1385
|
+
return await this.ensureDoc(docId);
|
|
1216
1386
|
}
|
|
1217
1387
|
async openDetachedDoc(docId) {
|
|
1218
|
-
return
|
|
1388
|
+
return await this.materializeDetachedDoc(docId);
|
|
1219
1389
|
}
|
|
1220
1390
|
async ensureDoc(docId) {
|
|
1221
1391
|
const cached = this.docs.get(docId);
|
|
@@ -1231,7 +1401,7 @@ var DocManager = class {
|
|
|
1231
1401
|
return stored;
|
|
1232
1402
|
}
|
|
1233
1403
|
}
|
|
1234
|
-
const created =
|
|
1404
|
+
const created = new loro_crdt.LoroDoc();
|
|
1235
1405
|
this.registerDoc(docId, created);
|
|
1236
1406
|
return created;
|
|
1237
1407
|
}
|
|
@@ -1257,27 +1427,42 @@ var DocManager = class {
|
|
|
1257
1427
|
}
|
|
1258
1428
|
}
|
|
1259
1429
|
async updateDocFrontiers(docId, doc, defaultBy) {
|
|
1260
|
-
const
|
|
1430
|
+
const frontiers = doc.oplogFrontiers();
|
|
1431
|
+
const { json, key } = canonicalizeFrontiers(frontiers);
|
|
1261
1432
|
const existingKeys = this.docFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1262
1433
|
let mutated = false;
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1434
|
+
const metaFlock = this.metaFlock;
|
|
1435
|
+
const vv = doc.version();
|
|
1436
|
+
for (const entry of existingKeys) {
|
|
1437
|
+
if (entry === key) continue;
|
|
1438
|
+
let oldFrontiers;
|
|
1439
|
+
try {
|
|
1440
|
+
oldFrontiers = JSON.parse(entry);
|
|
1441
|
+
} catch {
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
if (includesFrontiers(vv, oldFrontiers)) {
|
|
1445
|
+
metaFlock.delete([
|
|
1446
|
+
"f",
|
|
1447
|
+
docId,
|
|
1448
|
+
entry
|
|
1449
|
+
]);
|
|
1450
|
+
mutated = true;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
if (!existingKeys.has(key)) {
|
|
1270
1454
|
metaFlock.put([
|
|
1271
1455
|
"f",
|
|
1272
1456
|
docId,
|
|
1273
1457
|
key
|
|
1274
1458
|
], json);
|
|
1275
|
-
this.docFrontierKeys.set(docId, new Set([key]));
|
|
1276
1459
|
mutated = true;
|
|
1277
1460
|
}
|
|
1278
|
-
if (mutated)
|
|
1461
|
+
if (mutated) {
|
|
1462
|
+
this.refreshDocFrontierKeys(docId);
|
|
1463
|
+
await this.persistMeta();
|
|
1464
|
+
}
|
|
1279
1465
|
const by = this.eventBus.resolveEventBy(defaultBy);
|
|
1280
|
-
const frontiers = getDocFrontiers(doc);
|
|
1281
1466
|
this.eventBus.emit({
|
|
1282
1467
|
kind: "doc-frontiers",
|
|
1283
1468
|
docId,
|
|
@@ -1298,20 +1483,33 @@ var DocManager = class {
|
|
|
1298
1483
|
}
|
|
1299
1484
|
return true;
|
|
1300
1485
|
}
|
|
1486
|
+
async unloadDoc(docId) {
|
|
1487
|
+
const doc = this.docs.get(docId);
|
|
1488
|
+
if (!doc) return;
|
|
1489
|
+
await this.flushScheduledDocFrontierUpdate(docId);
|
|
1490
|
+
await this.persistDocUpdate(docId, doc);
|
|
1491
|
+
await this.updateDocFrontiers(docId, doc, "local");
|
|
1492
|
+
this.docSubscriptions.get(docId)?.();
|
|
1493
|
+
this.docSubscriptions.delete(docId);
|
|
1494
|
+
this.docs.delete(docId);
|
|
1495
|
+
this.docPersistedVersions.delete(docId);
|
|
1496
|
+
}
|
|
1497
|
+
async flush() {
|
|
1498
|
+
const promises = [];
|
|
1499
|
+
for (const [docId, doc] of this.docs) promises.push((async () => {
|
|
1500
|
+
await this.persistDocUpdate(docId, doc);
|
|
1501
|
+
await this.flushScheduledDocFrontierUpdate(docId);
|
|
1502
|
+
})());
|
|
1503
|
+
await Promise.all(promises);
|
|
1504
|
+
}
|
|
1301
1505
|
async close() {
|
|
1506
|
+
await this.flush();
|
|
1302
1507
|
for (const unsubscribe of this.docSubscriptions.values()) try {
|
|
1303
1508
|
unsubscribe();
|
|
1304
1509
|
} catch {}
|
|
1305
1510
|
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
1511
|
this.docFrontierUpdates.clear();
|
|
1313
1512
|
this.docs.clear();
|
|
1314
|
-
this.docRefs.clear();
|
|
1315
1513
|
this.docPersistedVersions.clear();
|
|
1316
1514
|
this.docFrontierKeys.clear();
|
|
1317
1515
|
}
|
|
@@ -1335,6 +1533,7 @@ var DocManager = class {
|
|
|
1335
1533
|
const keys = /* @__PURE__ */ new Set();
|
|
1336
1534
|
for (const row of rows) {
|
|
1337
1535
|
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1536
|
+
if (row.value === void 0 || row.value === null) continue;
|
|
1338
1537
|
const frontierKey = row.key[2];
|
|
1339
1538
|
if (typeof frontierKey === "string") keys.add(frontierKey);
|
|
1340
1539
|
}
|
|
@@ -1389,22 +1588,10 @@ var DocManager = class {
|
|
|
1389
1588
|
}
|
|
1390
1589
|
})().catch(logAsyncError(`doc ${docId} frontier debounce`));
|
|
1391
1590
|
}
|
|
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
1591
|
async materializeDetachedDoc(docId) {
|
|
1405
1592
|
const snapshot = await this.exportDocSnapshot(docId);
|
|
1406
1593
|
if (snapshot) return loro_crdt.LoroDoc.fromSnapshot(snapshot);
|
|
1407
|
-
return
|
|
1594
|
+
return new loro_crdt.LoroDoc();
|
|
1408
1595
|
}
|
|
1409
1596
|
async exportDocSnapshot(docId) {
|
|
1410
1597
|
const cached = this.docs.get(docId);
|
|
@@ -1414,7 +1601,7 @@ var DocManager = class {
|
|
|
1414
1601
|
}
|
|
1415
1602
|
async persistDocUpdate(docId, doc) {
|
|
1416
1603
|
const previousVersion = this.docPersistedVersions.get(docId);
|
|
1417
|
-
const nextVersion = doc.
|
|
1604
|
+
const nextVersion = doc.oplogVersion();
|
|
1418
1605
|
if (!this.storage) {
|
|
1419
1606
|
this.docPersistedVersions.set(docId, nextVersion);
|
|
1420
1607
|
return;
|
|
@@ -1424,14 +1611,11 @@ var DocManager = class {
|
|
|
1424
1611
|
this.docPersistedVersions.set(docId, nextVersion);
|
|
1425
1612
|
return;
|
|
1426
1613
|
}
|
|
1614
|
+
if (previousVersion.compare(nextVersion) === 0) return;
|
|
1427
1615
|
const update = doc.export({
|
|
1428
1616
|
mode: "update",
|
|
1429
1617
|
from: previousVersion
|
|
1430
1618
|
});
|
|
1431
|
-
if (!update.length) {
|
|
1432
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1433
|
-
return;
|
|
1434
|
-
}
|
|
1435
1619
|
this.docPersistedVersions.set(docId, nextVersion);
|
|
1436
1620
|
try {
|
|
1437
1621
|
await this.storage.save({
|
|
@@ -1453,7 +1637,14 @@ var DocManager = class {
|
|
|
1453
1637
|
return;
|
|
1454
1638
|
}
|
|
1455
1639
|
const flushed = this.flushScheduledDocFrontierUpdate(docId);
|
|
1456
|
-
const updated =
|
|
1640
|
+
const updated = (async () => {
|
|
1641
|
+
this.eventBus.pushEventBy(by);
|
|
1642
|
+
try {
|
|
1643
|
+
await this.updateDocFrontiers(docId, doc, by);
|
|
1644
|
+
} finally {
|
|
1645
|
+
this.eventBus.popEventBy();
|
|
1646
|
+
}
|
|
1647
|
+
})();
|
|
1457
1648
|
await Promise.all([
|
|
1458
1649
|
persist,
|
|
1459
1650
|
flushed,
|
|
@@ -1486,17 +1677,38 @@ var MetadataManager = class {
|
|
|
1486
1677
|
const metadata = this.state.metadata.get(docId);
|
|
1487
1678
|
return metadata ? cloneJsonObject(metadata) : void 0;
|
|
1488
1679
|
}
|
|
1489
|
-
|
|
1680
|
+
listDoc(query) {
|
|
1681
|
+
if (query?.limit !== void 0 && query.limit <= 0) return [];
|
|
1682
|
+
const { startKey, endKey } = this.computeDocRangeKeys(query);
|
|
1683
|
+
if (startKey && endKey && startKey >= endKey) return [];
|
|
1684
|
+
const scanOptions = { prefix: ["m"] };
|
|
1685
|
+
if (startKey) scanOptions.start = {
|
|
1686
|
+
kind: "inclusive",
|
|
1687
|
+
key: ["m", startKey]
|
|
1688
|
+
};
|
|
1689
|
+
if (endKey) scanOptions.end = {
|
|
1690
|
+
kind: "exclusive",
|
|
1691
|
+
key: ["m", endKey]
|
|
1692
|
+
};
|
|
1693
|
+
const rows = this.metaFlock.scan(scanOptions);
|
|
1694
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1490
1695
|
const entries = [];
|
|
1491
|
-
for (const
|
|
1696
|
+
for (const row of rows) {
|
|
1697
|
+
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
1698
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1699
|
+
const docId = row.key[1];
|
|
1700
|
+
if (typeof docId !== "string") continue;
|
|
1701
|
+
if (seen.has(docId)) continue;
|
|
1702
|
+
seen.add(docId);
|
|
1703
|
+
const metadata = this.state.metadata.get(docId);
|
|
1704
|
+
if (!metadata) continue;
|
|
1492
1705
|
if (!matchesQuery(docId, metadata, query)) continue;
|
|
1493
1706
|
entries.push({
|
|
1494
1707
|
docId,
|
|
1495
1708
|
meta: cloneJsonObject(metadata)
|
|
1496
1709
|
});
|
|
1710
|
+
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
1497
1711
|
}
|
|
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
1712
|
return entries;
|
|
1501
1713
|
}
|
|
1502
1714
|
async upsert(docId, patch) {
|
|
@@ -1590,6 +1802,26 @@ var MetadataManager = class {
|
|
|
1590
1802
|
clear() {
|
|
1591
1803
|
this.state.metadata.clear();
|
|
1592
1804
|
}
|
|
1805
|
+
computeDocRangeKeys(query) {
|
|
1806
|
+
if (!query) return {};
|
|
1807
|
+
const prefix = query.prefix && query.prefix.length > 0 ? query.prefix : void 0;
|
|
1808
|
+
let startKey = query.start;
|
|
1809
|
+
if (prefix) startKey = !startKey || prefix > startKey ? prefix : startKey;
|
|
1810
|
+
let endKey = query.end;
|
|
1811
|
+
const prefixEnd = this.nextLexicographicString(prefix);
|
|
1812
|
+
if (prefixEnd) endKey = !endKey || prefixEnd < endKey ? prefixEnd : endKey;
|
|
1813
|
+
return {
|
|
1814
|
+
startKey,
|
|
1815
|
+
endKey
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1818
|
+
nextLexicographicString(value) {
|
|
1819
|
+
if (!value) return void 0;
|
|
1820
|
+
for (let i = value.length - 1; i >= 0; i -= 1) {
|
|
1821
|
+
const code = value.charCodeAt(i);
|
|
1822
|
+
if (code < 65535) return `${value.slice(0, i)}${String.fromCharCode(code + 1)}`;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1593
1825
|
readDocMetadataFromFlock(docId) {
|
|
1594
1826
|
const rows = this.metaFlock.scan({ prefix: ["m", docId] });
|
|
1595
1827
|
if (!rows.length) return void 0;
|
|
@@ -2328,6 +2560,9 @@ var FlockHydrator = class {
|
|
|
2328
2560
|
|
|
2329
2561
|
//#endregion
|
|
2330
2562
|
//#region src/internal/sync-runner.ts
|
|
2563
|
+
/**
|
|
2564
|
+
* Sync data between storage and transport layer
|
|
2565
|
+
*/
|
|
2331
2566
|
var SyncRunner = class {
|
|
2332
2567
|
storage;
|
|
2333
2568
|
transport;
|
|
@@ -2342,6 +2577,7 @@ var SyncRunner = class {
|
|
|
2342
2577
|
readyPromise;
|
|
2343
2578
|
metaRoomSubscription;
|
|
2344
2579
|
unsubscribeMetaFlock;
|
|
2580
|
+
docSubscriptions = /* @__PURE__ */ new Map();
|
|
2345
2581
|
constructor(options) {
|
|
2346
2582
|
this.storage = options.storage;
|
|
2347
2583
|
this.transport = options.transport;
|
|
@@ -2351,7 +2587,7 @@ var SyncRunner = class {
|
|
|
2351
2587
|
this.assetManager = options.assetManager;
|
|
2352
2588
|
this.flockHydrator = options.flockHydrator;
|
|
2353
2589
|
this.getMetaFlock = options.getMetaFlock;
|
|
2354
|
-
this.replaceMetaFlock = options.
|
|
2590
|
+
this.replaceMetaFlock = options.mergeFlock;
|
|
2355
2591
|
this.persistMeta = options.persistMeta;
|
|
2356
2592
|
}
|
|
2357
2593
|
async ready() {
|
|
@@ -2428,15 +2664,30 @@ var SyncRunner = class {
|
|
|
2428
2664
|
await this.ready();
|
|
2429
2665
|
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
2430
2666
|
if (!this.transport.isConnected()) await this.transport.connect();
|
|
2667
|
+
const existing = this.docSubscriptions.get(docId);
|
|
2668
|
+
if (existing) return existing;
|
|
2431
2669
|
const doc = await this.docManager.ensureDoc(docId);
|
|
2432
2670
|
const subscription = this.transport.joinDocRoom(docId, doc, params);
|
|
2671
|
+
const wrapped = {
|
|
2672
|
+
unsubscribe: () => {
|
|
2673
|
+
subscription.unsubscribe();
|
|
2674
|
+
if (this.docSubscriptions.get(docId) === wrapped) this.docSubscriptions.delete(docId);
|
|
2675
|
+
},
|
|
2676
|
+
firstSyncedWithRemote: subscription.firstSyncedWithRemote,
|
|
2677
|
+
get connected() {
|
|
2678
|
+
return subscription.connected;
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
this.docSubscriptions.set(docId, wrapped);
|
|
2433
2682
|
subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
2434
|
-
return
|
|
2683
|
+
return wrapped;
|
|
2435
2684
|
}
|
|
2436
|
-
async
|
|
2685
|
+
async destroy() {
|
|
2437
2686
|
await this.docManager.close();
|
|
2438
2687
|
this.metaRoomSubscription?.unsubscribe();
|
|
2439
2688
|
this.metaRoomSubscription = void 0;
|
|
2689
|
+
for (const sub of this.docSubscriptions.values()) sub.unsubscribe();
|
|
2690
|
+
this.docSubscriptions.clear();
|
|
2440
2691
|
if (this.unsubscribeMetaFlock) {
|
|
2441
2692
|
this.unsubscribeMetaFlock();
|
|
2442
2693
|
this.unsubscribeMetaFlock = void 0;
|
|
@@ -2487,16 +2738,17 @@ function createRepoState() {
|
|
|
2487
2738
|
//#region src/index.ts
|
|
2488
2739
|
const textEncoder = new TextEncoder();
|
|
2489
2740
|
const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
|
|
2490
|
-
var LoroRepo = class {
|
|
2741
|
+
var LoroRepo = class LoroRepo {
|
|
2491
2742
|
options;
|
|
2743
|
+
_destroyed = false;
|
|
2492
2744
|
transport;
|
|
2493
2745
|
storage;
|
|
2494
|
-
docFactory;
|
|
2495
2746
|
metaFlock = new __loro_dev_flock.Flock();
|
|
2496
2747
|
eventBus;
|
|
2497
2748
|
docManager;
|
|
2498
2749
|
metadataManager;
|
|
2499
2750
|
assetManager;
|
|
2751
|
+
assetTransport;
|
|
2500
2752
|
flockHydrator;
|
|
2501
2753
|
state;
|
|
2502
2754
|
syncRunner;
|
|
@@ -2504,14 +2756,13 @@ var LoroRepo = class {
|
|
|
2504
2756
|
this.options = options;
|
|
2505
2757
|
this.transport = options.transportAdapter;
|
|
2506
2758
|
this.storage = options.storageAdapter;
|
|
2507
|
-
this.
|
|
2759
|
+
this.assetTransport = options.assetTransportAdapter;
|
|
2508
2760
|
this.eventBus = new RepoEventBus();
|
|
2509
2761
|
this.state = createRepoState();
|
|
2510
2762
|
const configuredDebounce = options.docFrontierDebounceMs;
|
|
2511
2763
|
const docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
|
|
2512
2764
|
this.docManager = new DocManager({
|
|
2513
2765
|
storage: this.storage,
|
|
2514
|
-
docFactory: this.docFactory,
|
|
2515
2766
|
docFrontierDebounceMs,
|
|
2516
2767
|
getMetaFlock: () => this.metaFlock,
|
|
2517
2768
|
eventBus: this.eventBus,
|
|
@@ -2526,7 +2777,7 @@ var LoroRepo = class {
|
|
|
2526
2777
|
});
|
|
2527
2778
|
this.assetManager = new AssetManager({
|
|
2528
2779
|
storage: this.storage,
|
|
2529
|
-
assetTransport:
|
|
2780
|
+
assetTransport: this.assetTransport,
|
|
2530
2781
|
getMetaFlock: () => this.metaFlock,
|
|
2531
2782
|
eventBus: this.eventBus,
|
|
2532
2783
|
persistMeta: () => this.persistMeta(),
|
|
@@ -2547,96 +2798,133 @@ var LoroRepo = class {
|
|
|
2547
2798
|
assetManager: this.assetManager,
|
|
2548
2799
|
flockHydrator: this.flockHydrator,
|
|
2549
2800
|
getMetaFlock: () => this.metaFlock,
|
|
2550
|
-
|
|
2551
|
-
this.metaFlock
|
|
2801
|
+
mergeFlock: (snapshot) => {
|
|
2802
|
+
this.metaFlock.merge(snapshot);
|
|
2552
2803
|
},
|
|
2553
2804
|
persistMeta: () => this.persistMeta()
|
|
2554
2805
|
});
|
|
2555
2806
|
}
|
|
2807
|
+
static async create(options) {
|
|
2808
|
+
const repo = new LoroRepo(options);
|
|
2809
|
+
await repo.storage?.init?.();
|
|
2810
|
+
await repo.ready();
|
|
2811
|
+
return repo;
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Load meta from storage.
|
|
2815
|
+
*
|
|
2816
|
+
* You need to call this before all other operations to make the app functioning correctly.
|
|
2817
|
+
* Though we do that implicitly already
|
|
2818
|
+
*/
|
|
2556
2819
|
async ready() {
|
|
2557
2820
|
await this.syncRunner.ready();
|
|
2558
2821
|
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Sync selected data via the transport adaptor
|
|
2824
|
+
* @param options
|
|
2825
|
+
*/
|
|
2559
2826
|
async sync(options = {}) {
|
|
2560
2827
|
await this.syncRunner.sync(options);
|
|
2561
2828
|
}
|
|
2829
|
+
/**
|
|
2830
|
+
* Start syncing the metadata (Flock) room. It will establish a realtime connection to the transport adaptor.
|
|
2831
|
+
* All changes on the room will be synced to the Flock, and all changes on the Flock will be synced to the room.
|
|
2832
|
+
* @param params
|
|
2833
|
+
* @returns
|
|
2834
|
+
*/
|
|
2562
2835
|
async joinMetaRoom(params) {
|
|
2563
2836
|
return this.syncRunner.joinMetaRoom(params);
|
|
2564
2837
|
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Start syncing the given doc. It will establish a realtime connection to the transport adaptor.
|
|
2840
|
+
* All changes on the doc will be synced to the transport, and all changes on the transport will be synced to the doc.
|
|
2841
|
+
*
|
|
2842
|
+
* All the changes on the room will be reflected on the same doc you get from `repo.openCollaborativeDoc(docId)`
|
|
2843
|
+
* @param docId
|
|
2844
|
+
* @param params
|
|
2845
|
+
* @returns
|
|
2846
|
+
*/
|
|
2565
2847
|
async joinDocRoom(docId, params) {
|
|
2566
2848
|
return this.syncRunner.joinDocRoom(docId, params);
|
|
2567
2849
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2850
|
+
/**
|
|
2851
|
+
* Opens a document that is automatically persisted to the configured storage adapter.
|
|
2852
|
+
*
|
|
2853
|
+
* - Edits are saved to storage (debounced).
|
|
2854
|
+
* - Frontiers are synced to the metadata (Flock).
|
|
2855
|
+
* - Realtime collaboration is NOT enabled by default; use `joinDocRoom` to connect.
|
|
2856
|
+
*/
|
|
2857
|
+
async openPersistedDoc(docId) {
|
|
2858
|
+
return {
|
|
2859
|
+
doc: await this.docManager.openCollaborativeDoc(docId),
|
|
2860
|
+
syncOnce: () => {
|
|
2861
|
+
return this.sync({
|
|
2862
|
+
scope: "doc",
|
|
2863
|
+
docIds: [docId]
|
|
2864
|
+
});
|
|
2865
|
+
},
|
|
2866
|
+
joinRoom: (auth) => {
|
|
2867
|
+
return this.syncRunner.joinDocRoom(docId, { auth });
|
|
2868
|
+
}
|
|
2869
|
+
};
|
|
2570
2870
|
}
|
|
2571
|
-
async upsertDocMeta(docId, patch
|
|
2572
|
-
await this.ready();
|
|
2871
|
+
async upsertDocMeta(docId, patch) {
|
|
2573
2872
|
await this.metadataManager.upsert(docId, patch);
|
|
2574
2873
|
}
|
|
2575
2874
|
async getDocMeta(docId) {
|
|
2576
|
-
await this.ready();
|
|
2577
2875
|
return this.metadataManager.get(docId);
|
|
2578
2876
|
}
|
|
2579
2877
|
async listDoc(query) {
|
|
2580
|
-
|
|
2581
|
-
return this.metadataManager.list(query);
|
|
2878
|
+
return this.metadataManager.listDoc(query);
|
|
2582
2879
|
}
|
|
2583
|
-
|
|
2880
|
+
getMeta() {
|
|
2584
2881
|
return this.metaFlock;
|
|
2585
2882
|
}
|
|
2586
2883
|
watch(listener, filter = {}) {
|
|
2587
2884
|
return this.eventBus.watch(listener, filter);
|
|
2588
2885
|
}
|
|
2589
2886
|
/**
|
|
2590
|
-
* Opens
|
|
2591
|
-
*
|
|
2887
|
+
* Opens a detached `LoroDoc` snapshot.
|
|
2888
|
+
*
|
|
2889
|
+
* - **No Persistence**: Edits to this document are NOT saved to storage.
|
|
2890
|
+
* - **No Sync**: This document does not participate in realtime updates.
|
|
2891
|
+
* - **Use Case**: Ideal for read-only history inspection, temporary drafts, or conflict resolution without affecting the main state.
|
|
2592
2892
|
*/
|
|
2593
|
-
async
|
|
2594
|
-
|
|
2595
|
-
const whenSyncedWithRemote = this.whenDocInSyncWithRemote(docId);
|
|
2596
|
-
return this.docManager.openCollaborativeDoc(docId, whenSyncedWithRemote);
|
|
2893
|
+
async openDetachedDoc(docId) {
|
|
2894
|
+
return this.docManager.openDetachedDoc(docId);
|
|
2597
2895
|
}
|
|
2598
2896
|
/**
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
2897
|
+
* Explicitly unloads a document from memory.
|
|
2898
|
+
*
|
|
2899
|
+
* - **Persists Immediately**: Forces a save of the document's current state to storage.
|
|
2900
|
+
* - **Frees Memory**: Removes the document from the internal cache.
|
|
2901
|
+
* - **Note**: If the document is currently being synced (via `joinDocRoom`), you should also unsubscribe from the room to fully release resources.
|
|
2601
2902
|
*/
|
|
2602
|
-
async
|
|
2603
|
-
await this.
|
|
2604
|
-
|
|
2903
|
+
async unloadDoc(docId) {
|
|
2904
|
+
await this.docManager.unloadDoc(docId);
|
|
2905
|
+
}
|
|
2906
|
+
async flush() {
|
|
2907
|
+
await this.docManager.flush();
|
|
2605
2908
|
}
|
|
2606
2909
|
async uploadAsset(params) {
|
|
2607
|
-
await this.ready();
|
|
2608
2910
|
return this.assetManager.uploadAsset(params);
|
|
2609
2911
|
}
|
|
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
2912
|
async linkAsset(docId, params) {
|
|
2619
|
-
await this.ready();
|
|
2620
2913
|
return this.assetManager.linkAsset(docId, params);
|
|
2621
2914
|
}
|
|
2622
2915
|
async fetchAsset(assetId) {
|
|
2623
|
-
await this.ready();
|
|
2624
2916
|
return this.assetManager.fetchAsset(assetId);
|
|
2625
2917
|
}
|
|
2626
2918
|
async unlinkAsset(docId, assetId) {
|
|
2627
|
-
await this.ready();
|
|
2628
2919
|
await this.assetManager.unlinkAsset(docId, assetId);
|
|
2629
2920
|
}
|
|
2630
2921
|
async listAssets(docId) {
|
|
2631
|
-
await this.ready();
|
|
2632
2922
|
return this.assetManager.listAssets(docId);
|
|
2633
2923
|
}
|
|
2634
2924
|
async ensureAsset(assetId) {
|
|
2635
|
-
await this.ready();
|
|
2636
2925
|
return this.assetManager.ensureAsset(assetId);
|
|
2637
2926
|
}
|
|
2638
2927
|
async gcAssets(options = {}) {
|
|
2639
|
-
await this.ready();
|
|
2640
2928
|
return this.assetManager.gcAssets(options);
|
|
2641
2929
|
}
|
|
2642
2930
|
async persistMeta() {
|
|
@@ -2648,6 +2936,17 @@ var LoroRepo = class {
|
|
|
2648
2936
|
update: encoded
|
|
2649
2937
|
});
|
|
2650
2938
|
}
|
|
2939
|
+
get destroyed() {
|
|
2940
|
+
return this._destroyed;
|
|
2941
|
+
}
|
|
2942
|
+
async destroy() {
|
|
2943
|
+
if (this._destroyed) return;
|
|
2944
|
+
this._destroyed = true;
|
|
2945
|
+
await this.syncRunner.destroy();
|
|
2946
|
+
this.assetTransport?.close?.();
|
|
2947
|
+
this.storage?.close?.();
|
|
2948
|
+
await this.transport?.close();
|
|
2949
|
+
}
|
|
2651
2950
|
};
|
|
2652
2951
|
|
|
2653
2952
|
//#endregion
|