loro-repo 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{index.d.cts → index-DsCaL9JX.d.cts} +101 -49
- package/dist/{index.d.ts → index-tq65q3qY.d.ts} +102 -50
- package/dist/index.cjs +463 -164
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +453 -158
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Flock } from "@loro-dev/flock";
|
|
2
|
-
import { LoroDoc } from "loro-crdt";
|
|
3
2
|
import { FlockAdaptor, LoroAdaptor } from "loro-adaptors";
|
|
4
3
|
import { CrdtType, bytesToHex } from "loro-protocol";
|
|
5
4
|
import { LoroWebsocketClient } from "loro-websocket";
|
|
5
|
+
import { LoroDoc } from "loro-crdt";
|
|
6
6
|
import { promises } from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
@@ -12,8 +12,47 @@ function createRepoFlockAdaptorFromDoc(flock, config = {}) {
|
|
|
12
12
|
return new FlockAdaptor(flock, config);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/internal/debug.ts
|
|
17
|
+
const getEnv = () => {
|
|
18
|
+
if (typeof globalThis !== "object" || globalThis === null) return;
|
|
19
|
+
return globalThis.process?.env;
|
|
20
|
+
};
|
|
21
|
+
const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
|
|
22
|
+
const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
|
|
23
|
+
const wildcardTokens = new Set([
|
|
24
|
+
"*",
|
|
25
|
+
"1",
|
|
26
|
+
"true",
|
|
27
|
+
"all"
|
|
28
|
+
]);
|
|
29
|
+
const namespaceSet = new Set(normalizedNamespaces);
|
|
30
|
+
const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
|
|
31
|
+
const isDebugEnabled = (namespace) => {
|
|
32
|
+
if (!namespaceSet.size) return false;
|
|
33
|
+
if (!namespace) return hasWildcard;
|
|
34
|
+
const normalized = namespace.toLowerCase();
|
|
35
|
+
if (hasWildcard) return true;
|
|
36
|
+
if (namespaceSet.has(normalized)) return true;
|
|
37
|
+
const [root] = normalized.split(":");
|
|
38
|
+
return namespaceSet.has(root);
|
|
39
|
+
};
|
|
40
|
+
const createDebugLogger = (namespace) => {
|
|
41
|
+
const normalized = namespace.toLowerCase();
|
|
42
|
+
return (...args) => {
|
|
43
|
+
if (!isDebugEnabled(normalized)) return;
|
|
44
|
+
const prefix = `[loro-repo:${namespace}]`;
|
|
45
|
+
if (args.length === 0) {
|
|
46
|
+
console.info(prefix);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.info(prefix, ...args);
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
15
53
|
//#endregion
|
|
16
54
|
//#region src/transport/websocket.ts
|
|
55
|
+
const debug = createDebugLogger("transport:websocket");
|
|
17
56
|
function withTimeout(promise, timeoutMs) {
|
|
18
57
|
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
19
58
|
return new Promise((resolve, reject) => {
|
|
@@ -58,30 +97,51 @@ var WebSocketTransportAdapter = class {
|
|
|
58
97
|
}
|
|
59
98
|
async connect(_options) {
|
|
60
99
|
const client = this.ensureClient();
|
|
61
|
-
|
|
62
|
-
|
|
100
|
+
debug("connect requested", { status: client.getStatus() });
|
|
101
|
+
try {
|
|
102
|
+
await client.connect();
|
|
103
|
+
debug("client.connect resolved");
|
|
104
|
+
await client.waitConnected();
|
|
105
|
+
debug("client.waitConnected resolved", { status: client.getStatus() });
|
|
106
|
+
} catch (error) {
|
|
107
|
+
debug("connect failed", error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
63
110
|
}
|
|
64
111
|
async close() {
|
|
112
|
+
debug("close requested", {
|
|
113
|
+
docSessions: this.docSessions.size,
|
|
114
|
+
metadataSession: Boolean(this.metadataSession)
|
|
115
|
+
});
|
|
65
116
|
for (const [docId] of this.docSessions) await this.leaveDocSession(docId).catch(() => {});
|
|
66
117
|
this.docSessions.clear();
|
|
67
118
|
await this.teardownMetadataSession().catch(() => {});
|
|
68
119
|
if (this.client) {
|
|
69
|
-
this.client
|
|
120
|
+
const client = this.client;
|
|
70
121
|
this.client = void 0;
|
|
122
|
+
client.destroy();
|
|
123
|
+
debug("websocket client destroyed");
|
|
71
124
|
}
|
|
125
|
+
debug("close completed");
|
|
72
126
|
}
|
|
73
127
|
isConnected() {
|
|
74
128
|
return this.client?.getStatus() === "connected";
|
|
75
129
|
}
|
|
76
130
|
async syncMeta(flock, options) {
|
|
77
|
-
if (!this.options.metadataRoomId)
|
|
131
|
+
if (!this.options.metadataRoomId) {
|
|
132
|
+
debug("syncMeta skipped; metadata room not configured");
|
|
133
|
+
return { ok: true };
|
|
134
|
+
}
|
|
135
|
+
debug("syncMeta requested", { roomId: this.options.metadataRoomId });
|
|
78
136
|
try {
|
|
79
137
|
await withTimeout((await this.ensureMetadataSession(flock, {
|
|
80
138
|
roomId: this.options.metadataRoomId,
|
|
81
139
|
auth: this.options.metadataAuth
|
|
82
140
|
})).firstSynced, options?.timeout);
|
|
141
|
+
debug("syncMeta completed", { roomId: this.options.metadataRoomId });
|
|
83
142
|
return { ok: true };
|
|
84
|
-
} catch {
|
|
143
|
+
} catch (error) {
|
|
144
|
+
debug("syncMeta failed", error);
|
|
85
145
|
return { ok: false };
|
|
86
146
|
}
|
|
87
147
|
}
|
|
@@ -90,6 +150,10 @@ var WebSocketTransportAdapter = class {
|
|
|
90
150
|
const roomId = normalizeRoomId(params?.roomId, fallback);
|
|
91
151
|
if (!roomId) throw new Error("Metadata room id not configured");
|
|
92
152
|
const auth = params?.auth ?? this.options.metadataAuth;
|
|
153
|
+
debug("joinMetaRoom requested", {
|
|
154
|
+
roomId,
|
|
155
|
+
hasAuth: Boolean(auth && auth.length)
|
|
156
|
+
});
|
|
93
157
|
const ensure = this.ensureMetadataSession(flock, {
|
|
94
158
|
roomId,
|
|
95
159
|
auth
|
|
@@ -100,7 +164,14 @@ var WebSocketTransportAdapter = class {
|
|
|
100
164
|
unsubscribe: () => {
|
|
101
165
|
ensure.then((session) => {
|
|
102
166
|
session.refCount = Math.max(0, session.refCount - 1);
|
|
103
|
-
|
|
167
|
+
debug("metadata session refCount decremented", {
|
|
168
|
+
roomId: session.roomId,
|
|
169
|
+
refCount: session.refCount
|
|
170
|
+
});
|
|
171
|
+
if (session.refCount === 0) {
|
|
172
|
+
debug("tearing down metadata session due to refCount=0", { roomId: session.roomId });
|
|
173
|
+
this.teardownMetadataSession(session).catch(() => {});
|
|
174
|
+
}
|
|
104
175
|
});
|
|
105
176
|
},
|
|
106
177
|
firstSyncedWithRemote: firstSynced,
|
|
@@ -110,18 +181,37 @@ var WebSocketTransportAdapter = class {
|
|
|
110
181
|
};
|
|
111
182
|
ensure.then((session) => {
|
|
112
183
|
session.refCount += 1;
|
|
184
|
+
debug("metadata session refCount incremented", {
|
|
185
|
+
roomId: session.roomId,
|
|
186
|
+
refCount: session.refCount
|
|
187
|
+
});
|
|
113
188
|
});
|
|
114
189
|
return subscription;
|
|
115
190
|
}
|
|
116
191
|
async syncDoc(docId, doc, options) {
|
|
192
|
+
debug("syncDoc requested", { docId });
|
|
117
193
|
try {
|
|
118
|
-
|
|
194
|
+
const session = await this.ensureDocSession(docId, doc, {});
|
|
195
|
+
await withTimeout(session.firstSynced, options?.timeout);
|
|
196
|
+
debug("syncDoc completed", {
|
|
197
|
+
docId,
|
|
198
|
+
roomId: session.roomId
|
|
199
|
+
});
|
|
119
200
|
return { ok: true };
|
|
120
|
-
} catch {
|
|
201
|
+
} catch (error) {
|
|
202
|
+
debug("syncDoc failed", {
|
|
203
|
+
docId,
|
|
204
|
+
error
|
|
205
|
+
});
|
|
121
206
|
return { ok: false };
|
|
122
207
|
}
|
|
123
208
|
}
|
|
124
209
|
joinDocRoom(docId, doc, params) {
|
|
210
|
+
debug("joinDocRoom requested", {
|
|
211
|
+
docId,
|
|
212
|
+
roomParamType: params?.roomId ? typeof params.roomId === "string" ? "string" : "uint8array" : void 0,
|
|
213
|
+
hasAuthOverride: Boolean(params?.auth && params.auth.length)
|
|
214
|
+
});
|
|
125
215
|
const ensure = this.ensureDocSession(docId, doc, params ?? {});
|
|
126
216
|
const firstSynced = ensure.then((session) => session.firstSynced);
|
|
127
217
|
const getConnected = () => this.isConnected();
|
|
@@ -129,6 +219,11 @@ var WebSocketTransportAdapter = class {
|
|
|
129
219
|
unsubscribe: () => {
|
|
130
220
|
ensure.then((session) => {
|
|
131
221
|
session.refCount = Math.max(0, session.refCount - 1);
|
|
222
|
+
debug("doc session refCount decremented", {
|
|
223
|
+
docId,
|
|
224
|
+
roomId: session.roomId,
|
|
225
|
+
refCount: session.refCount
|
|
226
|
+
});
|
|
132
227
|
if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
|
|
133
228
|
});
|
|
134
229
|
},
|
|
@@ -139,12 +234,24 @@ var WebSocketTransportAdapter = class {
|
|
|
139
234
|
};
|
|
140
235
|
ensure.then((session) => {
|
|
141
236
|
session.refCount += 1;
|
|
237
|
+
debug("doc session refCount incremented", {
|
|
238
|
+
docId,
|
|
239
|
+
roomId: session.roomId,
|
|
240
|
+
refCount: session.refCount
|
|
241
|
+
});
|
|
142
242
|
});
|
|
143
243
|
return subscription;
|
|
144
244
|
}
|
|
145
245
|
ensureClient() {
|
|
146
|
-
if (this.client)
|
|
246
|
+
if (this.client) {
|
|
247
|
+
debug("reusing websocket client", { status: this.client.getStatus() });
|
|
248
|
+
return this.client;
|
|
249
|
+
}
|
|
147
250
|
const { url, client: clientOptions } = this.options;
|
|
251
|
+
debug("creating websocket client", {
|
|
252
|
+
url,
|
|
253
|
+
clientOptionsKeys: clientOptions ? Object.keys(clientOptions) : []
|
|
254
|
+
});
|
|
148
255
|
const client = new LoroWebsocketClient({
|
|
149
256
|
url,
|
|
150
257
|
...clientOptions
|
|
@@ -153,22 +260,49 @@ var WebSocketTransportAdapter = class {
|
|
|
153
260
|
return client;
|
|
154
261
|
}
|
|
155
262
|
async ensureMetadataSession(flock, params) {
|
|
263
|
+
debug("ensureMetadataSession invoked", {
|
|
264
|
+
roomId: params.roomId,
|
|
265
|
+
hasAuth: Boolean(params.auth && params.auth.length)
|
|
266
|
+
});
|
|
156
267
|
const client = this.ensureClient();
|
|
157
268
|
await client.waitConnected();
|
|
158
|
-
|
|
159
|
-
if (this.metadataSession
|
|
269
|
+
debug("websocket client ready for metadata session", { status: client.getStatus() });
|
|
270
|
+
if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) {
|
|
271
|
+
debug("reusing metadata session", {
|
|
272
|
+
roomId: this.metadataSession.roomId,
|
|
273
|
+
refCount: this.metadataSession.refCount
|
|
274
|
+
});
|
|
275
|
+
return this.metadataSession;
|
|
276
|
+
}
|
|
277
|
+
if (this.metadataSession) {
|
|
278
|
+
debug("tearing down previous metadata session", { roomId: this.metadataSession.roomId });
|
|
279
|
+
await this.teardownMetadataSession(this.metadataSession).catch(() => {});
|
|
280
|
+
}
|
|
160
281
|
const configuredType = this.options.metadataCrdtType;
|
|
161
282
|
if (configuredType && configuredType !== CrdtType.Flock) throw new Error(`metadataCrdtType must be ${CrdtType.Flock} when syncing Flock metadata`);
|
|
162
283
|
const adaptor = createRepoFlockAdaptorFromDoc(flock, this.options.metadataAdaptorConfig ?? {});
|
|
284
|
+
debug("joining metadata room", {
|
|
285
|
+
roomId: params.roomId,
|
|
286
|
+
hasAuth: Boolean(params.auth && params.auth.length)
|
|
287
|
+
});
|
|
163
288
|
const room = await client.join({
|
|
164
289
|
roomId: params.roomId,
|
|
165
290
|
crdtAdaptor: adaptor,
|
|
166
291
|
auth: params.auth
|
|
167
292
|
});
|
|
293
|
+
const firstSynced = room.waitForReachingServerVersion();
|
|
294
|
+
firstSynced.then(() => {
|
|
295
|
+
debug("metadata session firstSynced resolved", { roomId: params.roomId });
|
|
296
|
+
}, (error) => {
|
|
297
|
+
debug("metadata session firstSynced rejected", {
|
|
298
|
+
roomId: params.roomId,
|
|
299
|
+
error
|
|
300
|
+
});
|
|
301
|
+
});
|
|
168
302
|
const session = {
|
|
169
303
|
adaptor,
|
|
170
304
|
room,
|
|
171
|
-
firstSynced
|
|
305
|
+
firstSynced,
|
|
172
306
|
flock,
|
|
173
307
|
roomId: params.roomId,
|
|
174
308
|
auth: params.auth,
|
|
@@ -180,34 +314,83 @@ var WebSocketTransportAdapter = class {
|
|
|
180
314
|
async teardownMetadataSession(session) {
|
|
181
315
|
const target = session ?? this.metadataSession;
|
|
182
316
|
if (!target) return;
|
|
317
|
+
debug("teardownMetadataSession invoked", { roomId: target.roomId });
|
|
183
318
|
if (this.metadataSession === target) this.metadataSession = void 0;
|
|
184
319
|
const { adaptor, room } = target;
|
|
185
320
|
try {
|
|
186
321
|
await room.leave();
|
|
187
|
-
|
|
322
|
+
debug("metadata room left", { roomId: target.roomId });
|
|
323
|
+
} catch (error) {
|
|
324
|
+
debug("metadata room leave failed; destroying", {
|
|
325
|
+
roomId: target.roomId,
|
|
326
|
+
error
|
|
327
|
+
});
|
|
188
328
|
await room.destroy().catch(() => {});
|
|
189
329
|
}
|
|
190
330
|
adaptor.destroy();
|
|
331
|
+
debug("metadata session destroyed", { roomId: target.roomId });
|
|
191
332
|
}
|
|
192
333
|
async ensureDocSession(docId, doc, params) {
|
|
334
|
+
debug("ensureDocSession invoked", { docId });
|
|
193
335
|
const client = this.ensureClient();
|
|
194
336
|
await client.waitConnected();
|
|
337
|
+
debug("websocket client ready for doc session", {
|
|
338
|
+
docId,
|
|
339
|
+
status: client.getStatus()
|
|
340
|
+
});
|
|
195
341
|
const existing = this.docSessions.get(docId);
|
|
196
342
|
const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
|
|
197
343
|
const roomId = normalizeRoomId(params.roomId, derivedRoomId);
|
|
198
344
|
const auth = params.auth ?? this.options.docAuth?.(docId);
|
|
199
|
-
|
|
200
|
-
|
|
345
|
+
debug("doc session params resolved", {
|
|
346
|
+
docId,
|
|
347
|
+
roomId,
|
|
348
|
+
hasAuth: Boolean(auth && auth.length)
|
|
349
|
+
});
|
|
350
|
+
if (existing && existing.doc === doc && existing.roomId === roomId) {
|
|
351
|
+
debug("reusing doc session", {
|
|
352
|
+
docId,
|
|
353
|
+
roomId,
|
|
354
|
+
refCount: existing.refCount
|
|
355
|
+
});
|
|
356
|
+
return existing;
|
|
357
|
+
}
|
|
358
|
+
if (existing) {
|
|
359
|
+
debug("doc session mismatch; leaving existing session", {
|
|
360
|
+
docId,
|
|
361
|
+
previousRoomId: existing.roomId,
|
|
362
|
+
nextRoomId: roomId
|
|
363
|
+
});
|
|
364
|
+
await this.leaveDocSession(docId).catch(() => {});
|
|
365
|
+
}
|
|
201
366
|
const adaptor = new LoroAdaptor(doc);
|
|
367
|
+
debug("joining doc room", {
|
|
368
|
+
docId,
|
|
369
|
+
roomId,
|
|
370
|
+
hasAuth: Boolean(auth && auth.length)
|
|
371
|
+
});
|
|
202
372
|
const room = await client.join({
|
|
203
373
|
roomId,
|
|
204
374
|
crdtAdaptor: adaptor,
|
|
205
375
|
auth
|
|
206
376
|
});
|
|
377
|
+
const firstSynced = room.waitForReachingServerVersion();
|
|
378
|
+
firstSynced.then(() => {
|
|
379
|
+
debug("doc session firstSynced resolved", {
|
|
380
|
+
docId,
|
|
381
|
+
roomId
|
|
382
|
+
});
|
|
383
|
+
}, (error) => {
|
|
384
|
+
debug("doc session firstSynced rejected", {
|
|
385
|
+
docId,
|
|
386
|
+
roomId,
|
|
387
|
+
error
|
|
388
|
+
});
|
|
389
|
+
});
|
|
207
390
|
const session = {
|
|
208
391
|
adaptor,
|
|
209
392
|
room,
|
|
210
|
-
firstSynced
|
|
393
|
+
firstSynced,
|
|
211
394
|
doc,
|
|
212
395
|
roomId,
|
|
213
396
|
refCount: 0
|
|
@@ -217,14 +400,34 @@ var WebSocketTransportAdapter = class {
|
|
|
217
400
|
}
|
|
218
401
|
async leaveDocSession(docId) {
|
|
219
402
|
const session = this.docSessions.get(docId);
|
|
220
|
-
if (!session)
|
|
403
|
+
if (!session) {
|
|
404
|
+
debug("leaveDocSession invoked but no session found", { docId });
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
221
407
|
this.docSessions.delete(docId);
|
|
408
|
+
debug("leaving doc session", {
|
|
409
|
+
docId,
|
|
410
|
+
roomId: session.roomId
|
|
411
|
+
});
|
|
222
412
|
try {
|
|
223
413
|
await session.room.leave();
|
|
224
|
-
|
|
414
|
+
debug("doc room left", {
|
|
415
|
+
docId,
|
|
416
|
+
roomId: session.roomId
|
|
417
|
+
});
|
|
418
|
+
} catch (error) {
|
|
419
|
+
debug("doc room leave failed; destroying", {
|
|
420
|
+
docId,
|
|
421
|
+
roomId: session.roomId,
|
|
422
|
+
error
|
|
423
|
+
});
|
|
225
424
|
await session.room.destroy().catch(() => {});
|
|
226
425
|
}
|
|
227
426
|
session.adaptor.destroy();
|
|
427
|
+
debug("doc session destroyed", {
|
|
428
|
+
docId,
|
|
429
|
+
roomId: session.roomId
|
|
430
|
+
});
|
|
228
431
|
}
|
|
229
432
|
};
|
|
230
433
|
|
|
@@ -929,24 +1132,6 @@ var RepoEventBus = class {
|
|
|
929
1132
|
}
|
|
930
1133
|
};
|
|
931
1134
|
|
|
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
1135
|
//#endregion
|
|
951
1136
|
//#region src/utils.ts
|
|
952
1137
|
async function streamToUint8Array(stream) {
|
|
@@ -1109,37 +1294,24 @@ function toReadableStream(bytes) {
|
|
|
1109
1294
|
controller.close();
|
|
1110
1295
|
} });
|
|
1111
1296
|
}
|
|
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);
|
|
1297
|
+
function canonicalizeFrontiers(frontiers) {
|
|
1298
|
+
const json = [...frontiers].sort((a, b) => {
|
|
1299
|
+
if (a.peer < b.peer) return -1;
|
|
1300
|
+
if (a.peer > b.peer) return 1;
|
|
1301
|
+
return a.counter - b.counter;
|
|
1302
|
+
}).map((f) => ({
|
|
1303
|
+
peer: f.peer,
|
|
1304
|
+
counter: f.counter
|
|
1305
|
+
}));
|
|
1138
1306
|
return {
|
|
1139
1307
|
json,
|
|
1140
1308
|
key: stableStringify(json)
|
|
1141
1309
|
};
|
|
1142
1310
|
}
|
|
1311
|
+
function includesFrontiers(vv, frontiers) {
|
|
1312
|
+
for (const { peer, counter } of frontiers) if ((vv.get(peer) ?? 0) <= counter) return false;
|
|
1313
|
+
return true;
|
|
1314
|
+
}
|
|
1143
1315
|
function matchesQuery(docId, _metadata, query) {
|
|
1144
1316
|
if (!query) return true;
|
|
1145
1317
|
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
@@ -1161,14 +1333,12 @@ function logAsyncError(context) {
|
|
|
1161
1333
|
//#region src/internal/doc-manager.ts
|
|
1162
1334
|
var DocManager = class {
|
|
1163
1335
|
storage;
|
|
1164
|
-
docFactory;
|
|
1165
1336
|
docFrontierDebounceMs;
|
|
1166
1337
|
getMetaFlock;
|
|
1167
1338
|
eventBus;
|
|
1168
1339
|
persistMeta;
|
|
1169
1340
|
state;
|
|
1170
1341
|
docs = /* @__PURE__ */ new Map();
|
|
1171
|
-
docRefs = /* @__PURE__ */ new Map();
|
|
1172
1342
|
docSubscriptions = /* @__PURE__ */ new Map();
|
|
1173
1343
|
docFrontierUpdates = /* @__PURE__ */ new Map();
|
|
1174
1344
|
docPersistedVersions = /* @__PURE__ */ new Map();
|
|
@@ -1177,21 +1347,17 @@ var DocManager = class {
|
|
|
1177
1347
|
}
|
|
1178
1348
|
constructor(options) {
|
|
1179
1349
|
this.storage = options.storage;
|
|
1180
|
-
this.docFactory = options.docFactory;
|
|
1181
1350
|
this.docFrontierDebounceMs = options.docFrontierDebounceMs;
|
|
1182
1351
|
this.getMetaFlock = options.getMetaFlock;
|
|
1183
1352
|
this.eventBus = options.eventBus;
|
|
1184
1353
|
this.persistMeta = options.persistMeta;
|
|
1185
1354
|
this.state = options.state;
|
|
1186
1355
|
}
|
|
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));
|
|
1356
|
+
async openCollaborativeDoc(docId) {
|
|
1357
|
+
return await this.ensureDoc(docId);
|
|
1192
1358
|
}
|
|
1193
1359
|
async openDetachedDoc(docId) {
|
|
1194
|
-
return
|
|
1360
|
+
return await this.materializeDetachedDoc(docId);
|
|
1195
1361
|
}
|
|
1196
1362
|
async ensureDoc(docId) {
|
|
1197
1363
|
const cached = this.docs.get(docId);
|
|
@@ -1207,7 +1373,7 @@ var DocManager = class {
|
|
|
1207
1373
|
return stored;
|
|
1208
1374
|
}
|
|
1209
1375
|
}
|
|
1210
|
-
const created =
|
|
1376
|
+
const created = new LoroDoc();
|
|
1211
1377
|
this.registerDoc(docId, created);
|
|
1212
1378
|
return created;
|
|
1213
1379
|
}
|
|
@@ -1233,27 +1399,42 @@ var DocManager = class {
|
|
|
1233
1399
|
}
|
|
1234
1400
|
}
|
|
1235
1401
|
async updateDocFrontiers(docId, doc, defaultBy) {
|
|
1236
|
-
const
|
|
1402
|
+
const frontiers = doc.oplogFrontiers();
|
|
1403
|
+
const { json, key } = canonicalizeFrontiers(frontiers);
|
|
1237
1404
|
const existingKeys = this.docFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1238
1405
|
let mutated = false;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1406
|
+
const metaFlock = this.metaFlock;
|
|
1407
|
+
const vv = doc.version();
|
|
1408
|
+
for (const entry of existingKeys) {
|
|
1409
|
+
if (entry === key) continue;
|
|
1410
|
+
let oldFrontiers;
|
|
1411
|
+
try {
|
|
1412
|
+
oldFrontiers = JSON.parse(entry);
|
|
1413
|
+
} catch {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
if (includesFrontiers(vv, oldFrontiers)) {
|
|
1417
|
+
metaFlock.delete([
|
|
1418
|
+
"f",
|
|
1419
|
+
docId,
|
|
1420
|
+
entry
|
|
1421
|
+
]);
|
|
1422
|
+
mutated = true;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (!existingKeys.has(key)) {
|
|
1246
1426
|
metaFlock.put([
|
|
1247
1427
|
"f",
|
|
1248
1428
|
docId,
|
|
1249
1429
|
key
|
|
1250
1430
|
], json);
|
|
1251
|
-
this.docFrontierKeys.set(docId, new Set([key]));
|
|
1252
1431
|
mutated = true;
|
|
1253
1432
|
}
|
|
1254
|
-
if (mutated)
|
|
1433
|
+
if (mutated) {
|
|
1434
|
+
this.refreshDocFrontierKeys(docId);
|
|
1435
|
+
await this.persistMeta();
|
|
1436
|
+
}
|
|
1255
1437
|
const by = this.eventBus.resolveEventBy(defaultBy);
|
|
1256
|
-
const frontiers = getDocFrontiers(doc);
|
|
1257
1438
|
this.eventBus.emit({
|
|
1258
1439
|
kind: "doc-frontiers",
|
|
1259
1440
|
docId,
|
|
@@ -1274,20 +1455,33 @@ var DocManager = class {
|
|
|
1274
1455
|
}
|
|
1275
1456
|
return true;
|
|
1276
1457
|
}
|
|
1458
|
+
async unloadDoc(docId) {
|
|
1459
|
+
const doc = this.docs.get(docId);
|
|
1460
|
+
if (!doc) return;
|
|
1461
|
+
await this.flushScheduledDocFrontierUpdate(docId);
|
|
1462
|
+
await this.persistDocUpdate(docId, doc);
|
|
1463
|
+
await this.updateDocFrontiers(docId, doc, "local");
|
|
1464
|
+
this.docSubscriptions.get(docId)?.();
|
|
1465
|
+
this.docSubscriptions.delete(docId);
|
|
1466
|
+
this.docs.delete(docId);
|
|
1467
|
+
this.docPersistedVersions.delete(docId);
|
|
1468
|
+
}
|
|
1469
|
+
async flush() {
|
|
1470
|
+
const promises$1 = [];
|
|
1471
|
+
for (const [docId, doc] of this.docs) promises$1.push((async () => {
|
|
1472
|
+
await this.persistDocUpdate(docId, doc);
|
|
1473
|
+
await this.flushScheduledDocFrontierUpdate(docId);
|
|
1474
|
+
})());
|
|
1475
|
+
await Promise.all(promises$1);
|
|
1476
|
+
}
|
|
1277
1477
|
async close() {
|
|
1478
|
+
await this.flush();
|
|
1278
1479
|
for (const unsubscribe of this.docSubscriptions.values()) try {
|
|
1279
1480
|
unsubscribe();
|
|
1280
1481
|
} catch {}
|
|
1281
1482
|
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
1483
|
this.docFrontierUpdates.clear();
|
|
1289
1484
|
this.docs.clear();
|
|
1290
|
-
this.docRefs.clear();
|
|
1291
1485
|
this.docPersistedVersions.clear();
|
|
1292
1486
|
this.docFrontierKeys.clear();
|
|
1293
1487
|
}
|
|
@@ -1311,6 +1505,7 @@ var DocManager = class {
|
|
|
1311
1505
|
const keys = /* @__PURE__ */ new Set();
|
|
1312
1506
|
for (const row of rows) {
|
|
1313
1507
|
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1508
|
+
if (row.value === void 0 || row.value === null) continue;
|
|
1314
1509
|
const frontierKey = row.key[2];
|
|
1315
1510
|
if (typeof frontierKey === "string") keys.add(frontierKey);
|
|
1316
1511
|
}
|
|
@@ -1365,22 +1560,10 @@ var DocManager = class {
|
|
|
1365
1560
|
}
|
|
1366
1561
|
})().catch(logAsyncError(`doc ${docId} frontier debounce`));
|
|
1367
1562
|
}
|
|
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
1563
|
async materializeDetachedDoc(docId) {
|
|
1381
1564
|
const snapshot = await this.exportDocSnapshot(docId);
|
|
1382
1565
|
if (snapshot) return LoroDoc.fromSnapshot(snapshot);
|
|
1383
|
-
return
|
|
1566
|
+
return new LoroDoc();
|
|
1384
1567
|
}
|
|
1385
1568
|
async exportDocSnapshot(docId) {
|
|
1386
1569
|
const cached = this.docs.get(docId);
|
|
@@ -1390,7 +1573,7 @@ var DocManager = class {
|
|
|
1390
1573
|
}
|
|
1391
1574
|
async persistDocUpdate(docId, doc) {
|
|
1392
1575
|
const previousVersion = this.docPersistedVersions.get(docId);
|
|
1393
|
-
const nextVersion = doc.
|
|
1576
|
+
const nextVersion = doc.oplogVersion();
|
|
1394
1577
|
if (!this.storage) {
|
|
1395
1578
|
this.docPersistedVersions.set(docId, nextVersion);
|
|
1396
1579
|
return;
|
|
@@ -1400,14 +1583,11 @@ var DocManager = class {
|
|
|
1400
1583
|
this.docPersistedVersions.set(docId, nextVersion);
|
|
1401
1584
|
return;
|
|
1402
1585
|
}
|
|
1586
|
+
if (previousVersion.compare(nextVersion) === 0) return;
|
|
1403
1587
|
const update = doc.export({
|
|
1404
1588
|
mode: "update",
|
|
1405
1589
|
from: previousVersion
|
|
1406
1590
|
});
|
|
1407
|
-
if (!update.length) {
|
|
1408
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1409
|
-
return;
|
|
1410
|
-
}
|
|
1411
1591
|
this.docPersistedVersions.set(docId, nextVersion);
|
|
1412
1592
|
try {
|
|
1413
1593
|
await this.storage.save({
|
|
@@ -1429,7 +1609,14 @@ var DocManager = class {
|
|
|
1429
1609
|
return;
|
|
1430
1610
|
}
|
|
1431
1611
|
const flushed = this.flushScheduledDocFrontierUpdate(docId);
|
|
1432
|
-
const updated =
|
|
1612
|
+
const updated = (async () => {
|
|
1613
|
+
this.eventBus.pushEventBy(by);
|
|
1614
|
+
try {
|
|
1615
|
+
await this.updateDocFrontiers(docId, doc, by);
|
|
1616
|
+
} finally {
|
|
1617
|
+
this.eventBus.popEventBy();
|
|
1618
|
+
}
|
|
1619
|
+
})();
|
|
1433
1620
|
await Promise.all([
|
|
1434
1621
|
persist,
|
|
1435
1622
|
flushed,
|
|
@@ -1462,17 +1649,38 @@ var MetadataManager = class {
|
|
|
1462
1649
|
const metadata = this.state.metadata.get(docId);
|
|
1463
1650
|
return metadata ? cloneJsonObject(metadata) : void 0;
|
|
1464
1651
|
}
|
|
1465
|
-
|
|
1652
|
+
listDoc(query) {
|
|
1653
|
+
if (query?.limit !== void 0 && query.limit <= 0) return [];
|
|
1654
|
+
const { startKey, endKey } = this.computeDocRangeKeys(query);
|
|
1655
|
+
if (startKey && endKey && startKey >= endKey) return [];
|
|
1656
|
+
const scanOptions = { prefix: ["m"] };
|
|
1657
|
+
if (startKey) scanOptions.start = {
|
|
1658
|
+
kind: "inclusive",
|
|
1659
|
+
key: ["m", startKey]
|
|
1660
|
+
};
|
|
1661
|
+
if (endKey) scanOptions.end = {
|
|
1662
|
+
kind: "exclusive",
|
|
1663
|
+
key: ["m", endKey]
|
|
1664
|
+
};
|
|
1665
|
+
const rows = this.metaFlock.scan(scanOptions);
|
|
1666
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1466
1667
|
const entries = [];
|
|
1467
|
-
for (const
|
|
1668
|
+
for (const row of rows) {
|
|
1669
|
+
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
1670
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1671
|
+
const docId = row.key[1];
|
|
1672
|
+
if (typeof docId !== "string") continue;
|
|
1673
|
+
if (seen.has(docId)) continue;
|
|
1674
|
+
seen.add(docId);
|
|
1675
|
+
const metadata = this.state.metadata.get(docId);
|
|
1676
|
+
if (!metadata) continue;
|
|
1468
1677
|
if (!matchesQuery(docId, metadata, query)) continue;
|
|
1469
1678
|
entries.push({
|
|
1470
1679
|
docId,
|
|
1471
1680
|
meta: cloneJsonObject(metadata)
|
|
1472
1681
|
});
|
|
1682
|
+
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
1473
1683
|
}
|
|
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
1684
|
return entries;
|
|
1477
1685
|
}
|
|
1478
1686
|
async upsert(docId, patch) {
|
|
@@ -1566,6 +1774,26 @@ var MetadataManager = class {
|
|
|
1566
1774
|
clear() {
|
|
1567
1775
|
this.state.metadata.clear();
|
|
1568
1776
|
}
|
|
1777
|
+
computeDocRangeKeys(query) {
|
|
1778
|
+
if (!query) return {};
|
|
1779
|
+
const prefix = query.prefix && query.prefix.length > 0 ? query.prefix : void 0;
|
|
1780
|
+
let startKey = query.start;
|
|
1781
|
+
if (prefix) startKey = !startKey || prefix > startKey ? prefix : startKey;
|
|
1782
|
+
let endKey = query.end;
|
|
1783
|
+
const prefixEnd = this.nextLexicographicString(prefix);
|
|
1784
|
+
if (prefixEnd) endKey = !endKey || prefixEnd < endKey ? prefixEnd : endKey;
|
|
1785
|
+
return {
|
|
1786
|
+
startKey,
|
|
1787
|
+
endKey
|
|
1788
|
+
};
|
|
1789
|
+
}
|
|
1790
|
+
nextLexicographicString(value) {
|
|
1791
|
+
if (!value) return void 0;
|
|
1792
|
+
for (let i = value.length - 1; i >= 0; i -= 1) {
|
|
1793
|
+
const code = value.charCodeAt(i);
|
|
1794
|
+
if (code < 65535) return `${value.slice(0, i)}${String.fromCharCode(code + 1)}`;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1569
1797
|
readDocMetadataFromFlock(docId) {
|
|
1570
1798
|
const rows = this.metaFlock.scan({ prefix: ["m", docId] });
|
|
1571
1799
|
if (!rows.length) return void 0;
|
|
@@ -2304,6 +2532,9 @@ var FlockHydrator = class {
|
|
|
2304
2532
|
|
|
2305
2533
|
//#endregion
|
|
2306
2534
|
//#region src/internal/sync-runner.ts
|
|
2535
|
+
/**
|
|
2536
|
+
* Sync data between storage and transport layer
|
|
2537
|
+
*/
|
|
2307
2538
|
var SyncRunner = class {
|
|
2308
2539
|
storage;
|
|
2309
2540
|
transport;
|
|
@@ -2318,6 +2549,7 @@ var SyncRunner = class {
|
|
|
2318
2549
|
readyPromise;
|
|
2319
2550
|
metaRoomSubscription;
|
|
2320
2551
|
unsubscribeMetaFlock;
|
|
2552
|
+
docSubscriptions = /* @__PURE__ */ new Map();
|
|
2321
2553
|
constructor(options) {
|
|
2322
2554
|
this.storage = options.storage;
|
|
2323
2555
|
this.transport = options.transport;
|
|
@@ -2327,7 +2559,7 @@ var SyncRunner = class {
|
|
|
2327
2559
|
this.assetManager = options.assetManager;
|
|
2328
2560
|
this.flockHydrator = options.flockHydrator;
|
|
2329
2561
|
this.getMetaFlock = options.getMetaFlock;
|
|
2330
|
-
this.replaceMetaFlock = options.
|
|
2562
|
+
this.replaceMetaFlock = options.mergeFlock;
|
|
2331
2563
|
this.persistMeta = options.persistMeta;
|
|
2332
2564
|
}
|
|
2333
2565
|
async ready() {
|
|
@@ -2404,15 +2636,30 @@ var SyncRunner = class {
|
|
|
2404
2636
|
await this.ready();
|
|
2405
2637
|
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
2406
2638
|
if (!this.transport.isConnected()) await this.transport.connect();
|
|
2639
|
+
const existing = this.docSubscriptions.get(docId);
|
|
2640
|
+
if (existing) return existing;
|
|
2407
2641
|
const doc = await this.docManager.ensureDoc(docId);
|
|
2408
2642
|
const subscription = this.transport.joinDocRoom(docId, doc, params);
|
|
2643
|
+
const wrapped = {
|
|
2644
|
+
unsubscribe: () => {
|
|
2645
|
+
subscription.unsubscribe();
|
|
2646
|
+
if (this.docSubscriptions.get(docId) === wrapped) this.docSubscriptions.delete(docId);
|
|
2647
|
+
},
|
|
2648
|
+
firstSyncedWithRemote: subscription.firstSyncedWithRemote,
|
|
2649
|
+
get connected() {
|
|
2650
|
+
return subscription.connected;
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
this.docSubscriptions.set(docId, wrapped);
|
|
2409
2654
|
subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
2410
|
-
return
|
|
2655
|
+
return wrapped;
|
|
2411
2656
|
}
|
|
2412
|
-
async
|
|
2657
|
+
async destroy() {
|
|
2413
2658
|
await this.docManager.close();
|
|
2414
2659
|
this.metaRoomSubscription?.unsubscribe();
|
|
2415
2660
|
this.metaRoomSubscription = void 0;
|
|
2661
|
+
for (const sub of this.docSubscriptions.values()) sub.unsubscribe();
|
|
2662
|
+
this.docSubscriptions.clear();
|
|
2416
2663
|
if (this.unsubscribeMetaFlock) {
|
|
2417
2664
|
this.unsubscribeMetaFlock();
|
|
2418
2665
|
this.unsubscribeMetaFlock = void 0;
|
|
@@ -2463,16 +2710,17 @@ function createRepoState() {
|
|
|
2463
2710
|
//#region src/index.ts
|
|
2464
2711
|
const textEncoder = new TextEncoder();
|
|
2465
2712
|
const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
|
|
2466
|
-
var LoroRepo = class {
|
|
2713
|
+
var LoroRepo = class LoroRepo {
|
|
2467
2714
|
options;
|
|
2715
|
+
_destroyed = false;
|
|
2468
2716
|
transport;
|
|
2469
2717
|
storage;
|
|
2470
|
-
docFactory;
|
|
2471
2718
|
metaFlock = new Flock();
|
|
2472
2719
|
eventBus;
|
|
2473
2720
|
docManager;
|
|
2474
2721
|
metadataManager;
|
|
2475
2722
|
assetManager;
|
|
2723
|
+
assetTransport;
|
|
2476
2724
|
flockHydrator;
|
|
2477
2725
|
state;
|
|
2478
2726
|
syncRunner;
|
|
@@ -2480,14 +2728,13 @@ var LoroRepo = class {
|
|
|
2480
2728
|
this.options = options;
|
|
2481
2729
|
this.transport = options.transportAdapter;
|
|
2482
2730
|
this.storage = options.storageAdapter;
|
|
2483
|
-
this.
|
|
2731
|
+
this.assetTransport = options.assetTransportAdapter;
|
|
2484
2732
|
this.eventBus = new RepoEventBus();
|
|
2485
2733
|
this.state = createRepoState();
|
|
2486
2734
|
const configuredDebounce = options.docFrontierDebounceMs;
|
|
2487
2735
|
const docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
|
|
2488
2736
|
this.docManager = new DocManager({
|
|
2489
2737
|
storage: this.storage,
|
|
2490
|
-
docFactory: this.docFactory,
|
|
2491
2738
|
docFrontierDebounceMs,
|
|
2492
2739
|
getMetaFlock: () => this.metaFlock,
|
|
2493
2740
|
eventBus: this.eventBus,
|
|
@@ -2502,7 +2749,7 @@ var LoroRepo = class {
|
|
|
2502
2749
|
});
|
|
2503
2750
|
this.assetManager = new AssetManager({
|
|
2504
2751
|
storage: this.storage,
|
|
2505
|
-
assetTransport:
|
|
2752
|
+
assetTransport: this.assetTransport,
|
|
2506
2753
|
getMetaFlock: () => this.metaFlock,
|
|
2507
2754
|
eventBus: this.eventBus,
|
|
2508
2755
|
persistMeta: () => this.persistMeta(),
|
|
@@ -2523,96 +2770,133 @@ var LoroRepo = class {
|
|
|
2523
2770
|
assetManager: this.assetManager,
|
|
2524
2771
|
flockHydrator: this.flockHydrator,
|
|
2525
2772
|
getMetaFlock: () => this.metaFlock,
|
|
2526
|
-
|
|
2527
|
-
this.metaFlock
|
|
2773
|
+
mergeFlock: (snapshot) => {
|
|
2774
|
+
this.metaFlock.merge(snapshot);
|
|
2528
2775
|
},
|
|
2529
2776
|
persistMeta: () => this.persistMeta()
|
|
2530
2777
|
});
|
|
2531
2778
|
}
|
|
2779
|
+
static async create(options) {
|
|
2780
|
+
const repo = new LoroRepo(options);
|
|
2781
|
+
await repo.storage?.init?.();
|
|
2782
|
+
await repo.ready();
|
|
2783
|
+
return repo;
|
|
2784
|
+
}
|
|
2785
|
+
/**
|
|
2786
|
+
* Load meta from storage.
|
|
2787
|
+
*
|
|
2788
|
+
* You need to call this before all other operations to make the app functioning correctly.
|
|
2789
|
+
* Though we do that implicitly already
|
|
2790
|
+
*/
|
|
2532
2791
|
async ready() {
|
|
2533
2792
|
await this.syncRunner.ready();
|
|
2534
2793
|
}
|
|
2794
|
+
/**
|
|
2795
|
+
* Sync selected data via the transport adaptor
|
|
2796
|
+
* @param options
|
|
2797
|
+
*/
|
|
2535
2798
|
async sync(options = {}) {
|
|
2536
2799
|
await this.syncRunner.sync(options);
|
|
2537
2800
|
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Start syncing the metadata (Flock) room. It will establish a realtime connection to the transport adaptor.
|
|
2803
|
+
* All changes on the room will be synced to the Flock, and all changes on the Flock will be synced to the room.
|
|
2804
|
+
* @param params
|
|
2805
|
+
* @returns
|
|
2806
|
+
*/
|
|
2538
2807
|
async joinMetaRoom(params) {
|
|
2539
2808
|
return this.syncRunner.joinMetaRoom(params);
|
|
2540
2809
|
}
|
|
2810
|
+
/**
|
|
2811
|
+
* Start syncing the given doc. It will establish a realtime connection to the transport adaptor.
|
|
2812
|
+
* All changes on the doc will be synced to the transport, and all changes on the transport will be synced to the doc.
|
|
2813
|
+
*
|
|
2814
|
+
* All the changes on the room will be reflected on the same doc you get from `repo.openCollaborativeDoc(docId)`
|
|
2815
|
+
* @param docId
|
|
2816
|
+
* @param params
|
|
2817
|
+
* @returns
|
|
2818
|
+
*/
|
|
2541
2819
|
async joinDocRoom(docId, params) {
|
|
2542
2820
|
return this.syncRunner.joinDocRoom(docId, params);
|
|
2543
2821
|
}
|
|
2544
|
-
|
|
2545
|
-
|
|
2822
|
+
/**
|
|
2823
|
+
* Opens a document that is automatically persisted to the configured storage adapter.
|
|
2824
|
+
*
|
|
2825
|
+
* - Edits are saved to storage (debounced).
|
|
2826
|
+
* - Frontiers are synced to the metadata (Flock).
|
|
2827
|
+
* - Realtime collaboration is NOT enabled by default; use `joinDocRoom` to connect.
|
|
2828
|
+
*/
|
|
2829
|
+
async openPersistedDoc(docId) {
|
|
2830
|
+
return {
|
|
2831
|
+
doc: await this.docManager.openCollaborativeDoc(docId),
|
|
2832
|
+
syncOnce: () => {
|
|
2833
|
+
return this.sync({
|
|
2834
|
+
scope: "doc",
|
|
2835
|
+
docIds: [docId]
|
|
2836
|
+
});
|
|
2837
|
+
},
|
|
2838
|
+
joinRoom: (auth) => {
|
|
2839
|
+
return this.syncRunner.joinDocRoom(docId, { auth });
|
|
2840
|
+
}
|
|
2841
|
+
};
|
|
2546
2842
|
}
|
|
2547
|
-
async upsertDocMeta(docId, patch
|
|
2548
|
-
await this.ready();
|
|
2843
|
+
async upsertDocMeta(docId, patch) {
|
|
2549
2844
|
await this.metadataManager.upsert(docId, patch);
|
|
2550
2845
|
}
|
|
2551
2846
|
async getDocMeta(docId) {
|
|
2552
|
-
await this.ready();
|
|
2553
2847
|
return this.metadataManager.get(docId);
|
|
2554
2848
|
}
|
|
2555
2849
|
async listDoc(query) {
|
|
2556
|
-
|
|
2557
|
-
return this.metadataManager.list(query);
|
|
2850
|
+
return this.metadataManager.listDoc(query);
|
|
2558
2851
|
}
|
|
2559
|
-
|
|
2852
|
+
getMeta() {
|
|
2560
2853
|
return this.metaFlock;
|
|
2561
2854
|
}
|
|
2562
2855
|
watch(listener, filter = {}) {
|
|
2563
2856
|
return this.eventBus.watch(listener, filter);
|
|
2564
2857
|
}
|
|
2565
2858
|
/**
|
|
2566
|
-
* Opens
|
|
2567
|
-
*
|
|
2859
|
+
* Opens a detached `LoroDoc` snapshot.
|
|
2860
|
+
*
|
|
2861
|
+
* - **No Persistence**: Edits to this document are NOT saved to storage.
|
|
2862
|
+
* - **No Sync**: This document does not participate in realtime updates.
|
|
2863
|
+
* - **Use Case**: Ideal for read-only history inspection, temporary drafts, or conflict resolution without affecting the main state.
|
|
2568
2864
|
*/
|
|
2569
|
-
async
|
|
2570
|
-
|
|
2571
|
-
const whenSyncedWithRemote = this.whenDocInSyncWithRemote(docId);
|
|
2572
|
-
return this.docManager.openCollaborativeDoc(docId, whenSyncedWithRemote);
|
|
2865
|
+
async openDetachedDoc(docId) {
|
|
2866
|
+
return this.docManager.openDetachedDoc(docId);
|
|
2573
2867
|
}
|
|
2574
2868
|
/**
|
|
2575
|
-
*
|
|
2576
|
-
*
|
|
2869
|
+
* Explicitly unloads a document from memory.
|
|
2870
|
+
*
|
|
2871
|
+
* - **Persists Immediately**: Forces a save of the document's current state to storage.
|
|
2872
|
+
* - **Frees Memory**: Removes the document from the internal cache.
|
|
2873
|
+
* - **Note**: If the document is currently being synced (via `joinDocRoom`), you should also unsubscribe from the room to fully release resources.
|
|
2577
2874
|
*/
|
|
2578
|
-
async
|
|
2579
|
-
await this.
|
|
2580
|
-
|
|
2875
|
+
async unloadDoc(docId) {
|
|
2876
|
+
await this.docManager.unloadDoc(docId);
|
|
2877
|
+
}
|
|
2878
|
+
async flush() {
|
|
2879
|
+
await this.docManager.flush();
|
|
2581
2880
|
}
|
|
2582
2881
|
async uploadAsset(params) {
|
|
2583
|
-
await this.ready();
|
|
2584
2882
|
return this.assetManager.uploadAsset(params);
|
|
2585
2883
|
}
|
|
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
2884
|
async linkAsset(docId, params) {
|
|
2595
|
-
await this.ready();
|
|
2596
2885
|
return this.assetManager.linkAsset(docId, params);
|
|
2597
2886
|
}
|
|
2598
2887
|
async fetchAsset(assetId) {
|
|
2599
|
-
await this.ready();
|
|
2600
2888
|
return this.assetManager.fetchAsset(assetId);
|
|
2601
2889
|
}
|
|
2602
2890
|
async unlinkAsset(docId, assetId) {
|
|
2603
|
-
await this.ready();
|
|
2604
2891
|
await this.assetManager.unlinkAsset(docId, assetId);
|
|
2605
2892
|
}
|
|
2606
2893
|
async listAssets(docId) {
|
|
2607
|
-
await this.ready();
|
|
2608
2894
|
return this.assetManager.listAssets(docId);
|
|
2609
2895
|
}
|
|
2610
2896
|
async ensureAsset(assetId) {
|
|
2611
|
-
await this.ready();
|
|
2612
2897
|
return this.assetManager.ensureAsset(assetId);
|
|
2613
2898
|
}
|
|
2614
2899
|
async gcAssets(options = {}) {
|
|
2615
|
-
await this.ready();
|
|
2616
2900
|
return this.assetManager.gcAssets(options);
|
|
2617
2901
|
}
|
|
2618
2902
|
async persistMeta() {
|
|
@@ -2624,6 +2908,17 @@ var LoroRepo = class {
|
|
|
2624
2908
|
update: encoded
|
|
2625
2909
|
});
|
|
2626
2910
|
}
|
|
2911
|
+
get destroyed() {
|
|
2912
|
+
return this._destroyed;
|
|
2913
|
+
}
|
|
2914
|
+
async destroy() {
|
|
2915
|
+
if (this._destroyed) return;
|
|
2916
|
+
this._destroyed = true;
|
|
2917
|
+
await this.syncRunner.destroy();
|
|
2918
|
+
this.assetTransport?.close?.();
|
|
2919
|
+
this.storage?.close?.();
|
|
2920
|
+
await this.transport?.close();
|
|
2921
|
+
}
|
|
2627
2922
|
};
|
|
2628
2923
|
|
|
2629
2924
|
//#endregion
|