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