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