loro-repo 0.1.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/README.md +3 -0
- package/dist/{index.d.cts → index-DsCaL9JX.d.cts} +159 -111
- package/dist/{index.d.ts → index-tq65q3qY.d.ts} +160 -112
- package/dist/index.cjs +1620 -835
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1596 -840
- package/dist/index.js.map +1 -1
- package/package.json +8 -5
package/dist/index.cjs
CHANGED
|
@@ -1,16 +1,86 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
1
28
|
let __loro_dev_flock = require("@loro-dev/flock");
|
|
2
|
-
let loro_crdt = require("loro-crdt");
|
|
3
29
|
let loro_adaptors = require("loro-adaptors");
|
|
4
30
|
let loro_protocol = require("loro-protocol");
|
|
5
31
|
let loro_websocket = require("loro-websocket");
|
|
32
|
+
let loro_crdt = require("loro-crdt");
|
|
33
|
+
let node_fs = require("node:fs");
|
|
34
|
+
let node_path = require("node:path");
|
|
35
|
+
node_path = __toESM(node_path);
|
|
36
|
+
let node_crypto = require("node:crypto");
|
|
6
37
|
|
|
7
38
|
//#region src/loro-adaptor.ts
|
|
8
39
|
function createRepoFlockAdaptorFromDoc(flock, config = {}) {
|
|
9
40
|
return new loro_adaptors.FlockAdaptor(flock, config);
|
|
10
41
|
}
|
|
11
42
|
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/internal/debug.ts
|
|
45
|
+
const getEnv = () => {
|
|
46
|
+
if (typeof globalThis !== "object" || globalThis === null) return;
|
|
47
|
+
return globalThis.process?.env;
|
|
48
|
+
};
|
|
49
|
+
const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
|
|
50
|
+
const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
|
|
51
|
+
const wildcardTokens = new Set([
|
|
52
|
+
"*",
|
|
53
|
+
"1",
|
|
54
|
+
"true",
|
|
55
|
+
"all"
|
|
56
|
+
]);
|
|
57
|
+
const namespaceSet = new Set(normalizedNamespaces);
|
|
58
|
+
const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
|
|
59
|
+
const isDebugEnabled = (namespace) => {
|
|
60
|
+
if (!namespaceSet.size) return false;
|
|
61
|
+
if (!namespace) return hasWildcard;
|
|
62
|
+
const normalized = namespace.toLowerCase();
|
|
63
|
+
if (hasWildcard) return true;
|
|
64
|
+
if (namespaceSet.has(normalized)) return true;
|
|
65
|
+
const [root] = normalized.split(":");
|
|
66
|
+
return namespaceSet.has(root);
|
|
67
|
+
};
|
|
68
|
+
const createDebugLogger = (namespace) => {
|
|
69
|
+
const normalized = namespace.toLowerCase();
|
|
70
|
+
return (...args) => {
|
|
71
|
+
if (!isDebugEnabled(normalized)) return;
|
|
72
|
+
const prefix = `[loro-repo:${namespace}]`;
|
|
73
|
+
if (args.length === 0) {
|
|
74
|
+
console.info(prefix);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.info(prefix, ...args);
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
|
|
12
81
|
//#endregion
|
|
13
82
|
//#region src/transport/websocket.ts
|
|
83
|
+
const debug = createDebugLogger("transport:websocket");
|
|
14
84
|
function withTimeout(promise, timeoutMs) {
|
|
15
85
|
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
16
86
|
return new Promise((resolve, reject) => {
|
|
@@ -55,30 +125,51 @@ var WebSocketTransportAdapter = class {
|
|
|
55
125
|
}
|
|
56
126
|
async connect(_options) {
|
|
57
127
|
const client = this.ensureClient();
|
|
58
|
-
|
|
59
|
-
|
|
128
|
+
debug("connect requested", { status: client.getStatus() });
|
|
129
|
+
try {
|
|
130
|
+
await client.connect();
|
|
131
|
+
debug("client.connect resolved");
|
|
132
|
+
await client.waitConnected();
|
|
133
|
+
debug("client.waitConnected resolved", { status: client.getStatus() });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
debug("connect failed", error);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
60
138
|
}
|
|
61
139
|
async close() {
|
|
140
|
+
debug("close requested", {
|
|
141
|
+
docSessions: this.docSessions.size,
|
|
142
|
+
metadataSession: Boolean(this.metadataSession)
|
|
143
|
+
});
|
|
62
144
|
for (const [docId] of this.docSessions) await this.leaveDocSession(docId).catch(() => {});
|
|
63
145
|
this.docSessions.clear();
|
|
64
146
|
await this.teardownMetadataSession().catch(() => {});
|
|
65
147
|
if (this.client) {
|
|
66
|
-
this.client
|
|
148
|
+
const client = this.client;
|
|
67
149
|
this.client = void 0;
|
|
150
|
+
client.destroy();
|
|
151
|
+
debug("websocket client destroyed");
|
|
68
152
|
}
|
|
153
|
+
debug("close completed");
|
|
69
154
|
}
|
|
70
155
|
isConnected() {
|
|
71
156
|
return this.client?.getStatus() === "connected";
|
|
72
157
|
}
|
|
73
158
|
async syncMeta(flock, options) {
|
|
74
|
-
if (!this.options.metadataRoomId)
|
|
159
|
+
if (!this.options.metadataRoomId) {
|
|
160
|
+
debug("syncMeta skipped; metadata room not configured");
|
|
161
|
+
return { ok: true };
|
|
162
|
+
}
|
|
163
|
+
debug("syncMeta requested", { roomId: this.options.metadataRoomId });
|
|
75
164
|
try {
|
|
76
165
|
await withTimeout((await this.ensureMetadataSession(flock, {
|
|
77
166
|
roomId: this.options.metadataRoomId,
|
|
78
167
|
auth: this.options.metadataAuth
|
|
79
168
|
})).firstSynced, options?.timeout);
|
|
169
|
+
debug("syncMeta completed", { roomId: this.options.metadataRoomId });
|
|
80
170
|
return { ok: true };
|
|
81
|
-
} catch {
|
|
171
|
+
} catch (error) {
|
|
172
|
+
debug("syncMeta failed", error);
|
|
82
173
|
return { ok: false };
|
|
83
174
|
}
|
|
84
175
|
}
|
|
@@ -87,6 +178,10 @@ var WebSocketTransportAdapter = class {
|
|
|
87
178
|
const roomId = normalizeRoomId(params?.roomId, fallback);
|
|
88
179
|
if (!roomId) throw new Error("Metadata room id not configured");
|
|
89
180
|
const auth = params?.auth ?? this.options.metadataAuth;
|
|
181
|
+
debug("joinMetaRoom requested", {
|
|
182
|
+
roomId,
|
|
183
|
+
hasAuth: Boolean(auth && auth.length)
|
|
184
|
+
});
|
|
90
185
|
const ensure = this.ensureMetadataSession(flock, {
|
|
91
186
|
roomId,
|
|
92
187
|
auth
|
|
@@ -97,7 +192,14 @@ var WebSocketTransportAdapter = class {
|
|
|
97
192
|
unsubscribe: () => {
|
|
98
193
|
ensure.then((session) => {
|
|
99
194
|
session.refCount = Math.max(0, session.refCount - 1);
|
|
100
|
-
|
|
195
|
+
debug("metadata session refCount decremented", {
|
|
196
|
+
roomId: session.roomId,
|
|
197
|
+
refCount: session.refCount
|
|
198
|
+
});
|
|
199
|
+
if (session.refCount === 0) {
|
|
200
|
+
debug("tearing down metadata session due to refCount=0", { roomId: session.roomId });
|
|
201
|
+
this.teardownMetadataSession(session).catch(() => {});
|
|
202
|
+
}
|
|
101
203
|
});
|
|
102
204
|
},
|
|
103
205
|
firstSyncedWithRemote: firstSynced,
|
|
@@ -107,18 +209,37 @@ var WebSocketTransportAdapter = class {
|
|
|
107
209
|
};
|
|
108
210
|
ensure.then((session) => {
|
|
109
211
|
session.refCount += 1;
|
|
212
|
+
debug("metadata session refCount incremented", {
|
|
213
|
+
roomId: session.roomId,
|
|
214
|
+
refCount: session.refCount
|
|
215
|
+
});
|
|
110
216
|
});
|
|
111
217
|
return subscription;
|
|
112
218
|
}
|
|
113
219
|
async syncDoc(docId, doc, options) {
|
|
220
|
+
debug("syncDoc requested", { docId });
|
|
114
221
|
try {
|
|
115
|
-
|
|
222
|
+
const session = await this.ensureDocSession(docId, doc, {});
|
|
223
|
+
await withTimeout(session.firstSynced, options?.timeout);
|
|
224
|
+
debug("syncDoc completed", {
|
|
225
|
+
docId,
|
|
226
|
+
roomId: session.roomId
|
|
227
|
+
});
|
|
116
228
|
return { ok: true };
|
|
117
|
-
} catch {
|
|
229
|
+
} catch (error) {
|
|
230
|
+
debug("syncDoc failed", {
|
|
231
|
+
docId,
|
|
232
|
+
error
|
|
233
|
+
});
|
|
118
234
|
return { ok: false };
|
|
119
235
|
}
|
|
120
236
|
}
|
|
121
237
|
joinDocRoom(docId, doc, params) {
|
|
238
|
+
debug("joinDocRoom requested", {
|
|
239
|
+
docId,
|
|
240
|
+
roomParamType: params?.roomId ? typeof params.roomId === "string" ? "string" : "uint8array" : void 0,
|
|
241
|
+
hasAuthOverride: Boolean(params?.auth && params.auth.length)
|
|
242
|
+
});
|
|
122
243
|
const ensure = this.ensureDocSession(docId, doc, params ?? {});
|
|
123
244
|
const firstSynced = ensure.then((session) => session.firstSynced);
|
|
124
245
|
const getConnected = () => this.isConnected();
|
|
@@ -126,6 +247,11 @@ var WebSocketTransportAdapter = class {
|
|
|
126
247
|
unsubscribe: () => {
|
|
127
248
|
ensure.then((session) => {
|
|
128
249
|
session.refCount = Math.max(0, session.refCount - 1);
|
|
250
|
+
debug("doc session refCount decremented", {
|
|
251
|
+
docId,
|
|
252
|
+
roomId: session.roomId,
|
|
253
|
+
refCount: session.refCount
|
|
254
|
+
});
|
|
129
255
|
if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
|
|
130
256
|
});
|
|
131
257
|
},
|
|
@@ -136,12 +262,24 @@ var WebSocketTransportAdapter = class {
|
|
|
136
262
|
};
|
|
137
263
|
ensure.then((session) => {
|
|
138
264
|
session.refCount += 1;
|
|
265
|
+
debug("doc session refCount incremented", {
|
|
266
|
+
docId,
|
|
267
|
+
roomId: session.roomId,
|
|
268
|
+
refCount: session.refCount
|
|
269
|
+
});
|
|
139
270
|
});
|
|
140
271
|
return subscription;
|
|
141
272
|
}
|
|
142
273
|
ensureClient() {
|
|
143
|
-
if (this.client)
|
|
274
|
+
if (this.client) {
|
|
275
|
+
debug("reusing websocket client", { status: this.client.getStatus() });
|
|
276
|
+
return this.client;
|
|
277
|
+
}
|
|
144
278
|
const { url, client: clientOptions } = this.options;
|
|
279
|
+
debug("creating websocket client", {
|
|
280
|
+
url,
|
|
281
|
+
clientOptionsKeys: clientOptions ? Object.keys(clientOptions) : []
|
|
282
|
+
});
|
|
145
283
|
const client = new loro_websocket.LoroWebsocketClient({
|
|
146
284
|
url,
|
|
147
285
|
...clientOptions
|
|
@@ -150,22 +288,49 @@ var WebSocketTransportAdapter = class {
|
|
|
150
288
|
return client;
|
|
151
289
|
}
|
|
152
290
|
async ensureMetadataSession(flock, params) {
|
|
291
|
+
debug("ensureMetadataSession invoked", {
|
|
292
|
+
roomId: params.roomId,
|
|
293
|
+
hasAuth: Boolean(params.auth && params.auth.length)
|
|
294
|
+
});
|
|
153
295
|
const client = this.ensureClient();
|
|
154
296
|
await client.waitConnected();
|
|
155
|
-
|
|
156
|
-
if (this.metadataSession
|
|
297
|
+
debug("websocket client ready for metadata session", { status: client.getStatus() });
|
|
298
|
+
if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) {
|
|
299
|
+
debug("reusing metadata session", {
|
|
300
|
+
roomId: this.metadataSession.roomId,
|
|
301
|
+
refCount: this.metadataSession.refCount
|
|
302
|
+
});
|
|
303
|
+
return this.metadataSession;
|
|
304
|
+
}
|
|
305
|
+
if (this.metadataSession) {
|
|
306
|
+
debug("tearing down previous metadata session", { roomId: this.metadataSession.roomId });
|
|
307
|
+
await this.teardownMetadataSession(this.metadataSession).catch(() => {});
|
|
308
|
+
}
|
|
157
309
|
const configuredType = this.options.metadataCrdtType;
|
|
158
310
|
if (configuredType && configuredType !== loro_protocol.CrdtType.Flock) throw new Error(`metadataCrdtType must be ${loro_protocol.CrdtType.Flock} when syncing Flock metadata`);
|
|
159
311
|
const adaptor = createRepoFlockAdaptorFromDoc(flock, this.options.metadataAdaptorConfig ?? {});
|
|
312
|
+
debug("joining metadata room", {
|
|
313
|
+
roomId: params.roomId,
|
|
314
|
+
hasAuth: Boolean(params.auth && params.auth.length)
|
|
315
|
+
});
|
|
160
316
|
const room = await client.join({
|
|
161
317
|
roomId: params.roomId,
|
|
162
318
|
crdtAdaptor: adaptor,
|
|
163
319
|
auth: params.auth
|
|
164
320
|
});
|
|
321
|
+
const firstSynced = room.waitForReachingServerVersion();
|
|
322
|
+
firstSynced.then(() => {
|
|
323
|
+
debug("metadata session firstSynced resolved", { roomId: params.roomId });
|
|
324
|
+
}, (error) => {
|
|
325
|
+
debug("metadata session firstSynced rejected", {
|
|
326
|
+
roomId: params.roomId,
|
|
327
|
+
error
|
|
328
|
+
});
|
|
329
|
+
});
|
|
165
330
|
const session = {
|
|
166
331
|
adaptor,
|
|
167
332
|
room,
|
|
168
|
-
firstSynced
|
|
333
|
+
firstSynced,
|
|
169
334
|
flock,
|
|
170
335
|
roomId: params.roomId,
|
|
171
336
|
auth: params.auth,
|
|
@@ -177,34 +342,83 @@ var WebSocketTransportAdapter = class {
|
|
|
177
342
|
async teardownMetadataSession(session) {
|
|
178
343
|
const target = session ?? this.metadataSession;
|
|
179
344
|
if (!target) return;
|
|
345
|
+
debug("teardownMetadataSession invoked", { roomId: target.roomId });
|
|
180
346
|
if (this.metadataSession === target) this.metadataSession = void 0;
|
|
181
347
|
const { adaptor, room } = target;
|
|
182
348
|
try {
|
|
183
349
|
await room.leave();
|
|
184
|
-
|
|
350
|
+
debug("metadata room left", { roomId: target.roomId });
|
|
351
|
+
} catch (error) {
|
|
352
|
+
debug("metadata room leave failed; destroying", {
|
|
353
|
+
roomId: target.roomId,
|
|
354
|
+
error
|
|
355
|
+
});
|
|
185
356
|
await room.destroy().catch(() => {});
|
|
186
357
|
}
|
|
187
358
|
adaptor.destroy();
|
|
359
|
+
debug("metadata session destroyed", { roomId: target.roomId });
|
|
188
360
|
}
|
|
189
361
|
async ensureDocSession(docId, doc, params) {
|
|
362
|
+
debug("ensureDocSession invoked", { docId });
|
|
190
363
|
const client = this.ensureClient();
|
|
191
364
|
await client.waitConnected();
|
|
365
|
+
debug("websocket client ready for doc session", {
|
|
366
|
+
docId,
|
|
367
|
+
status: client.getStatus()
|
|
368
|
+
});
|
|
192
369
|
const existing = this.docSessions.get(docId);
|
|
193
370
|
const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
|
|
194
371
|
const roomId = normalizeRoomId(params.roomId, derivedRoomId);
|
|
195
372
|
const auth = params.auth ?? this.options.docAuth?.(docId);
|
|
196
|
-
|
|
197
|
-
|
|
373
|
+
debug("doc session params resolved", {
|
|
374
|
+
docId,
|
|
375
|
+
roomId,
|
|
376
|
+
hasAuth: Boolean(auth && auth.length)
|
|
377
|
+
});
|
|
378
|
+
if (existing && existing.doc === doc && existing.roomId === roomId) {
|
|
379
|
+
debug("reusing doc session", {
|
|
380
|
+
docId,
|
|
381
|
+
roomId,
|
|
382
|
+
refCount: existing.refCount
|
|
383
|
+
});
|
|
384
|
+
return existing;
|
|
385
|
+
}
|
|
386
|
+
if (existing) {
|
|
387
|
+
debug("doc session mismatch; leaving existing session", {
|
|
388
|
+
docId,
|
|
389
|
+
previousRoomId: existing.roomId,
|
|
390
|
+
nextRoomId: roomId
|
|
391
|
+
});
|
|
392
|
+
await this.leaveDocSession(docId).catch(() => {});
|
|
393
|
+
}
|
|
198
394
|
const adaptor = new loro_adaptors.LoroAdaptor(doc);
|
|
395
|
+
debug("joining doc room", {
|
|
396
|
+
docId,
|
|
397
|
+
roomId,
|
|
398
|
+
hasAuth: Boolean(auth && auth.length)
|
|
399
|
+
});
|
|
199
400
|
const room = await client.join({
|
|
200
401
|
roomId,
|
|
201
402
|
crdtAdaptor: adaptor,
|
|
202
403
|
auth
|
|
203
404
|
});
|
|
405
|
+
const firstSynced = room.waitForReachingServerVersion();
|
|
406
|
+
firstSynced.then(() => {
|
|
407
|
+
debug("doc session firstSynced resolved", {
|
|
408
|
+
docId,
|
|
409
|
+
roomId
|
|
410
|
+
});
|
|
411
|
+
}, (error) => {
|
|
412
|
+
debug("doc session firstSynced rejected", {
|
|
413
|
+
docId,
|
|
414
|
+
roomId,
|
|
415
|
+
error
|
|
416
|
+
});
|
|
417
|
+
});
|
|
204
418
|
const session = {
|
|
205
419
|
adaptor,
|
|
206
420
|
room,
|
|
207
|
-
firstSynced
|
|
421
|
+
firstSynced,
|
|
208
422
|
doc,
|
|
209
423
|
roomId,
|
|
210
424
|
refCount: 0
|
|
@@ -214,14 +428,34 @@ var WebSocketTransportAdapter = class {
|
|
|
214
428
|
}
|
|
215
429
|
async leaveDocSession(docId) {
|
|
216
430
|
const session = this.docSessions.get(docId);
|
|
217
|
-
if (!session)
|
|
431
|
+
if (!session) {
|
|
432
|
+
debug("leaveDocSession invoked but no session found", { docId });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
218
435
|
this.docSessions.delete(docId);
|
|
436
|
+
debug("leaving doc session", {
|
|
437
|
+
docId,
|
|
438
|
+
roomId: session.roomId
|
|
439
|
+
});
|
|
219
440
|
try {
|
|
220
441
|
await session.room.leave();
|
|
221
|
-
|
|
442
|
+
debug("doc room left", {
|
|
443
|
+
docId,
|
|
444
|
+
roomId: session.roomId
|
|
445
|
+
});
|
|
446
|
+
} catch (error) {
|
|
447
|
+
debug("doc room leave failed; destroying", {
|
|
448
|
+
docId,
|
|
449
|
+
roomId: session.roomId,
|
|
450
|
+
error
|
|
451
|
+
});
|
|
222
452
|
await session.room.destroy().catch(() => {});
|
|
223
453
|
}
|
|
224
454
|
session.adaptor.destroy();
|
|
455
|
+
debug("doc session destroyed", {
|
|
456
|
+
docId,
|
|
457
|
+
roomId: session.roomId
|
|
458
|
+
});
|
|
225
459
|
}
|
|
226
460
|
};
|
|
227
461
|
|
|
@@ -483,7 +717,7 @@ const DEFAULT_META_STORE = "meta";
|
|
|
483
717
|
const DEFAULT_ASSET_STORE = "assets";
|
|
484
718
|
const DEFAULT_DOC_UPDATE_STORE = "doc-updates";
|
|
485
719
|
const DEFAULT_META_KEY = "snapshot";
|
|
486
|
-
const textDecoder = new TextDecoder();
|
|
720
|
+
const textDecoder$1 = new TextDecoder();
|
|
487
721
|
function describeUnknown(cause) {
|
|
488
722
|
if (typeof cause === "string") return cause;
|
|
489
723
|
if (typeof cause === "number" || typeof cause === "boolean") return String(cause);
|
|
@@ -584,7 +818,7 @@ var IndexedDBStorageAdaptor = class {
|
|
|
584
818
|
const bytes = await this.getBinary(this.metaStore, this.metaKey);
|
|
585
819
|
if (!bytes) return void 0;
|
|
586
820
|
try {
|
|
587
|
-
const json = textDecoder.decode(bytes);
|
|
821
|
+
const json = textDecoder$1.decode(bytes);
|
|
588
822
|
const bundle = JSON.parse(json);
|
|
589
823
|
const flock = new __loro_dev_flock.Flock();
|
|
590
824
|
flock.importJson(bundle);
|
|
@@ -728,15 +962,206 @@ var IndexedDBStorageAdaptor = class {
|
|
|
728
962
|
};
|
|
729
963
|
|
|
730
964
|
//#endregion
|
|
731
|
-
//#region src/
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
965
|
+
//#region src/storage/filesystem.ts
|
|
966
|
+
const textDecoder = new TextDecoder();
|
|
967
|
+
var FileSystemStorageAdaptor = class {
|
|
968
|
+
baseDir;
|
|
969
|
+
docsDir;
|
|
970
|
+
assetsDir;
|
|
971
|
+
metaPath;
|
|
972
|
+
initPromise;
|
|
973
|
+
updateCounter = 0;
|
|
974
|
+
constructor(options = {}) {
|
|
975
|
+
this.baseDir = node_path.resolve(options.baseDir ?? node_path.join(process.cwd(), ".loro-repo"));
|
|
976
|
+
this.docsDir = node_path.join(this.baseDir, options.docsDirName ?? "docs");
|
|
977
|
+
this.assetsDir = node_path.join(this.baseDir, options.assetsDirName ?? "assets");
|
|
978
|
+
this.metaPath = node_path.join(this.baseDir, options.metaFileName ?? "meta.json");
|
|
979
|
+
this.initPromise = this.ensureLayout();
|
|
980
|
+
}
|
|
981
|
+
async save(payload) {
|
|
982
|
+
await this.initPromise;
|
|
983
|
+
switch (payload.type) {
|
|
984
|
+
case "doc-snapshot":
|
|
985
|
+
await this.writeDocSnapshot(payload.docId, payload.snapshot);
|
|
986
|
+
return;
|
|
987
|
+
case "doc-update":
|
|
988
|
+
await this.enqueueDocUpdate(payload.docId, payload.update);
|
|
989
|
+
return;
|
|
990
|
+
case "asset":
|
|
991
|
+
await this.writeAsset(payload.assetId, payload.data);
|
|
992
|
+
return;
|
|
993
|
+
case "meta":
|
|
994
|
+
await writeFileAtomic(this.metaPath, payload.update);
|
|
995
|
+
return;
|
|
996
|
+
default: throw new Error(`Unsupported payload type: ${payload.type}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async deleteAsset(assetId) {
|
|
1000
|
+
await this.initPromise;
|
|
1001
|
+
await removeIfExists(this.assetPath(assetId));
|
|
1002
|
+
}
|
|
1003
|
+
async loadDoc(docId) {
|
|
1004
|
+
await this.initPromise;
|
|
1005
|
+
const snapshotBytes = await readFileIfExists(this.docSnapshotPath(docId));
|
|
1006
|
+
const updateDir = this.docUpdatesDir(docId);
|
|
1007
|
+
const updateFiles = await listFiles(updateDir);
|
|
1008
|
+
if (!snapshotBytes && updateFiles.length === 0) return;
|
|
1009
|
+
const doc = snapshotBytes ? loro_crdt.LoroDoc.fromSnapshot(snapshotBytes) : new loro_crdt.LoroDoc();
|
|
1010
|
+
if (updateFiles.length === 0) return doc;
|
|
1011
|
+
const updatePaths = updateFiles.map((file) => node_path.join(updateDir, file));
|
|
1012
|
+
for (const updatePath of updatePaths) {
|
|
1013
|
+
const update = await readFileIfExists(updatePath);
|
|
1014
|
+
if (!update) continue;
|
|
1015
|
+
doc.import(update);
|
|
1016
|
+
}
|
|
1017
|
+
await Promise.all(updatePaths.map((filePath) => removeIfExists(filePath)));
|
|
1018
|
+
const consolidated = doc.export({ mode: "snapshot" });
|
|
1019
|
+
await this.writeDocSnapshot(docId, consolidated);
|
|
1020
|
+
return doc;
|
|
1021
|
+
}
|
|
1022
|
+
async loadMeta() {
|
|
1023
|
+
await this.initPromise;
|
|
1024
|
+
const bytes = await readFileIfExists(this.metaPath);
|
|
1025
|
+
if (!bytes) return void 0;
|
|
1026
|
+
try {
|
|
1027
|
+
const bundle = JSON.parse(textDecoder.decode(bytes));
|
|
1028
|
+
const flock = new __loro_dev_flock.Flock();
|
|
1029
|
+
flock.importJson(bundle);
|
|
1030
|
+
return flock;
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
throw new Error("Failed to hydrate metadata snapshot", { cause: error });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
async loadAsset(assetId) {
|
|
1036
|
+
await this.initPromise;
|
|
1037
|
+
return readFileIfExists(this.assetPath(assetId));
|
|
1038
|
+
}
|
|
1039
|
+
async ensureLayout() {
|
|
1040
|
+
await Promise.all([
|
|
1041
|
+
ensureDir(this.baseDir),
|
|
1042
|
+
ensureDir(this.docsDir),
|
|
1043
|
+
ensureDir(this.assetsDir)
|
|
1044
|
+
]);
|
|
1045
|
+
}
|
|
1046
|
+
async writeDocSnapshot(docId, snapshot) {
|
|
1047
|
+
await ensureDir(this.docDir(docId));
|
|
1048
|
+
await writeFileAtomic(this.docSnapshotPath(docId), snapshot);
|
|
1049
|
+
}
|
|
1050
|
+
async enqueueDocUpdate(docId, update) {
|
|
1051
|
+
const dir = this.docUpdatesDir(docId);
|
|
1052
|
+
await ensureDir(dir);
|
|
1053
|
+
const counter = this.updateCounter = (this.updateCounter + 1) % 1e6;
|
|
1054
|
+
const fileName = `${Date.now().toString().padStart(13, "0")}-${counter.toString().padStart(6, "0")}.bin`;
|
|
1055
|
+
await writeFileAtomic(node_path.join(dir, fileName), update);
|
|
1056
|
+
}
|
|
1057
|
+
async writeAsset(assetId, data) {
|
|
1058
|
+
const filePath = this.assetPath(assetId);
|
|
1059
|
+
await ensureDir(node_path.dirname(filePath));
|
|
1060
|
+
await writeFileAtomic(filePath, data);
|
|
1061
|
+
}
|
|
1062
|
+
docDir(docId) {
|
|
1063
|
+
return node_path.join(this.docsDir, encodeComponent(docId));
|
|
1064
|
+
}
|
|
1065
|
+
docSnapshotPath(docId) {
|
|
1066
|
+
return node_path.join(this.docDir(docId), "snapshot.bin");
|
|
1067
|
+
}
|
|
1068
|
+
docUpdatesDir(docId) {
|
|
1069
|
+
return node_path.join(this.docDir(docId), "updates");
|
|
1070
|
+
}
|
|
1071
|
+
assetPath(assetId) {
|
|
1072
|
+
return node_path.join(this.assetsDir, encodeComponent(assetId));
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
function encodeComponent(value) {
|
|
1076
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
1077
|
+
}
|
|
1078
|
+
async function ensureDir(dir) {
|
|
1079
|
+
await node_fs.promises.mkdir(dir, { recursive: true });
|
|
1080
|
+
}
|
|
1081
|
+
async function readFileIfExists(filePath) {
|
|
1082
|
+
try {
|
|
1083
|
+
const data = await node_fs.promises.readFile(filePath);
|
|
1084
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
if (error.code === "ENOENT") return;
|
|
1087
|
+
throw error;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
async function removeIfExists(filePath) {
|
|
1091
|
+
try {
|
|
1092
|
+
await node_fs.promises.rm(filePath);
|
|
1093
|
+
} catch (error) {
|
|
1094
|
+
if (error.code === "ENOENT") return;
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
async function listFiles(dir) {
|
|
1099
|
+
try {
|
|
1100
|
+
return (await node_fs.promises.readdir(dir)).sort();
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
if (error.code === "ENOENT") return [];
|
|
1103
|
+
throw error;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async function writeFileAtomic(targetPath, data) {
|
|
1107
|
+
const dir = node_path.dirname(targetPath);
|
|
1108
|
+
await ensureDir(dir);
|
|
1109
|
+
const tempPath = node_path.join(dir, `.tmp-${(0, node_crypto.randomUUID)()}`);
|
|
1110
|
+
await node_fs.promises.writeFile(tempPath, data);
|
|
1111
|
+
await node_fs.promises.rename(tempPath, targetPath);
|
|
739
1112
|
}
|
|
1113
|
+
|
|
1114
|
+
//#endregion
|
|
1115
|
+
//#region src/internal/event-bus.ts
|
|
1116
|
+
var RepoEventBus = class {
|
|
1117
|
+
watchers = /* @__PURE__ */ new Set();
|
|
1118
|
+
eventByStack = [];
|
|
1119
|
+
watch(listener, filter = {}) {
|
|
1120
|
+
const entry = {
|
|
1121
|
+
listener,
|
|
1122
|
+
filter
|
|
1123
|
+
};
|
|
1124
|
+
this.watchers.add(entry);
|
|
1125
|
+
return { unsubscribe: () => {
|
|
1126
|
+
this.watchers.delete(entry);
|
|
1127
|
+
} };
|
|
1128
|
+
}
|
|
1129
|
+
emit(event) {
|
|
1130
|
+
for (const entry of this.watchers) if (this.shouldNotify(entry.filter, event)) entry.listener(event);
|
|
1131
|
+
}
|
|
1132
|
+
clear() {
|
|
1133
|
+
this.watchers.clear();
|
|
1134
|
+
this.eventByStack.length = 0;
|
|
1135
|
+
}
|
|
1136
|
+
pushEventBy(by) {
|
|
1137
|
+
this.eventByStack.push(by);
|
|
1138
|
+
}
|
|
1139
|
+
popEventBy() {
|
|
1140
|
+
this.eventByStack.pop();
|
|
1141
|
+
}
|
|
1142
|
+
resolveEventBy(defaultBy) {
|
|
1143
|
+
const index = this.eventByStack.length - 1;
|
|
1144
|
+
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
1145
|
+
}
|
|
1146
|
+
shouldNotify(filter, event) {
|
|
1147
|
+
if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
|
|
1148
|
+
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
1149
|
+
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
1150
|
+
const docId = (() => {
|
|
1151
|
+
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
1152
|
+
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
1153
|
+
})();
|
|
1154
|
+
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
1155
|
+
if (filter.docIds && !docId) return false;
|
|
1156
|
+
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
1157
|
+
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
1158
|
+
}
|
|
1159
|
+
return true;
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
//#endregion
|
|
1164
|
+
//#region src/utils.ts
|
|
740
1165
|
async function streamToUint8Array(stream) {
|
|
741
1166
|
const reader = stream.getReader();
|
|
742
1167
|
const chunks = [];
|
|
@@ -814,12 +1239,14 @@ function asJsonObject(value) {
|
|
|
814
1239
|
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
815
1240
|
}
|
|
816
1241
|
function isJsonObjectValue(value) {
|
|
817
|
-
return typeof value === "object" &&
|
|
1242
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
818
1243
|
}
|
|
819
1244
|
function stableStringify(value) {
|
|
820
|
-
if (value === null
|
|
1245
|
+
if (value === null) return "null";
|
|
1246
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1247
|
+
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
821
1248
|
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
822
|
-
if (!isJsonObjectValue(value)) return
|
|
1249
|
+
if (!isJsonObjectValue(value)) return "null";
|
|
823
1250
|
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
824
1251
|
}
|
|
825
1252
|
function jsonEquals(a, b) {
|
|
@@ -829,15 +1256,17 @@ function jsonEquals(a, b) {
|
|
|
829
1256
|
}
|
|
830
1257
|
function diffJsonObjects(previous, next) {
|
|
831
1258
|
const patch = {};
|
|
832
|
-
const keys =
|
|
1259
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1260
|
+
if (previous) for (const key of Object.keys(previous)) keys.add(key);
|
|
1261
|
+
for (const key of Object.keys(next)) keys.add(key);
|
|
833
1262
|
for (const key of keys) {
|
|
834
1263
|
const prevValue = previous ? previous[key] : void 0;
|
|
835
|
-
if (!Object.prototype.hasOwnProperty.call(next, key)) {
|
|
836
|
-
patch[key] = null;
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
1264
|
const nextValue = next[key];
|
|
840
1265
|
if (!jsonEquals(prevValue, nextValue)) {
|
|
1266
|
+
if (nextValue === void 0 && previous && key in previous) {
|
|
1267
|
+
patch[key] = null;
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
841
1270
|
const cloned = cloneJsonValue(nextValue);
|
|
842
1271
|
if (cloned !== void 0) patch[key] = cloned;
|
|
843
1272
|
}
|
|
@@ -893,61 +1322,24 @@ function toReadableStream(bytes) {
|
|
|
893
1322
|
controller.close();
|
|
894
1323
|
} });
|
|
895
1324
|
}
|
|
896
|
-
function
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
return
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
}
|
|
905
|
-
function emptyFrontiers() {
|
|
906
|
-
return [];
|
|
907
|
-
}
|
|
908
|
-
function getDocFrontiers(doc) {
|
|
909
|
-
const candidate = doc;
|
|
910
|
-
if (typeof candidate.frontiers === "function") {
|
|
911
|
-
const result = candidate.frontiers();
|
|
912
|
-
if (result) return result;
|
|
913
|
-
}
|
|
914
|
-
return emptyFrontiers();
|
|
915
|
-
}
|
|
916
|
-
function versionVectorToJson(vv) {
|
|
917
|
-
const map = vv.toJSON();
|
|
918
|
-
const record = {};
|
|
919
|
-
if (map instanceof Map) {
|
|
920
|
-
const entries = Array.from(map.entries()).sort(([a], [b]) => String(a).localeCompare(String(b)));
|
|
921
|
-
for (const [peer, counter] of entries) {
|
|
922
|
-
if (typeof counter !== "number" || !Number.isFinite(counter)) continue;
|
|
923
|
-
const key = typeof peer === "string" ? peer : JSON.stringify(peer);
|
|
924
|
-
record[key] = counter;
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
return record;
|
|
928
|
-
}
|
|
929
|
-
function canonicalizeVersionVector(vv) {
|
|
930
|
-
const json = versionVectorToJson(vv);
|
|
1325
|
+
function canonicalizeFrontiers(frontiers) {
|
|
1326
|
+
const json = [...frontiers].sort((a, b) => {
|
|
1327
|
+
if (a.peer < b.peer) return -1;
|
|
1328
|
+
if (a.peer > b.peer) return 1;
|
|
1329
|
+
return a.counter - b.counter;
|
|
1330
|
+
}).map((f) => ({
|
|
1331
|
+
peer: f.peer,
|
|
1332
|
+
counter: f.counter
|
|
1333
|
+
}));
|
|
931
1334
|
return {
|
|
932
1335
|
json,
|
|
933
1336
|
key: stableStringify(json)
|
|
934
1337
|
};
|
|
935
1338
|
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
onClose;
|
|
941
|
-
constructor(docId, doc, whenSyncedWithRemote, onClose) {
|
|
942
|
-
this.docId = docId;
|
|
943
|
-
this.doc = doc;
|
|
944
|
-
this.whenSyncedWithRemote = whenSyncedWithRemote;
|
|
945
|
-
this.onClose = onClose;
|
|
946
|
-
}
|
|
947
|
-
async close() {
|
|
948
|
-
await this.onClose(this.docId, this.doc);
|
|
949
|
-
}
|
|
950
|
-
};
|
|
1339
|
+
function includesFrontiers(vv, frontiers) {
|
|
1340
|
+
for (const { peer, counter } of frontiers) if ((vv.get(peer) ?? 0) <= counter) return false;
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
951
1343
|
function matchesQuery(docId, _metadata, query) {
|
|
952
1344
|
if (!query) return true;
|
|
953
1345
|
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
@@ -955,345 +1347,372 @@ function matchesQuery(docId, _metadata, query) {
|
|
|
955
1347
|
if (query.end && docId > query.end) return false;
|
|
956
1348
|
return true;
|
|
957
1349
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1350
|
+
|
|
1351
|
+
//#endregion
|
|
1352
|
+
//#region src/internal/logging.ts
|
|
1353
|
+
function logAsyncError(context) {
|
|
1354
|
+
return (error) => {
|
|
1355
|
+
if (error instanceof Error) console.error(`[loro-repo] ${context} failed: ${error.message}`, error);
|
|
1356
|
+
else console.error(`[loro-repo] ${context} failed with non-error reason:`, error);
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
//#endregion
|
|
1361
|
+
//#region src/internal/doc-manager.ts
|
|
1362
|
+
var DocManager = class {
|
|
961
1363
|
storage;
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1364
|
+
docFrontierDebounceMs;
|
|
1365
|
+
getMetaFlock;
|
|
1366
|
+
eventBus;
|
|
1367
|
+
persistMeta;
|
|
1368
|
+
state;
|
|
966
1369
|
docs = /* @__PURE__ */ new Map();
|
|
967
|
-
docRefs = /* @__PURE__ */ new Map();
|
|
968
1370
|
docSubscriptions = /* @__PURE__ */ new Map();
|
|
969
|
-
docAssets = /* @__PURE__ */ new Map();
|
|
970
|
-
assets = /* @__PURE__ */ new Map();
|
|
971
|
-
orphanedAssets = /* @__PURE__ */ new Map();
|
|
972
|
-
assetToDocRefs = /* @__PURE__ */ new Map();
|
|
973
|
-
docFrontierKeys = /* @__PURE__ */ new Map();
|
|
974
1371
|
docFrontierUpdates = /* @__PURE__ */ new Map();
|
|
975
1372
|
docPersistedVersions = /* @__PURE__ */ new Map();
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
metaRoomSubscription;
|
|
980
|
-
unsubscribeMetaFlock;
|
|
981
|
-
readyPromise;
|
|
1373
|
+
get docFrontierKeys() {
|
|
1374
|
+
return this.state.docFrontierKeys;
|
|
1375
|
+
}
|
|
982
1376
|
constructor(options) {
|
|
983
|
-
this.
|
|
984
|
-
this.
|
|
985
|
-
this.
|
|
986
|
-
this.
|
|
987
|
-
this.
|
|
988
|
-
|
|
989
|
-
this.docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
|
|
1377
|
+
this.storage = options.storage;
|
|
1378
|
+
this.docFrontierDebounceMs = options.docFrontierDebounceMs;
|
|
1379
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
1380
|
+
this.eventBus = options.eventBus;
|
|
1381
|
+
this.persistMeta = options.persistMeta;
|
|
1382
|
+
this.state = options.state;
|
|
990
1383
|
}
|
|
991
|
-
async
|
|
992
|
-
|
|
993
|
-
await this.readyPromise;
|
|
1384
|
+
async openCollaborativeDoc(docId) {
|
|
1385
|
+
return await this.ensureDoc(docId);
|
|
994
1386
|
}
|
|
995
|
-
async
|
|
1387
|
+
async openDetachedDoc(docId) {
|
|
1388
|
+
return await this.materializeDetachedDoc(docId);
|
|
1389
|
+
}
|
|
1390
|
+
async ensureDoc(docId) {
|
|
1391
|
+
const cached = this.docs.get(docId);
|
|
1392
|
+
if (cached) {
|
|
1393
|
+
this.ensureDocSubscription(docId, cached);
|
|
1394
|
+
if (!this.docPersistedVersions.has(docId)) this.docPersistedVersions.set(docId, cached.version());
|
|
1395
|
+
return cached;
|
|
1396
|
+
}
|
|
996
1397
|
if (this.storage) {
|
|
997
|
-
const
|
|
998
|
-
if (
|
|
1398
|
+
const stored = await this.storage.loadDoc(docId);
|
|
1399
|
+
if (stored) {
|
|
1400
|
+
this.registerDoc(docId, stored);
|
|
1401
|
+
return stored;
|
|
1402
|
+
}
|
|
999
1403
|
}
|
|
1000
|
-
|
|
1404
|
+
const created = new loro_crdt.LoroDoc();
|
|
1405
|
+
this.registerDoc(docId, created);
|
|
1406
|
+
return created;
|
|
1001
1407
|
}
|
|
1002
|
-
async
|
|
1003
|
-
|
|
1004
|
-
const {
|
|
1005
|
-
|
|
1006
|
-
if (!this.
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const recordedEvents = [];
|
|
1010
|
-
const unsubscribe = this.metaFlock.subscribe((batch) => {
|
|
1011
|
-
if (batch.source === "local") return;
|
|
1012
|
-
recordedEvents.push(...batch.events);
|
|
1013
|
-
});
|
|
1014
|
-
try {
|
|
1015
|
-
if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
|
|
1016
|
-
if (recordedEvents.length > 0) this.applyMetaFlockEvents(recordedEvents, "sync");
|
|
1017
|
-
else this.hydrateMetadataFromFlock("sync");
|
|
1018
|
-
await this.persistMeta();
|
|
1019
|
-
} finally {
|
|
1020
|
-
unsubscribe();
|
|
1021
|
-
this.popEventBy();
|
|
1022
|
-
}
|
|
1408
|
+
async persistDoc(docId, doc) {
|
|
1409
|
+
const previousVersion = this.docPersistedVersions.get(docId);
|
|
1410
|
+
const snapshot = doc.export({ mode: "snapshot" });
|
|
1411
|
+
const nextVersion = doc.version();
|
|
1412
|
+
if (!this.storage) {
|
|
1413
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1414
|
+
return;
|
|
1023
1415
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
await this.updateDocFrontiers(docId, doc, "sync");
|
|
1036
|
-
}
|
|
1416
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1417
|
+
try {
|
|
1418
|
+
await this.storage.save({
|
|
1419
|
+
type: "doc-snapshot",
|
|
1420
|
+
docId,
|
|
1421
|
+
snapshot
|
|
1422
|
+
});
|
|
1423
|
+
} catch (error) {
|
|
1424
|
+
if (previousVersion) this.docPersistedVersions.set(docId, previousVersion);
|
|
1425
|
+
else this.docPersistedVersions.delete(docId);
|
|
1426
|
+
throw error;
|
|
1037
1427
|
}
|
|
1038
1428
|
}
|
|
1039
|
-
|
|
1040
|
-
const
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1429
|
+
async updateDocFrontiers(docId, doc, defaultBy) {
|
|
1430
|
+
const frontiers = doc.oplogFrontiers();
|
|
1431
|
+
const { json, key } = canonicalizeFrontiers(frontiers);
|
|
1432
|
+
const existingKeys = this.docFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1433
|
+
let mutated = false;
|
|
1434
|
+
const metaFlock = this.metaFlock;
|
|
1435
|
+
const vv = doc.version();
|
|
1436
|
+
for (const entry of existingKeys) {
|
|
1437
|
+
if (entry === key) continue;
|
|
1438
|
+
let oldFrontiers;
|
|
1439
|
+
try {
|
|
1440
|
+
oldFrontiers = JSON.parse(entry);
|
|
1441
|
+
} catch {
|
|
1442
|
+
continue;
|
|
1051
1443
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
patch,
|
|
1060
|
-
by
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
refreshDocAssetsEntry(docId, by) {
|
|
1064
|
-
const mapping = this.readDocAssetsFromFlock(docId);
|
|
1065
|
-
const previous = this.docAssets.get(docId);
|
|
1066
|
-
if (!mapping.size) {
|
|
1067
|
-
if (previous?.size) {
|
|
1068
|
-
this.docAssets.delete(docId);
|
|
1069
|
-
for (const assetId of previous.keys()) {
|
|
1070
|
-
this.emit({
|
|
1071
|
-
kind: "asset-unlink",
|
|
1072
|
-
docId,
|
|
1073
|
-
assetId,
|
|
1074
|
-
by
|
|
1075
|
-
});
|
|
1076
|
-
if (!Array.from(this.docAssets.values()).some((assets) => assets.has(assetId))) {
|
|
1077
|
-
const record = this.assets.get(assetId);
|
|
1078
|
-
if (record) {
|
|
1079
|
-
const deletedAt = this.orphanedAssets.get(assetId)?.deletedAt ?? Date.now();
|
|
1080
|
-
this.orphanedAssets.set(assetId, {
|
|
1081
|
-
metadata: record.metadata,
|
|
1082
|
-
deletedAt
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1444
|
+
if (includesFrontiers(vv, oldFrontiers)) {
|
|
1445
|
+
metaFlock.delete([
|
|
1446
|
+
"f",
|
|
1447
|
+
docId,
|
|
1448
|
+
entry
|
|
1449
|
+
]);
|
|
1450
|
+
mutated = true;
|
|
1087
1451
|
}
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
this.docAssets.set(docId, mapping);
|
|
1091
|
-
const added = [];
|
|
1092
|
-
const removed = [];
|
|
1093
|
-
if (previous) {
|
|
1094
|
-
for (const assetId of previous.keys()) if (!mapping.has(assetId)) removed.push(assetId);
|
|
1095
1452
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
kind: "asset-unlink",
|
|
1453
|
+
if (!existingKeys.has(key)) {
|
|
1454
|
+
metaFlock.put([
|
|
1455
|
+
"f",
|
|
1100
1456
|
docId,
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
if (!Array.from(this.docAssets.values()).some((assets) => assets.has(assetId))) {
|
|
1105
|
-
const record = this.assets.get(assetId);
|
|
1106
|
-
if (record) {
|
|
1107
|
-
const deletedAt = this.orphanedAssets.get(assetId)?.deletedAt ?? Date.now();
|
|
1108
|
-
this.orphanedAssets.set(assetId, {
|
|
1109
|
-
metadata: record.metadata,
|
|
1110
|
-
deletedAt
|
|
1111
|
-
});
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1457
|
+
key
|
|
1458
|
+
], json);
|
|
1459
|
+
mutated = true;
|
|
1114
1460
|
}
|
|
1115
|
-
|
|
1116
|
-
|
|
1461
|
+
if (mutated) {
|
|
1462
|
+
this.refreshDocFrontierKeys(docId);
|
|
1463
|
+
await this.persistMeta();
|
|
1464
|
+
}
|
|
1465
|
+
const by = this.eventBus.resolveEventBy(defaultBy);
|
|
1466
|
+
this.eventBus.emit({
|
|
1467
|
+
kind: "doc-frontiers",
|
|
1117
1468
|
docId,
|
|
1118
|
-
|
|
1469
|
+
frontiers,
|
|
1119
1470
|
by
|
|
1120
1471
|
});
|
|
1121
1472
|
}
|
|
1122
|
-
|
|
1123
|
-
const
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1473
|
+
async flushScheduledDocFrontierUpdate(docId) {
|
|
1474
|
+
const pending = this.docFrontierUpdates.get(docId);
|
|
1475
|
+
if (!pending) return false;
|
|
1476
|
+
clearTimeout(pending.timeout);
|
|
1477
|
+
this.docFrontierUpdates.delete(docId);
|
|
1478
|
+
this.eventBus.pushEventBy(pending.by);
|
|
1479
|
+
try {
|
|
1480
|
+
await this.updateDocFrontiers(docId, pending.doc, pending.by);
|
|
1481
|
+
} finally {
|
|
1482
|
+
this.eventBus.popEventBy();
|
|
1128
1483
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1484
|
+
return true;
|
|
1485
|
+
}
|
|
1486
|
+
async unloadDoc(docId) {
|
|
1487
|
+
const doc = this.docs.get(docId);
|
|
1488
|
+
if (!doc) return;
|
|
1489
|
+
await this.flushScheduledDocFrontierUpdate(docId);
|
|
1490
|
+
await this.persistDocUpdate(docId, doc);
|
|
1491
|
+
await this.updateDocFrontiers(docId, doc, "local");
|
|
1492
|
+
this.docSubscriptions.get(docId)?.();
|
|
1493
|
+
this.docSubscriptions.delete(docId);
|
|
1494
|
+
this.docs.delete(docId);
|
|
1495
|
+
this.docPersistedVersions.delete(docId);
|
|
1496
|
+
}
|
|
1497
|
+
async flush() {
|
|
1498
|
+
const promises = [];
|
|
1499
|
+
for (const [docId, doc] of this.docs) promises.push((async () => {
|
|
1500
|
+
await this.persistDocUpdate(docId, doc);
|
|
1501
|
+
await this.flushScheduledDocFrontierUpdate(docId);
|
|
1502
|
+
})());
|
|
1503
|
+
await Promise.all(promises);
|
|
1504
|
+
}
|
|
1505
|
+
async close() {
|
|
1506
|
+
await this.flush();
|
|
1507
|
+
for (const unsubscribe of this.docSubscriptions.values()) try {
|
|
1508
|
+
unsubscribe();
|
|
1509
|
+
} catch {}
|
|
1510
|
+
this.docSubscriptions.clear();
|
|
1511
|
+
this.docFrontierUpdates.clear();
|
|
1512
|
+
this.docs.clear();
|
|
1513
|
+
this.docPersistedVersions.clear();
|
|
1514
|
+
this.docFrontierKeys.clear();
|
|
1515
|
+
}
|
|
1516
|
+
hydrateFrontierKeys() {
|
|
1517
|
+
const nextFrontierKeys = /* @__PURE__ */ new Map();
|
|
1518
|
+
const frontierRows = this.metaFlock.scan({ prefix: ["f"] });
|
|
1519
|
+
for (const row of frontierRows) {
|
|
1520
|
+
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1521
|
+
const docId = row.key[1];
|
|
1522
|
+
const frontierKey = row.key[2];
|
|
1523
|
+
if (typeof docId !== "string" || typeof frontierKey !== "string") continue;
|
|
1524
|
+
const set = nextFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1525
|
+
set.add(frontierKey);
|
|
1526
|
+
nextFrontierKeys.set(docId, set);
|
|
1527
|
+
}
|
|
1528
|
+
this.docFrontierKeys.clear();
|
|
1529
|
+
for (const [docId, keys] of nextFrontierKeys) this.docFrontierKeys.set(docId, keys);
|
|
1137
1530
|
}
|
|
1138
1531
|
refreshDocFrontierKeys(docId) {
|
|
1139
1532
|
const rows = this.metaFlock.scan({ prefix: ["f", docId] });
|
|
1140
1533
|
const keys = /* @__PURE__ */ new Set();
|
|
1141
1534
|
for (const row of rows) {
|
|
1142
1535
|
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1536
|
+
if (row.value === void 0 || row.value === null) continue;
|
|
1143
1537
|
const frontierKey = row.key[2];
|
|
1144
1538
|
if (typeof frontierKey === "string") keys.add(frontierKey);
|
|
1145
1539
|
}
|
|
1146
1540
|
if (keys.size > 0) this.docFrontierKeys.set(docId, keys);
|
|
1147
1541
|
else this.docFrontierKeys.delete(docId);
|
|
1148
1542
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
if (!rows.length) return void 0;
|
|
1152
|
-
const docMeta = {};
|
|
1153
|
-
let populated = false;
|
|
1154
|
-
for (const row of rows) {
|
|
1155
|
-
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1156
|
-
if (row.key.length === 2) {
|
|
1157
|
-
const obj = asJsonObject(row.value);
|
|
1158
|
-
if (!obj) continue;
|
|
1159
|
-
for (const [field, value] of Object.entries(obj)) {
|
|
1160
|
-
const cloned = cloneJsonValue(value);
|
|
1161
|
-
if (cloned !== void 0) {
|
|
1162
|
-
docMeta[field] = cloned;
|
|
1163
|
-
populated = true;
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
continue;
|
|
1167
|
-
}
|
|
1168
|
-
const fieldKey = row.key[2];
|
|
1169
|
-
if (typeof fieldKey !== "string") continue;
|
|
1170
|
-
if (fieldKey === "$tombstone") {
|
|
1171
|
-
docMeta.tombstone = Boolean(row.value);
|
|
1172
|
-
populated = true;
|
|
1173
|
-
continue;
|
|
1174
|
-
}
|
|
1175
|
-
const jsonValue = cloneJsonValue(row.value);
|
|
1176
|
-
if (jsonValue === void 0) continue;
|
|
1177
|
-
docMeta[fieldKey] = jsonValue;
|
|
1178
|
-
populated = true;
|
|
1179
|
-
}
|
|
1180
|
-
return populated ? docMeta : void 0;
|
|
1181
|
-
}
|
|
1182
|
-
readDocAssetsFromFlock(docId) {
|
|
1183
|
-
const rows = this.metaFlock.scan({ prefix: ["ld", docId] });
|
|
1184
|
-
const mapping = /* @__PURE__ */ new Map();
|
|
1185
|
-
for (const row of rows) {
|
|
1186
|
-
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1187
|
-
const assetId = row.key[2];
|
|
1188
|
-
if (typeof assetId !== "string") continue;
|
|
1189
|
-
if (!(row.value !== void 0 && row.value !== null && row.value !== false)) continue;
|
|
1190
|
-
let metadata = this.assets.get(assetId)?.metadata;
|
|
1191
|
-
if (!metadata) {
|
|
1192
|
-
metadata = this.readAssetMetadataFromFlock(assetId);
|
|
1193
|
-
if (!metadata) continue;
|
|
1194
|
-
this.rememberAsset(metadata);
|
|
1195
|
-
}
|
|
1196
|
-
mapping.set(assetId, cloneRepoAssetMetadata(metadata));
|
|
1197
|
-
}
|
|
1198
|
-
return mapping;
|
|
1543
|
+
get metaFlock() {
|
|
1544
|
+
return this.getMetaFlock();
|
|
1199
1545
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1546
|
+
registerDoc(docId, doc) {
|
|
1547
|
+
this.docs.set(docId, doc);
|
|
1548
|
+
this.docPersistedVersions.set(docId, doc.version());
|
|
1549
|
+
this.ensureDocSubscription(docId, doc);
|
|
1202
1550
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
metadata: record.metadata,
|
|
1210
|
-
deletedAt
|
|
1551
|
+
ensureDocSubscription(docId, doc) {
|
|
1552
|
+
if (this.docSubscriptions.has(docId)) return;
|
|
1553
|
+
const unsubscribe = doc.subscribe((batch) => {
|
|
1554
|
+
const stackBy = this.eventBus.resolveEventBy("local");
|
|
1555
|
+
const by = stackBy === "local" && batch.by === "import" ? "live" : stackBy;
|
|
1556
|
+
this.onDocEvent(docId, doc, batch, by);
|
|
1211
1557
|
});
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1558
|
+
if (typeof unsubscribe === "function") this.docSubscriptions.set(docId, unsubscribe);
|
|
1559
|
+
}
|
|
1560
|
+
scheduleDocFrontierUpdate(docId, doc, by) {
|
|
1561
|
+
const existing = this.docFrontierUpdates.get(docId);
|
|
1562
|
+
const effectiveBy = existing ? this.mergeRepoEventBy(existing.by, by) : by;
|
|
1563
|
+
if (existing) clearTimeout(existing.timeout);
|
|
1564
|
+
const delay = this.docFrontierDebounceMs > 0 ? this.docFrontierDebounceMs : 0;
|
|
1565
|
+
const timeout = setTimeout(() => this.runScheduledDocFrontierUpdate(docId), delay);
|
|
1566
|
+
this.docFrontierUpdates.set(docId, {
|
|
1567
|
+
timeout,
|
|
1568
|
+
doc,
|
|
1569
|
+
by: effectiveBy
|
|
1222
1570
|
});
|
|
1223
1571
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1572
|
+
mergeRepoEventBy(current, next) {
|
|
1573
|
+
if (current === next) return current;
|
|
1574
|
+
if (current === "live" || next === "live") return "live";
|
|
1575
|
+
if (current === "sync" || next === "sync") return "sync";
|
|
1576
|
+
return "local";
|
|
1226
1577
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
if (!
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
if (this.metaRoomSubscription === wrapped) this.metaRoomSubscription = void 0;
|
|
1238
|
-
if (this.unsubscribeMetaFlock) {
|
|
1239
|
-
this.unsubscribeMetaFlock();
|
|
1240
|
-
this.unsubscribeMetaFlock = void 0;
|
|
1241
|
-
}
|
|
1242
|
-
},
|
|
1243
|
-
firstSyncedWithRemote: subscription.firstSyncedWithRemote,
|
|
1244
|
-
get connected() {
|
|
1245
|
-
return subscription.connected;
|
|
1578
|
+
runScheduledDocFrontierUpdate(docId) {
|
|
1579
|
+
const pending = this.docFrontierUpdates.get(docId);
|
|
1580
|
+
if (!pending) return;
|
|
1581
|
+
this.docFrontierUpdates.delete(docId);
|
|
1582
|
+
this.eventBus.pushEventBy(pending.by);
|
|
1583
|
+
(async () => {
|
|
1584
|
+
try {
|
|
1585
|
+
await this.updateDocFrontiers(docId, pending.doc, pending.by);
|
|
1586
|
+
} finally {
|
|
1587
|
+
this.eventBus.popEventBy();
|
|
1246
1588
|
}
|
|
1247
|
-
};
|
|
1248
|
-
this.metaRoomSubscription = wrapped;
|
|
1249
|
-
subscription.firstSyncedWithRemote.then(async () => {
|
|
1250
|
-
const by = this.resolveEventBy("live");
|
|
1251
|
-
this.hydrateMetadataFromFlock(by);
|
|
1252
|
-
await this.persistMeta();
|
|
1253
|
-
}).catch(logAsyncError("meta room first sync"));
|
|
1254
|
-
return wrapped;
|
|
1589
|
+
})().catch(logAsyncError(`doc ${docId} frontier debounce`));
|
|
1255
1590
|
}
|
|
1256
|
-
async
|
|
1257
|
-
await this.
|
|
1258
|
-
if (
|
|
1259
|
-
|
|
1260
|
-
const doc = await this.ensureDoc(docId);
|
|
1261
|
-
const subscription = this.transport.joinDocRoom(docId, doc, params);
|
|
1262
|
-
subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
1263
|
-
return subscription;
|
|
1591
|
+
async materializeDetachedDoc(docId) {
|
|
1592
|
+
const snapshot = await this.exportDocSnapshot(docId);
|
|
1593
|
+
if (snapshot) return loro_crdt.LoroDoc.fromSnapshot(snapshot);
|
|
1594
|
+
return new loro_crdt.LoroDoc();
|
|
1264
1595
|
}
|
|
1265
|
-
async
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
this.
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1596
|
+
async exportDocSnapshot(docId) {
|
|
1597
|
+
const cached = this.docs.get(docId);
|
|
1598
|
+
if (cached) return cached.export({ mode: "snapshot" });
|
|
1599
|
+
if (!this.storage) return;
|
|
1600
|
+
return (await this.storage.loadDoc(docId))?.export({ mode: "snapshot" });
|
|
1601
|
+
}
|
|
1602
|
+
async persistDocUpdate(docId, doc) {
|
|
1603
|
+
const previousVersion = this.docPersistedVersions.get(docId);
|
|
1604
|
+
const nextVersion = doc.oplogVersion();
|
|
1605
|
+
if (!this.storage) {
|
|
1606
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1607
|
+
return;
|
|
1275
1608
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1609
|
+
if (!previousVersion) {
|
|
1610
|
+
await this.persistDoc(docId, doc);
|
|
1611
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
if (previousVersion.compare(nextVersion) === 0) return;
|
|
1615
|
+
const update = doc.export({
|
|
1616
|
+
mode: "update",
|
|
1617
|
+
from: previousVersion
|
|
1618
|
+
});
|
|
1619
|
+
this.docPersistedVersions.set(docId, nextVersion);
|
|
1620
|
+
try {
|
|
1621
|
+
await this.storage.save({
|
|
1622
|
+
type: "doc-update",
|
|
1623
|
+
docId,
|
|
1624
|
+
update
|
|
1625
|
+
});
|
|
1279
1626
|
} catch (error) {
|
|
1280
|
-
|
|
1627
|
+
this.docPersistedVersions.set(docId, previousVersion);
|
|
1628
|
+
throw error;
|
|
1281
1629
|
}
|
|
1282
|
-
this.docFrontierUpdates.clear();
|
|
1283
|
-
this.watchers.clear();
|
|
1284
|
-
this.docs.clear();
|
|
1285
|
-
this.docRefs.clear();
|
|
1286
|
-
this.metadata.clear();
|
|
1287
|
-
this.docAssets.clear();
|
|
1288
|
-
this.assets.clear();
|
|
1289
|
-
this.docFrontierKeys.clear();
|
|
1290
|
-
this.docPersistedVersions.clear();
|
|
1291
|
-
this.readyPromise = void 0;
|
|
1292
|
-
await this.transport?.close();
|
|
1293
1630
|
}
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1631
|
+
onDocEvent(docId, doc, _batch, by) {
|
|
1632
|
+
(async () => {
|
|
1633
|
+
const persist = this.persistDocUpdate(docId, doc);
|
|
1634
|
+
if (by === "local") {
|
|
1635
|
+
this.scheduleDocFrontierUpdate(docId, doc, by);
|
|
1636
|
+
await persist;
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const flushed = this.flushScheduledDocFrontierUpdate(docId);
|
|
1640
|
+
const updated = (async () => {
|
|
1641
|
+
this.eventBus.pushEventBy(by);
|
|
1642
|
+
try {
|
|
1643
|
+
await this.updateDocFrontiers(docId, doc, by);
|
|
1644
|
+
} finally {
|
|
1645
|
+
this.eventBus.popEventBy();
|
|
1646
|
+
}
|
|
1647
|
+
})();
|
|
1648
|
+
await Promise.all([
|
|
1649
|
+
persist,
|
|
1650
|
+
flushed,
|
|
1651
|
+
updated
|
|
1652
|
+
]);
|
|
1653
|
+
})().catch(logAsyncError(`doc ${docId} event processing`));
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1657
|
+
//#endregion
|
|
1658
|
+
//#region src/internal/metadata-manager.ts
|
|
1659
|
+
var MetadataManager = class {
|
|
1660
|
+
getMetaFlock;
|
|
1661
|
+
eventBus;
|
|
1662
|
+
persistMeta;
|
|
1663
|
+
state;
|
|
1664
|
+
constructor(options) {
|
|
1665
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
1666
|
+
this.eventBus = options.eventBus;
|
|
1667
|
+
this.persistMeta = options.persistMeta;
|
|
1668
|
+
this.state = options.state;
|
|
1669
|
+
}
|
|
1670
|
+
getDocIds() {
|
|
1671
|
+
return Array.from(this.state.metadata.keys());
|
|
1672
|
+
}
|
|
1673
|
+
entries() {
|
|
1674
|
+
return this.state.metadata.entries();
|
|
1675
|
+
}
|
|
1676
|
+
get(docId) {
|
|
1677
|
+
const metadata = this.state.metadata.get(docId);
|
|
1678
|
+
return metadata ? cloneJsonObject(metadata) : void 0;
|
|
1679
|
+
}
|
|
1680
|
+
listDoc(query) {
|
|
1681
|
+
if (query?.limit !== void 0 && query.limit <= 0) return [];
|
|
1682
|
+
const { startKey, endKey } = this.computeDocRangeKeys(query);
|
|
1683
|
+
if (startKey && endKey && startKey >= endKey) return [];
|
|
1684
|
+
const scanOptions = { prefix: ["m"] };
|
|
1685
|
+
if (startKey) scanOptions.start = {
|
|
1686
|
+
kind: "inclusive",
|
|
1687
|
+
key: ["m", startKey]
|
|
1688
|
+
};
|
|
1689
|
+
if (endKey) scanOptions.end = {
|
|
1690
|
+
kind: "exclusive",
|
|
1691
|
+
key: ["m", endKey]
|
|
1692
|
+
};
|
|
1693
|
+
const rows = this.metaFlock.scan(scanOptions);
|
|
1694
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1695
|
+
const entries = [];
|
|
1696
|
+
for (const row of rows) {
|
|
1697
|
+
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
1698
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1699
|
+
const docId = row.key[1];
|
|
1700
|
+
if (typeof docId !== "string") continue;
|
|
1701
|
+
if (seen.has(docId)) continue;
|
|
1702
|
+
seen.add(docId);
|
|
1703
|
+
const metadata = this.state.metadata.get(docId);
|
|
1704
|
+
if (!metadata) continue;
|
|
1705
|
+
if (!matchesQuery(docId, metadata, query)) continue;
|
|
1706
|
+
entries.push({
|
|
1707
|
+
docId,
|
|
1708
|
+
meta: cloneJsonObject(metadata)
|
|
1709
|
+
});
|
|
1710
|
+
if (query?.limit !== void 0 && entries.length >= query.limit) break;
|
|
1711
|
+
}
|
|
1712
|
+
return entries;
|
|
1713
|
+
}
|
|
1714
|
+
async upsert(docId, patch) {
|
|
1715
|
+
const base = this.state.metadata.get(docId);
|
|
1297
1716
|
const next = base ? cloneJsonObject(base) : {};
|
|
1298
1717
|
const outPatch = {};
|
|
1299
1718
|
let changed = false;
|
|
@@ -1318,70 +1737,159 @@ var LoroRepo = class {
|
|
|
1318
1737
|
changed = true;
|
|
1319
1738
|
}
|
|
1320
1739
|
if (!changed) {
|
|
1321
|
-
if (!this.metadata.has(docId)) this.metadata.set(docId, next);
|
|
1740
|
+
if (!this.state.metadata.has(docId)) this.state.metadata.set(docId, next);
|
|
1322
1741
|
return;
|
|
1323
1742
|
}
|
|
1324
|
-
this.metadata.set(docId, next);
|
|
1743
|
+
this.state.metadata.set(docId, next);
|
|
1325
1744
|
await this.persistMeta();
|
|
1326
|
-
this.emit({
|
|
1745
|
+
this.eventBus.emit({
|
|
1327
1746
|
kind: "doc-metadata",
|
|
1328
1747
|
docId,
|
|
1329
1748
|
patch: cloneJsonObject(outPatch),
|
|
1330
1749
|
by: "local"
|
|
1331
1750
|
});
|
|
1332
1751
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1336
|
-
|
|
1752
|
+
refreshFromFlock(docId, by) {
|
|
1753
|
+
const previous = this.state.metadata.get(docId);
|
|
1754
|
+
const next = this.readDocMetadataFromFlock(docId);
|
|
1755
|
+
if (!next) {
|
|
1756
|
+
if (previous) {
|
|
1757
|
+
this.state.metadata.delete(docId);
|
|
1758
|
+
this.eventBus.emit({
|
|
1759
|
+
kind: "doc-metadata",
|
|
1760
|
+
docId,
|
|
1761
|
+
patch: {},
|
|
1762
|
+
by
|
|
1763
|
+
});
|
|
1764
|
+
}
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
this.state.metadata.set(docId, next);
|
|
1768
|
+
const patch = diffJsonObjects(previous, next);
|
|
1769
|
+
if (!previous || Object.keys(patch).length > 0) this.eventBus.emit({
|
|
1770
|
+
kind: "doc-metadata",
|
|
1771
|
+
docId,
|
|
1772
|
+
patch,
|
|
1773
|
+
by
|
|
1774
|
+
});
|
|
1337
1775
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
for (const [docId,
|
|
1342
|
-
|
|
1343
|
-
|
|
1776
|
+
replaceAll(nextMetadata, by) {
|
|
1777
|
+
const prevMetadata = new Map(this.state.metadata);
|
|
1778
|
+
this.state.metadata.clear();
|
|
1779
|
+
for (const [docId, meta] of nextMetadata) this.state.metadata.set(docId, meta);
|
|
1780
|
+
const docIds = new Set([...prevMetadata.keys(), ...nextMetadata.keys()]);
|
|
1781
|
+
for (const docId of docIds) {
|
|
1782
|
+
const previous = prevMetadata.get(docId);
|
|
1783
|
+
const current = nextMetadata.get(docId);
|
|
1784
|
+
if (!current) {
|
|
1785
|
+
if (previous) this.eventBus.emit({
|
|
1786
|
+
kind: "doc-metadata",
|
|
1787
|
+
docId,
|
|
1788
|
+
patch: {},
|
|
1789
|
+
by
|
|
1790
|
+
});
|
|
1791
|
+
continue;
|
|
1792
|
+
}
|
|
1793
|
+
const patch = diffJsonObjects(previous, current);
|
|
1794
|
+
if (!previous || Object.keys(patch).length > 0) this.eventBus.emit({
|
|
1795
|
+
kind: "doc-metadata",
|
|
1344
1796
|
docId,
|
|
1345
|
-
|
|
1797
|
+
patch,
|
|
1798
|
+
by
|
|
1346
1799
|
});
|
|
1347
1800
|
}
|
|
1348
|
-
entries.sort((a, b) => a.docId < b.docId ? -1 : a.docId > b.docId ? 1 : 0);
|
|
1349
|
-
if (query?.limit !== void 0) return entries.slice(0, query.limit);
|
|
1350
|
-
return entries;
|
|
1351
1801
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1802
|
+
clear() {
|
|
1803
|
+
this.state.metadata.clear();
|
|
1354
1804
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1805
|
+
computeDocRangeKeys(query) {
|
|
1806
|
+
if (!query) return {};
|
|
1807
|
+
const prefix = query.prefix && query.prefix.length > 0 ? query.prefix : void 0;
|
|
1808
|
+
let startKey = query.start;
|
|
1809
|
+
if (prefix) startKey = !startKey || prefix > startKey ? prefix : startKey;
|
|
1810
|
+
let endKey = query.end;
|
|
1811
|
+
const prefixEnd = this.nextLexicographicString(prefix);
|
|
1812
|
+
if (prefixEnd) endKey = !endKey || prefixEnd < endKey ? prefixEnd : endKey;
|
|
1813
|
+
return {
|
|
1814
|
+
startKey,
|
|
1815
|
+
endKey
|
|
1359
1816
|
};
|
|
1360
|
-
this.watchers.add(entry);
|
|
1361
|
-
return { unsubscribe: () => {
|
|
1362
|
-
this.watchers.delete(entry);
|
|
1363
|
-
} };
|
|
1364
1817
|
}
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
const refs = this.docRefs.get(docId) ?? 0;
|
|
1372
|
-
this.docRefs.set(docId, refs + 1);
|
|
1373
|
-
return new RepoDocHandleImpl(docId, doc, this.whenDocInSyncWithRemote(docId), async (id, instance) => this.onDocHandleClose(id, instance));
|
|
1818
|
+
nextLexicographicString(value) {
|
|
1819
|
+
if (!value) return void 0;
|
|
1820
|
+
for (let i = value.length - 1; i >= 0; i -= 1) {
|
|
1821
|
+
const code = value.charCodeAt(i);
|
|
1822
|
+
if (code < 65535) return `${value.slice(0, i)}${String.fromCharCode(code + 1)}`;
|
|
1823
|
+
}
|
|
1374
1824
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1825
|
+
readDocMetadataFromFlock(docId) {
|
|
1826
|
+
const rows = this.metaFlock.scan({ prefix: ["m", docId] });
|
|
1827
|
+
if (!rows.length) return void 0;
|
|
1828
|
+
const docMeta = {};
|
|
1829
|
+
let populated = false;
|
|
1830
|
+
for (const row of rows) {
|
|
1831
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
1832
|
+
if (row.key.length === 2) {
|
|
1833
|
+
const obj = asJsonObject(row.value);
|
|
1834
|
+
if (!obj) continue;
|
|
1835
|
+
for (const [field, value] of Object.entries(obj)) {
|
|
1836
|
+
const cloned = cloneJsonValue(value);
|
|
1837
|
+
if (cloned !== void 0) {
|
|
1838
|
+
docMeta[field] = cloned;
|
|
1839
|
+
populated = true;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
continue;
|
|
1843
|
+
}
|
|
1844
|
+
const fieldKey = row.key[2];
|
|
1845
|
+
if (typeof fieldKey !== "string") continue;
|
|
1846
|
+
if (fieldKey === "$tombstone") {
|
|
1847
|
+
docMeta.tombstone = Boolean(row.value);
|
|
1848
|
+
populated = true;
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
const jsonValue = cloneJsonValue(row.value);
|
|
1852
|
+
if (jsonValue === void 0) continue;
|
|
1853
|
+
docMeta[fieldKey] = jsonValue;
|
|
1854
|
+
populated = true;
|
|
1855
|
+
}
|
|
1856
|
+
return populated ? docMeta : void 0;
|
|
1857
|
+
}
|
|
1858
|
+
get metaFlock() {
|
|
1859
|
+
return this.getMetaFlock();
|
|
1860
|
+
}
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
//#endregion
|
|
1864
|
+
//#region src/internal/asset-manager.ts
|
|
1865
|
+
var AssetManager = class {
|
|
1866
|
+
storage;
|
|
1867
|
+
assetTransport;
|
|
1868
|
+
getMetaFlock;
|
|
1869
|
+
eventBus;
|
|
1870
|
+
persistMeta;
|
|
1871
|
+
state;
|
|
1872
|
+
get docAssets() {
|
|
1873
|
+
return this.state.docAssets;
|
|
1874
|
+
}
|
|
1875
|
+
get assets() {
|
|
1876
|
+
return this.state.assets;
|
|
1877
|
+
}
|
|
1878
|
+
get orphanedAssets() {
|
|
1879
|
+
return this.state.orphanedAssets;
|
|
1880
|
+
}
|
|
1881
|
+
get assetToDocRefs() {
|
|
1882
|
+
return this.state.assetToDocRefs;
|
|
1883
|
+
}
|
|
1884
|
+
constructor(options) {
|
|
1885
|
+
this.storage = options.storage;
|
|
1886
|
+
this.assetTransport = options.assetTransport;
|
|
1887
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
1888
|
+
this.eventBus = options.eventBus;
|
|
1889
|
+
this.persistMeta = options.persistMeta;
|
|
1890
|
+
this.state = options.state;
|
|
1382
1891
|
}
|
|
1383
1892
|
async uploadAsset(params) {
|
|
1384
|
-
await this.ready();
|
|
1385
1893
|
const bytes = await assetContentToUint8Array(params.content);
|
|
1386
1894
|
const assetId = await computeSha256(bytes);
|
|
1387
1895
|
if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
|
|
@@ -1418,7 +1926,7 @@ var LoroRepo = class {
|
|
|
1418
1926
|
existing.metadata = metadata$1;
|
|
1419
1927
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata$1));
|
|
1420
1928
|
await this.persistMeta();
|
|
1421
|
-
this.emit({
|
|
1929
|
+
this.eventBus.emit({
|
|
1422
1930
|
kind: "asset-metadata",
|
|
1423
1931
|
asset: this.createAssetDownload(assetId, metadata$1, existing.data),
|
|
1424
1932
|
by: "local"
|
|
@@ -1451,26 +1959,17 @@ var LoroRepo = class {
|
|
|
1451
1959
|
data: storedBytes.slice()
|
|
1452
1960
|
});
|
|
1453
1961
|
this.rememberAsset(metadata, storedBytes);
|
|
1454
|
-
|
|
1962
|
+
this.updateDocAssetMetadata(assetId, metadata);
|
|
1455
1963
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
1456
1964
|
await this.persistMeta();
|
|
1457
|
-
this.emit({
|
|
1965
|
+
this.eventBus.emit({
|
|
1458
1966
|
kind: "asset-metadata",
|
|
1459
1967
|
asset: this.createAssetDownload(assetId, metadata, storedBytes),
|
|
1460
1968
|
by: "local"
|
|
1461
1969
|
});
|
|
1462
1970
|
return assetId;
|
|
1463
1971
|
}
|
|
1464
|
-
async whenDocInSyncWithRemote(docId) {
|
|
1465
|
-
await this.ready();
|
|
1466
|
-
await this.ensureDoc(docId);
|
|
1467
|
-
await this.sync({
|
|
1468
|
-
scope: "doc",
|
|
1469
|
-
docIds: [docId]
|
|
1470
|
-
});
|
|
1471
|
-
}
|
|
1472
1972
|
async linkAsset(docId, params) {
|
|
1473
|
-
await this.ready();
|
|
1474
1973
|
const bytes = await assetContentToUint8Array(params.content);
|
|
1475
1974
|
const assetId = await computeSha256(bytes);
|
|
1476
1975
|
if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
|
|
@@ -1524,7 +2023,7 @@ var LoroRepo = class {
|
|
|
1524
2023
|
metadata = nextMetadata;
|
|
1525
2024
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
1526
2025
|
await this.persistMeta();
|
|
1527
|
-
this.emit({
|
|
2026
|
+
this.eventBus.emit({
|
|
1528
2027
|
kind: "asset-metadata",
|
|
1529
2028
|
asset: this.createAssetDownload(assetId, metadata, existing.data),
|
|
1530
2029
|
by: "local"
|
|
@@ -1557,7 +2056,7 @@ var LoroRepo = class {
|
|
|
1557
2056
|
data: storedBytes.slice()
|
|
1558
2057
|
});
|
|
1559
2058
|
this.rememberAsset(metadata, storedBytes);
|
|
1560
|
-
|
|
2059
|
+
this.updateDocAssetMetadata(assetId, metadata);
|
|
1561
2060
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
1562
2061
|
created = true;
|
|
1563
2062
|
}
|
|
@@ -1573,26 +2072,20 @@ var LoroRepo = class {
|
|
|
1573
2072
|
assetId
|
|
1574
2073
|
], true);
|
|
1575
2074
|
await this.persistMeta();
|
|
1576
|
-
this.emit({
|
|
2075
|
+
this.eventBus.emit({
|
|
1577
2076
|
kind: "asset-link",
|
|
1578
2077
|
docId,
|
|
1579
2078
|
assetId,
|
|
1580
2079
|
by: "local"
|
|
1581
2080
|
});
|
|
1582
|
-
if (created) this.emit({
|
|
2081
|
+
if (created) this.eventBus.emit({
|
|
1583
2082
|
kind: "asset-metadata",
|
|
1584
2083
|
asset: this.createAssetDownload(assetId, metadata, storedBytes ?? bytes),
|
|
1585
2084
|
by: "local"
|
|
1586
2085
|
});
|
|
1587
2086
|
return assetId;
|
|
1588
2087
|
}
|
|
1589
|
-
async fetchAsset(assetId) {
|
|
1590
|
-
await this.ready();
|
|
1591
|
-
const { metadata, bytes } = await this.materializeAsset(assetId);
|
|
1592
|
-
return this.createAssetDownload(assetId, metadata, bytes);
|
|
1593
|
-
}
|
|
1594
2088
|
async unlinkAsset(docId, assetId) {
|
|
1595
|
-
await this.ready();
|
|
1596
2089
|
const mapping = this.docAssets.get(docId);
|
|
1597
2090
|
if (!mapping || !mapping.has(assetId)) return;
|
|
1598
2091
|
mapping.delete(assetId);
|
|
@@ -1617,7 +2110,7 @@ var LoroRepo = class {
|
|
|
1617
2110
|
}
|
|
1618
2111
|
}
|
|
1619
2112
|
await this.persistMeta();
|
|
1620
|
-
this.emit({
|
|
2113
|
+
this.eventBus.emit({
|
|
1621
2114
|
kind: "asset-unlink",
|
|
1622
2115
|
docId,
|
|
1623
2116
|
assetId,
|
|
@@ -1625,7 +2118,6 @@ var LoroRepo = class {
|
|
|
1625
2118
|
});
|
|
1626
2119
|
}
|
|
1627
2120
|
async listAssets(docId) {
|
|
1628
|
-
await this.ready();
|
|
1629
2121
|
const mapping = this.docAssets.get(docId);
|
|
1630
2122
|
if (!mapping) return [];
|
|
1631
2123
|
return Array.from(mapping.values()).map((asset) => ({ ...asset }));
|
|
@@ -1633,21 +2125,236 @@ var LoroRepo = class {
|
|
|
1633
2125
|
async ensureAsset(assetId) {
|
|
1634
2126
|
return this.fetchAsset(assetId);
|
|
1635
2127
|
}
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
return
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
2128
|
+
async fetchAsset(assetId) {
|
|
2129
|
+
const { metadata, bytes } = await this.materializeAsset(assetId);
|
|
2130
|
+
return this.createAssetDownload(assetId, metadata, bytes);
|
|
2131
|
+
}
|
|
2132
|
+
async gcAssets(options = {}) {
|
|
2133
|
+
const { minKeepMs = 0 } = options;
|
|
2134
|
+
const now = Date.now();
|
|
2135
|
+
let removed = 0;
|
|
2136
|
+
for (const [assetId, orphan] of Array.from(this.orphanedAssets.entries())) {
|
|
2137
|
+
if (now - orphan.deletedAt < minKeepMs) continue;
|
|
2138
|
+
this.orphanedAssets.delete(assetId);
|
|
2139
|
+
if (this.storage?.deleteAsset) try {
|
|
2140
|
+
await this.storage.deleteAsset(assetId);
|
|
2141
|
+
} catch (error) {
|
|
2142
|
+
logAsyncError(`asset ${assetId} delete`)(error);
|
|
2143
|
+
}
|
|
2144
|
+
removed += 1;
|
|
2145
|
+
}
|
|
2146
|
+
return removed;
|
|
2147
|
+
}
|
|
2148
|
+
refreshDocAssetsEntry(docId, by) {
|
|
2149
|
+
const mapping = this.readDocAssetsFromFlock(docId);
|
|
2150
|
+
const previous = this.docAssets.get(docId);
|
|
2151
|
+
if (!mapping.size) {
|
|
2152
|
+
if (previous?.size) {
|
|
2153
|
+
this.docAssets.delete(docId);
|
|
2154
|
+
for (const assetId of previous.keys()) {
|
|
2155
|
+
this.removeDocAssetReference(assetId, docId);
|
|
2156
|
+
this.eventBus.emit({
|
|
2157
|
+
kind: "asset-unlink",
|
|
2158
|
+
docId,
|
|
2159
|
+
assetId,
|
|
2160
|
+
by
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
this.docAssets.set(docId, mapping);
|
|
2167
|
+
const removed = [];
|
|
2168
|
+
if (previous) {
|
|
2169
|
+
for (const assetId of previous.keys()) if (!mapping.has(assetId)) removed.push(assetId);
|
|
2170
|
+
}
|
|
2171
|
+
for (const assetId of removed) {
|
|
2172
|
+
this.removeDocAssetReference(assetId, docId);
|
|
2173
|
+
this.eventBus.emit({
|
|
2174
|
+
kind: "asset-unlink",
|
|
2175
|
+
docId,
|
|
2176
|
+
assetId,
|
|
2177
|
+
by
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
for (const assetId of mapping.keys()) {
|
|
2181
|
+
const isNew = !previous || !previous.has(assetId);
|
|
2182
|
+
this.addDocReference(assetId, docId);
|
|
2183
|
+
if (isNew) this.eventBus.emit({
|
|
2184
|
+
kind: "asset-link",
|
|
2185
|
+
docId,
|
|
2186
|
+
assetId,
|
|
2187
|
+
by
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
refreshAssetMetadataEntry(assetId, by) {
|
|
2192
|
+
const previous = this.assets.get(assetId);
|
|
2193
|
+
const metadata = assetMetaFromJson(this.metaFlock.get(["a", assetId]));
|
|
2194
|
+
if (!metadata) {
|
|
2195
|
+
this.handleAssetRemoval(assetId, by);
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
const existingData = previous?.data;
|
|
2199
|
+
this.rememberAsset(metadata, existingData);
|
|
2200
|
+
this.updateDocAssetMetadata(assetId, cloneRepoAssetMetadata(metadata));
|
|
2201
|
+
if (!previous || !assetMetadataEqual(previous.metadata, metadata)) this.eventBus.emit({
|
|
2202
|
+
kind: "asset-metadata",
|
|
2203
|
+
asset: this.createAssetDownload(assetId, metadata, existingData),
|
|
2204
|
+
by
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
hydrateFromFlock(by) {
|
|
2208
|
+
const prevDocAssets = new Map(this.docAssets);
|
|
2209
|
+
const prevAssets = new Map(this.assets);
|
|
2210
|
+
const nextAssets = /* @__PURE__ */ new Map();
|
|
2211
|
+
const assetRows = this.metaFlock.scan({ prefix: ["a"] });
|
|
2212
|
+
for (const row of assetRows) {
|
|
2213
|
+
if (!Array.isArray(row.key) || row.key.length < 2) continue;
|
|
2214
|
+
const assetId = row.key[1];
|
|
2215
|
+
if (typeof assetId !== "string") continue;
|
|
2216
|
+
const metadata = assetMetaFromJson(row.value);
|
|
2217
|
+
if (!metadata) continue;
|
|
2218
|
+
const existing = this.assets.get(assetId);
|
|
2219
|
+
nextAssets.set(assetId, {
|
|
2220
|
+
metadata,
|
|
2221
|
+
data: existing?.data
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
const nextDocAssets = /* @__PURE__ */ new Map();
|
|
2225
|
+
const linkRows = this.metaFlock.scan({ prefix: ["ld"] });
|
|
2226
|
+
for (const row of linkRows) {
|
|
2227
|
+
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
2228
|
+
const docId = row.key[1];
|
|
2229
|
+
const assetId = row.key[2];
|
|
2230
|
+
if (typeof docId !== "string" || typeof assetId !== "string") continue;
|
|
2231
|
+
const metadata = nextAssets.get(assetId)?.metadata;
|
|
2232
|
+
if (!metadata) continue;
|
|
2233
|
+
const mapping = nextDocAssets.get(docId) ?? /* @__PURE__ */ new Map();
|
|
2234
|
+
mapping.set(assetId, metadata);
|
|
2235
|
+
nextDocAssets.set(docId, mapping);
|
|
2236
|
+
}
|
|
2237
|
+
const removedAssets = [];
|
|
2238
|
+
for (const [assetId, record] of prevAssets) if (!nextAssets.has(assetId)) removedAssets.push([assetId, record]);
|
|
2239
|
+
if (removedAssets.length > 0) {
|
|
2240
|
+
const now = Date.now();
|
|
2241
|
+
for (const [assetId, record] of removedAssets) {
|
|
2242
|
+
const deletedAt = this.orphanedAssets.get(assetId)?.deletedAt ?? now;
|
|
2243
|
+
this.orphanedAssets.set(assetId, {
|
|
2244
|
+
metadata: record.metadata,
|
|
2245
|
+
deletedAt
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
this.docAssets.clear();
|
|
2250
|
+
for (const [docId, assets] of nextDocAssets) this.docAssets.set(docId, assets);
|
|
2251
|
+
this.assetToDocRefs.clear();
|
|
2252
|
+
for (const [docId, assets] of nextDocAssets) for (const assetId of assets.keys()) {
|
|
2253
|
+
const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
|
|
2254
|
+
refs.add(docId);
|
|
2255
|
+
this.assetToDocRefs.set(assetId, refs);
|
|
2256
|
+
}
|
|
2257
|
+
this.assets.clear();
|
|
2258
|
+
for (const record of nextAssets.values()) this.rememberAsset(record.metadata, record.data);
|
|
2259
|
+
for (const [assetId, record] of nextAssets) {
|
|
2260
|
+
const previous = prevAssets.get(assetId)?.metadata;
|
|
2261
|
+
if (!assetMetadataEqual(previous, record.metadata)) this.eventBus.emit({
|
|
2262
|
+
kind: "asset-metadata",
|
|
2263
|
+
asset: this.createAssetDownload(assetId, record.metadata, record.data),
|
|
2264
|
+
by
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
for (const [docId, assets] of nextDocAssets) {
|
|
2268
|
+
const previous = prevDocAssets.get(docId);
|
|
2269
|
+
for (const assetId of assets.keys()) if (!previous || !previous.has(assetId)) this.eventBus.emit({
|
|
2270
|
+
kind: "asset-link",
|
|
2271
|
+
docId,
|
|
2272
|
+
assetId,
|
|
2273
|
+
by
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
for (const [docId, assets] of prevDocAssets) {
|
|
2277
|
+
const current = nextDocAssets.get(docId);
|
|
2278
|
+
for (const assetId of assets.keys()) if (!current || !current.has(assetId)) this.eventBus.emit({
|
|
2279
|
+
kind: "asset-unlink",
|
|
2280
|
+
docId,
|
|
2281
|
+
assetId,
|
|
2282
|
+
by
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
clear() {
|
|
2287
|
+
this.docAssets.clear();
|
|
2288
|
+
this.assets.clear();
|
|
2289
|
+
this.orphanedAssets.clear();
|
|
2290
|
+
this.assetToDocRefs.clear();
|
|
2291
|
+
}
|
|
2292
|
+
readDocAssetsFromFlock(docId) {
|
|
2293
|
+
const rows = this.metaFlock.scan({ prefix: ["ld", docId] });
|
|
2294
|
+
const mapping = /* @__PURE__ */ new Map();
|
|
2295
|
+
for (const row of rows) {
|
|
2296
|
+
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
2297
|
+
const assetId = row.key[2];
|
|
2298
|
+
if (typeof assetId !== "string") continue;
|
|
2299
|
+
if (!(row.value !== void 0 && row.value !== null && row.value !== false)) continue;
|
|
2300
|
+
let metadata = this.assets.get(assetId)?.metadata;
|
|
2301
|
+
if (!metadata) {
|
|
2302
|
+
metadata = this.readAssetMetadataFromFlock(assetId);
|
|
2303
|
+
if (!metadata) continue;
|
|
2304
|
+
this.rememberAsset(metadata);
|
|
2305
|
+
}
|
|
2306
|
+
mapping.set(assetId, cloneRepoAssetMetadata(metadata));
|
|
2307
|
+
}
|
|
2308
|
+
return mapping;
|
|
2309
|
+
}
|
|
2310
|
+
readAssetMetadataFromFlock(assetId) {
|
|
2311
|
+
return assetMetaFromJson(this.metaFlock.get(["a", assetId]));
|
|
2312
|
+
}
|
|
2313
|
+
handleAssetRemoval(assetId, by) {
|
|
2314
|
+
const record = this.assets.get(assetId);
|
|
2315
|
+
if (!record) return;
|
|
2316
|
+
this.assets.delete(assetId);
|
|
2317
|
+
this.markAssetAsOrphan(assetId, record.metadata);
|
|
2318
|
+
const refs = this.assetToDocRefs.get(assetId);
|
|
2319
|
+
if (refs) {
|
|
2320
|
+
this.assetToDocRefs.delete(assetId);
|
|
2321
|
+
for (const docId of refs) {
|
|
2322
|
+
const assets = this.docAssets.get(docId);
|
|
2323
|
+
if (assets?.delete(assetId) && assets.size === 0) this.docAssets.delete(docId);
|
|
2324
|
+
this.eventBus.emit({
|
|
2325
|
+
kind: "asset-unlink",
|
|
2326
|
+
docId,
|
|
2327
|
+
assetId,
|
|
2328
|
+
by
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
for (const [docId, assets] of this.docAssets) if (assets.delete(assetId)) {
|
|
2334
|
+
if (assets.size === 0) this.docAssets.delete(docId);
|
|
2335
|
+
this.eventBus.emit({
|
|
2336
|
+
kind: "asset-unlink",
|
|
2337
|
+
docId,
|
|
2338
|
+
assetId,
|
|
2339
|
+
by
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
createAssetDownload(assetId, metadata, initialBytes) {
|
|
2344
|
+
let cached = initialBytes ? initialBytes.slice() : void 0;
|
|
2345
|
+
return {
|
|
2346
|
+
assetId,
|
|
2347
|
+
size: metadata.size,
|
|
2348
|
+
createdAt: metadata.createdAt,
|
|
2349
|
+
mime: metadata.mime,
|
|
2350
|
+
policy: metadata.policy,
|
|
2351
|
+
tag: metadata.tag,
|
|
2352
|
+
content: async () => {
|
|
2353
|
+
if (!cached) cached = (await this.materializeAsset(assetId)).bytes.slice();
|
|
2354
|
+
return toReadableStream(cached.slice());
|
|
2355
|
+
}
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
1651
2358
|
async materializeAsset(assetId) {
|
|
1652
2359
|
let record = this.assets.get(assetId);
|
|
1653
2360
|
if (record?.data) return {
|
|
@@ -1712,147 +2419,77 @@ var LoroRepo = class {
|
|
|
1712
2419
|
};
|
|
1713
2420
|
}
|
|
1714
2421
|
updateDocAssetMetadata(assetId, metadata) {
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
const now = Date.now();
|
|
1721
|
-
let removed = 0;
|
|
1722
|
-
for (const [assetId, orphan] of Array.from(this.orphanedAssets.entries())) {
|
|
1723
|
-
if (now - orphan.deletedAt < minKeepMs) continue;
|
|
1724
|
-
this.orphanedAssets.delete(assetId);
|
|
1725
|
-
if (this.storage?.deleteAsset) try {
|
|
1726
|
-
await this.storage.deleteAsset(assetId);
|
|
1727
|
-
} catch (error) {
|
|
1728
|
-
logAsyncError(`asset ${assetId} delete`)(error);
|
|
1729
|
-
}
|
|
1730
|
-
removed += 1;
|
|
2422
|
+
const refs = this.assetToDocRefs.get(assetId);
|
|
2423
|
+
if (!refs) return;
|
|
2424
|
+
for (const docId of refs) {
|
|
2425
|
+
const assets = this.docAssets.get(docId);
|
|
2426
|
+
if (assets) assets.set(assetId, metadata);
|
|
1731
2427
|
}
|
|
1732
|
-
return removed;
|
|
1733
2428
|
}
|
|
1734
|
-
|
|
1735
|
-
const
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
2429
|
+
rememberAsset(metadata, bytes) {
|
|
2430
|
+
const data = bytes ? bytes.slice() : this.assets.get(metadata.assetId)?.data;
|
|
2431
|
+
this.assets.set(metadata.assetId, {
|
|
2432
|
+
metadata,
|
|
2433
|
+
data
|
|
2434
|
+
});
|
|
2435
|
+
this.orphanedAssets.delete(metadata.assetId);
|
|
1740
2436
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
this.ensureDocSubscription(docId, cached);
|
|
1746
|
-
if (!this.docPersistedVersions.has(docId)) this.docPersistedVersions.set(docId, cached.version());
|
|
1747
|
-
return cached;
|
|
1748
|
-
}
|
|
1749
|
-
if (this.storage) {
|
|
1750
|
-
const stored = await this.storage.loadDoc(docId);
|
|
1751
|
-
if (stored) {
|
|
1752
|
-
this.registerDoc(docId, stored);
|
|
1753
|
-
return stored;
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
const created = await this.docFactory(docId);
|
|
1757
|
-
this.registerDoc(docId, created);
|
|
1758
|
-
return created;
|
|
2437
|
+
addDocReference(assetId, docId) {
|
|
2438
|
+
const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
|
|
2439
|
+
refs.add(docId);
|
|
2440
|
+
this.assetToDocRefs.set(assetId, refs);
|
|
1759
2441
|
}
|
|
1760
|
-
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
2442
|
+
removeDocAssetReference(assetId, docId) {
|
|
2443
|
+
const refs = this.assetToDocRefs.get(assetId);
|
|
2444
|
+
if (!refs) return;
|
|
2445
|
+
refs.delete(docId);
|
|
2446
|
+
if (refs.size === 0) {
|
|
2447
|
+
this.assetToDocRefs.delete(assetId);
|
|
2448
|
+
this.markAssetAsOrphan(assetId);
|
|
2449
|
+
}
|
|
1765
2450
|
}
|
|
1766
|
-
|
|
1767
|
-
const
|
|
1768
|
-
if (
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
if (!this.storage) return;
|
|
1774
|
-
const bundle = this.metaFlock.exportJson();
|
|
1775
|
-
const encoded = textEncoder.encode(JSON.stringify(bundle));
|
|
1776
|
-
await this.storage.save({
|
|
1777
|
-
type: "meta",
|
|
1778
|
-
update: encoded
|
|
2451
|
+
markAssetAsOrphan(assetId, metadataOverride) {
|
|
2452
|
+
const metadata = metadataOverride ?? this.assets.get(assetId)?.metadata;
|
|
2453
|
+
if (!metadata) return;
|
|
2454
|
+
const deletedAt = this.orphanedAssets.get(assetId)?.deletedAt ?? Date.now();
|
|
2455
|
+
this.orphanedAssets.set(assetId, {
|
|
2456
|
+
metadata,
|
|
2457
|
+
deletedAt
|
|
1779
2458
|
});
|
|
1780
2459
|
}
|
|
1781
|
-
|
|
1782
|
-
const
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
return;
|
|
1787
|
-
}
|
|
1788
|
-
const snapshot = doc.export({ mode: "snapshot" });
|
|
1789
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1790
|
-
try {
|
|
1791
|
-
await this.storage.save({
|
|
1792
|
-
type: "doc-snapshot",
|
|
1793
|
-
docId,
|
|
1794
|
-
snapshot
|
|
1795
|
-
});
|
|
1796
|
-
} catch (error) {
|
|
1797
|
-
if (previousVersion) this.docPersistedVersions.set(docId, previousVersion);
|
|
1798
|
-
else this.docPersistedVersions.delete(docId);
|
|
1799
|
-
throw error;
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
async persistDocUpdate(docId, doc) {
|
|
1803
|
-
const previousVersion = this.docPersistedVersions.get(docId);
|
|
1804
|
-
const nextVersion = doc.version();
|
|
1805
|
-
if (!this.storage) {
|
|
1806
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1807
|
-
return;
|
|
1808
|
-
}
|
|
1809
|
-
if (!previousVersion) {
|
|
1810
|
-
await this.persistDoc(docId, doc);
|
|
1811
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1812
|
-
return;
|
|
1813
|
-
}
|
|
1814
|
-
const update = doc.export({
|
|
1815
|
-
mode: "update",
|
|
1816
|
-
from: previousVersion
|
|
1817
|
-
});
|
|
1818
|
-
if (!update.length) {
|
|
1819
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1822
|
-
this.docPersistedVersions.set(docId, nextVersion);
|
|
1823
|
-
try {
|
|
1824
|
-
await this.storage.save({
|
|
1825
|
-
type: "doc-update",
|
|
1826
|
-
docId,
|
|
1827
|
-
update
|
|
1828
|
-
});
|
|
1829
|
-
} catch (error) {
|
|
1830
|
-
this.docPersistedVersions.set(docId, previousVersion);
|
|
1831
|
-
throw error;
|
|
2460
|
+
getAssetMetadata(assetId) {
|
|
2461
|
+
const record = this.assets.get(assetId);
|
|
2462
|
+
if (record) return record.metadata;
|
|
2463
|
+
for (const assets of this.docAssets.values()) {
|
|
2464
|
+
const metadata = assets.get(assetId);
|
|
2465
|
+
if (metadata) return metadata;
|
|
1832
2466
|
}
|
|
1833
2467
|
}
|
|
1834
|
-
|
|
1835
|
-
this.
|
|
1836
|
-
}
|
|
1837
|
-
popEventBy() {
|
|
1838
|
-
this.eventByStack.pop();
|
|
1839
|
-
}
|
|
1840
|
-
resolveEventBy(defaultBy) {
|
|
1841
|
-
const index = this.eventByStack.length - 1;
|
|
1842
|
-
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
1843
|
-
}
|
|
1844
|
-
ensureMetaLiveMonitor() {
|
|
1845
|
-
if (this.unsubscribeMetaFlock) return;
|
|
1846
|
-
this.unsubscribeMetaFlock = this.metaFlock.subscribe((batch) => {
|
|
1847
|
-
if (batch.source === "local") return;
|
|
1848
|
-
const by = this.resolveEventBy("live");
|
|
1849
|
-
(async () => {
|
|
1850
|
-
this.applyMetaFlockEvents(batch.events, by);
|
|
1851
|
-
await this.persistMeta();
|
|
1852
|
-
})().catch(logAsyncError("meta live monitor sync"));
|
|
1853
|
-
});
|
|
2468
|
+
get metaFlock() {
|
|
2469
|
+
return this.getMetaFlock();
|
|
1854
2470
|
}
|
|
1855
|
-
|
|
2471
|
+
};
|
|
2472
|
+
|
|
2473
|
+
//#endregion
|
|
2474
|
+
//#region src/internal/flock-hydrator.ts
|
|
2475
|
+
var FlockHydrator = class {
|
|
2476
|
+
getMetaFlock;
|
|
2477
|
+
metadataManager;
|
|
2478
|
+
assetManager;
|
|
2479
|
+
docManager;
|
|
2480
|
+
constructor(options) {
|
|
2481
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
2482
|
+
this.metadataManager = options.metadataManager;
|
|
2483
|
+
this.assetManager = options.assetManager;
|
|
2484
|
+
this.docManager = options.docManager;
|
|
2485
|
+
}
|
|
2486
|
+
hydrateAll(by) {
|
|
2487
|
+
const nextMetadata = this.readAllDocMetadata();
|
|
2488
|
+
this.metadataManager.replaceAll(nextMetadata, by);
|
|
2489
|
+
this.assetManager.hydrateFromFlock(by);
|
|
2490
|
+
this.docManager.hydrateFrontierKeys();
|
|
2491
|
+
}
|
|
2492
|
+
applyEvents(events, by) {
|
|
1856
2493
|
if (!events.length) return;
|
|
1857
2494
|
const docMetadataIds = /* @__PURE__ */ new Set();
|
|
1858
2495
|
const docAssetIds = /* @__PURE__ */ new Set();
|
|
@@ -1878,134 +2515,12 @@ var LoroRepo = class {
|
|
|
1878
2515
|
if (typeof docId === "string") docFrontiersIds.add(docId);
|
|
1879
2516
|
}
|
|
1880
2517
|
}
|
|
1881
|
-
for (const assetId of assetIds) this.refreshAssetMetadataEntry(assetId, by);
|
|
1882
|
-
for (const docId of docMetadataIds) this.
|
|
1883
|
-
for (const docId of docAssetIds) this.refreshDocAssetsEntry(docId, by);
|
|
1884
|
-
for (const docId of docFrontiersIds) this.refreshDocFrontierKeys(docId);
|
|
1885
|
-
}
|
|
1886
|
-
registerDoc(docId, doc) {
|
|
1887
|
-
this.docs.set(docId, doc);
|
|
1888
|
-
this.docPersistedVersions.set(docId, doc.version());
|
|
1889
|
-
this.ensureDocSubscription(docId, doc);
|
|
1890
|
-
}
|
|
1891
|
-
ensureDocSubscription(docId, doc) {
|
|
1892
|
-
if (this.docSubscriptions.has(docId)) return;
|
|
1893
|
-
const unsubscribe = doc.subscribe((batch) => {
|
|
1894
|
-
const stackBy = this.resolveEventBy("local");
|
|
1895
|
-
const by = stackBy === "local" && batch.by === "import" ? "live" : stackBy;
|
|
1896
|
-
this.onDocEvent(docId, doc, batch, by);
|
|
1897
|
-
});
|
|
1898
|
-
if (typeof unsubscribe === "function") this.docSubscriptions.set(docId, unsubscribe);
|
|
1899
|
-
}
|
|
1900
|
-
rememberAsset(metadata, bytes) {
|
|
1901
|
-
const data = bytes ? bytes.slice() : this.assets.get(metadata.assetId)?.data;
|
|
1902
|
-
this.assets.set(metadata.assetId, {
|
|
1903
|
-
metadata,
|
|
1904
|
-
data
|
|
1905
|
-
});
|
|
1906
|
-
this.orphanedAssets.delete(metadata.assetId);
|
|
1907
|
-
}
|
|
1908
|
-
scheduleDocFrontierUpdate(docId, doc, by) {
|
|
1909
|
-
const existing = this.docFrontierUpdates.get(docId);
|
|
1910
|
-
const effectiveBy = existing ? this.mergeRepoEventBy(existing.by, by) : by;
|
|
1911
|
-
if (existing) clearTimeout(existing.timeout);
|
|
1912
|
-
const delay = this.docFrontierDebounceMs > 0 ? this.docFrontierDebounceMs : 0;
|
|
1913
|
-
const timeout = setTimeout(() => this.runScheduledDocFrontierUpdate(docId), delay);
|
|
1914
|
-
this.docFrontierUpdates.set(docId, {
|
|
1915
|
-
timeout,
|
|
1916
|
-
doc,
|
|
1917
|
-
by: effectiveBy
|
|
1918
|
-
});
|
|
1919
|
-
}
|
|
1920
|
-
mergeRepoEventBy(current, next) {
|
|
1921
|
-
if (current === next) return current;
|
|
1922
|
-
if (current === "live" || next === "live") return "live";
|
|
1923
|
-
if (current === "sync" || next === "sync") return "sync";
|
|
1924
|
-
return "local";
|
|
1925
|
-
}
|
|
1926
|
-
runScheduledDocFrontierUpdate(docId) {
|
|
1927
|
-
const pending = this.docFrontierUpdates.get(docId);
|
|
1928
|
-
if (!pending) return;
|
|
1929
|
-
this.docFrontierUpdates.delete(docId);
|
|
1930
|
-
this.pushEventBy(pending.by);
|
|
1931
|
-
(async () => {
|
|
1932
|
-
try {
|
|
1933
|
-
await this.updateDocFrontiers(docId, pending.doc, pending.by);
|
|
1934
|
-
} finally {
|
|
1935
|
-
this.popEventBy();
|
|
1936
|
-
}
|
|
1937
|
-
})().catch(logAsyncError(`doc ${docId} frontier debounce`));
|
|
1938
|
-
}
|
|
1939
|
-
async flushScheduledDocFrontierUpdate(docId) {
|
|
1940
|
-
const pending = this.docFrontierUpdates.get(docId);
|
|
1941
|
-
if (!pending) return false;
|
|
1942
|
-
clearTimeout(pending.timeout);
|
|
1943
|
-
this.docFrontierUpdates.delete(docId);
|
|
1944
|
-
this.pushEventBy(pending.by);
|
|
1945
|
-
try {
|
|
1946
|
-
await this.updateDocFrontiers(docId, pending.doc, pending.by);
|
|
1947
|
-
} finally {
|
|
1948
|
-
this.popEventBy();
|
|
1949
|
-
}
|
|
1950
|
-
return true;
|
|
1951
|
-
}
|
|
1952
|
-
onDocEvent(docId, doc, _batch, by) {
|
|
1953
|
-
(async () => {
|
|
1954
|
-
const a = this.persistDocUpdate(docId, doc);
|
|
1955
|
-
if (by === "local") {
|
|
1956
|
-
this.scheduleDocFrontierUpdate(docId, doc, by);
|
|
1957
|
-
await a;
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
const b = this.flushScheduledDocFrontierUpdate(docId);
|
|
1961
|
-
const c = this.updateDocFrontiers(docId, doc, by);
|
|
1962
|
-
await Promise.all([
|
|
1963
|
-
a,
|
|
1964
|
-
b,
|
|
1965
|
-
c
|
|
1966
|
-
]);
|
|
1967
|
-
})().catch(logAsyncError(`doc ${docId} event processing`));
|
|
1968
|
-
}
|
|
1969
|
-
getAssetMetadata(assetId) {
|
|
1970
|
-
const record = this.assets.get(assetId);
|
|
1971
|
-
if (record) return record.metadata;
|
|
1972
|
-
for (const assets of this.docAssets.values()) {
|
|
1973
|
-
const metadata = assets.get(assetId);
|
|
1974
|
-
if (metadata) return metadata;
|
|
1975
|
-
}
|
|
2518
|
+
for (const assetId of assetIds) this.assetManager.refreshAssetMetadataEntry(assetId, by);
|
|
2519
|
+
for (const docId of docMetadataIds) this.metadataManager.refreshFromFlock(docId, by);
|
|
2520
|
+
for (const docId of docAssetIds) this.assetManager.refreshDocAssetsEntry(docId, by);
|
|
2521
|
+
for (const docId of docFrontiersIds) this.docManager.refreshDocFrontierKeys(docId);
|
|
1976
2522
|
}
|
|
1977
|
-
|
|
1978
|
-
const { json, key } = canonicalizeVersionVector(computeVersionVector(doc));
|
|
1979
|
-
const existingKeys = this.docFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1980
|
-
let mutated = false;
|
|
1981
|
-
if (existingKeys.size !== 1 || !existingKeys.has(key)) {
|
|
1982
|
-
for (const entry of existingKeys) this.metaFlock.delete([
|
|
1983
|
-
"f",
|
|
1984
|
-
docId,
|
|
1985
|
-
entry
|
|
1986
|
-
]);
|
|
1987
|
-
this.metaFlock.put([
|
|
1988
|
-
"f",
|
|
1989
|
-
docId,
|
|
1990
|
-
key
|
|
1991
|
-
], json);
|
|
1992
|
-
this.docFrontierKeys.set(docId, new Set([key]));
|
|
1993
|
-
mutated = true;
|
|
1994
|
-
}
|
|
1995
|
-
if (mutated) await this.persistMeta();
|
|
1996
|
-
const by = this.resolveEventBy(defaultBy);
|
|
1997
|
-
const frontiers = getDocFrontiers(doc);
|
|
1998
|
-
this.emit({
|
|
1999
|
-
kind: "doc-frontiers",
|
|
2000
|
-
docId,
|
|
2001
|
-
frontiers,
|
|
2002
|
-
by
|
|
2003
|
-
});
|
|
2004
|
-
}
|
|
2005
|
-
hydrateMetadataFromFlock(by) {
|
|
2006
|
-
const prevMetadata = new Map(this.metadata);
|
|
2007
|
-
const prevDocAssets = new Map(this.docAssets);
|
|
2008
|
-
const prevAssets = new Map(this.assets);
|
|
2523
|
+
readAllDocMetadata() {
|
|
2009
2524
|
const nextMetadata = /* @__PURE__ */ new Map();
|
|
2010
2525
|
const metadataRows = this.metaFlock.scan({ prefix: ["m"] });
|
|
2011
2526
|
for (const row of metadataRows) {
|
|
@@ -2036,137 +2551,407 @@ var LoroRepo = class {
|
|
|
2036
2551
|
if (jsonValue === void 0) continue;
|
|
2037
2552
|
docMeta[fieldKey] = jsonValue;
|
|
2038
2553
|
}
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2554
|
+
return nextMetadata;
|
|
2555
|
+
}
|
|
2556
|
+
get metaFlock() {
|
|
2557
|
+
return this.getMetaFlock();
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
|
|
2561
|
+
//#endregion
|
|
2562
|
+
//#region src/internal/sync-runner.ts
|
|
2563
|
+
/**
|
|
2564
|
+
* Sync data between storage and transport layer
|
|
2565
|
+
*/
|
|
2566
|
+
var SyncRunner = class {
|
|
2567
|
+
storage;
|
|
2568
|
+
transport;
|
|
2569
|
+
eventBus;
|
|
2570
|
+
docManager;
|
|
2571
|
+
metadataManager;
|
|
2572
|
+
assetManager;
|
|
2573
|
+
flockHydrator;
|
|
2574
|
+
getMetaFlock;
|
|
2575
|
+
replaceMetaFlock;
|
|
2576
|
+
persistMeta;
|
|
2577
|
+
readyPromise;
|
|
2578
|
+
metaRoomSubscription;
|
|
2579
|
+
unsubscribeMetaFlock;
|
|
2580
|
+
docSubscriptions = /* @__PURE__ */ new Map();
|
|
2581
|
+
constructor(options) {
|
|
2582
|
+
this.storage = options.storage;
|
|
2583
|
+
this.transport = options.transport;
|
|
2584
|
+
this.eventBus = options.eventBus;
|
|
2585
|
+
this.docManager = options.docManager;
|
|
2586
|
+
this.metadataManager = options.metadataManager;
|
|
2587
|
+
this.assetManager = options.assetManager;
|
|
2588
|
+
this.flockHydrator = options.flockHydrator;
|
|
2589
|
+
this.getMetaFlock = options.getMetaFlock;
|
|
2590
|
+
this.replaceMetaFlock = options.mergeFlock;
|
|
2591
|
+
this.persistMeta = options.persistMeta;
|
|
2592
|
+
}
|
|
2593
|
+
async ready() {
|
|
2594
|
+
if (!this.readyPromise) this.readyPromise = this.initialize();
|
|
2595
|
+
await this.readyPromise;
|
|
2596
|
+
}
|
|
2597
|
+
async sync(options = {}) {
|
|
2598
|
+
await this.ready();
|
|
2599
|
+
const { scope = "full", docIds } = options;
|
|
2600
|
+
if (!this.transport) return;
|
|
2601
|
+
if (!this.transport.isConnected()) await this.transport.connect();
|
|
2602
|
+
if (scope === "meta" || scope === "full") {
|
|
2603
|
+
this.eventBus.pushEventBy("sync");
|
|
2604
|
+
const recordedEvents = [];
|
|
2605
|
+
const unsubscribe = this.metaFlock.subscribe((batch) => {
|
|
2606
|
+
if (batch.source === "local") return;
|
|
2607
|
+
recordedEvents.push(...batch.events);
|
|
2051
2608
|
});
|
|
2609
|
+
try {
|
|
2610
|
+
if (!(await this.transport.syncMeta(this.metaFlock)).ok) throw new Error("Metadata sync failed");
|
|
2611
|
+
if (recordedEvents.length > 0) this.flockHydrator.applyEvents(recordedEvents, "sync");
|
|
2612
|
+
else this.flockHydrator.hydrateAll("sync");
|
|
2613
|
+
await this.persistMeta();
|
|
2614
|
+
} finally {
|
|
2615
|
+
unsubscribe();
|
|
2616
|
+
this.eventBus.popEventBy();
|
|
2617
|
+
}
|
|
2052
2618
|
}
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
const nextFrontierKeys = /* @__PURE__ */ new Map();
|
|
2067
|
-
const frontierRows = this.metaFlock.scan({ prefix: ["f"] });
|
|
2068
|
-
for (const row of frontierRows) {
|
|
2069
|
-
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
2070
|
-
const docId = row.key[1];
|
|
2071
|
-
const frontierKey = row.key[2];
|
|
2072
|
-
if (typeof docId !== "string" || typeof frontierKey !== "string") continue;
|
|
2073
|
-
const set = nextFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
2074
|
-
set.add(frontierKey);
|
|
2075
|
-
nextFrontierKeys.set(docId, set);
|
|
2619
|
+
if (scope === "doc" || scope === "full") {
|
|
2620
|
+
const targets = docIds ?? this.metadataManager.getDocIds();
|
|
2621
|
+
for (const docId of targets) {
|
|
2622
|
+
const doc = await this.docManager.ensureDoc(docId);
|
|
2623
|
+
this.eventBus.pushEventBy("sync");
|
|
2624
|
+
try {
|
|
2625
|
+
if (!(await this.transport.syncDoc(docId, doc)).ok) throw new Error(`Document sync failed for ${docId}`);
|
|
2626
|
+
} finally {
|
|
2627
|
+
this.eventBus.popEventBy();
|
|
2628
|
+
}
|
|
2629
|
+
await this.docManager.persistDoc(docId, doc);
|
|
2630
|
+
await this.docManager.updateDocFrontiers(docId, doc, "sync");
|
|
2631
|
+
}
|
|
2076
2632
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2633
|
+
}
|
|
2634
|
+
async joinMetaRoom(params) {
|
|
2635
|
+
await this.ready();
|
|
2636
|
+
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
2637
|
+
if (!this.transport.isConnected()) await this.transport.connect();
|
|
2638
|
+
if (this.metaRoomSubscription) return this.metaRoomSubscription;
|
|
2639
|
+
this.ensureMetaLiveMonitor();
|
|
2640
|
+
const subscription = this.transport.joinMetaRoom(this.metaFlock, params);
|
|
2641
|
+
const wrapped = {
|
|
2642
|
+
unsubscribe: () => {
|
|
2643
|
+
subscription.unsubscribe();
|
|
2644
|
+
if (this.metaRoomSubscription === wrapped) this.metaRoomSubscription = void 0;
|
|
2645
|
+
if (this.unsubscribeMetaFlock) {
|
|
2646
|
+
this.unsubscribeMetaFlock();
|
|
2647
|
+
this.unsubscribeMetaFlock = void 0;
|
|
2648
|
+
}
|
|
2649
|
+
},
|
|
2650
|
+
firstSyncedWithRemote: subscription.firstSyncedWithRemote,
|
|
2651
|
+
get connected() {
|
|
2652
|
+
return subscription.connected;
|
|
2087
2653
|
}
|
|
2654
|
+
};
|
|
2655
|
+
this.metaRoomSubscription = wrapped;
|
|
2656
|
+
subscription.firstSyncedWithRemote.then(async () => {
|
|
2657
|
+
const by = this.eventBus.resolveEventBy("live");
|
|
2658
|
+
this.flockHydrator.hydrateAll(by);
|
|
2659
|
+
await this.persistMeta();
|
|
2660
|
+
}).catch(logAsyncError("meta room first sync"));
|
|
2661
|
+
return wrapped;
|
|
2662
|
+
}
|
|
2663
|
+
async joinDocRoom(docId, params) {
|
|
2664
|
+
await this.ready();
|
|
2665
|
+
if (!this.transport) throw new Error("Transport adapter not configured");
|
|
2666
|
+
if (!this.transport.isConnected()) await this.transport.connect();
|
|
2667
|
+
const existing = this.docSubscriptions.get(docId);
|
|
2668
|
+
if (existing) return existing;
|
|
2669
|
+
const doc = await this.docManager.ensureDoc(docId);
|
|
2670
|
+
const subscription = this.transport.joinDocRoom(docId, doc, params);
|
|
2671
|
+
const wrapped = {
|
|
2672
|
+
unsubscribe: () => {
|
|
2673
|
+
subscription.unsubscribe();
|
|
2674
|
+
if (this.docSubscriptions.get(docId) === wrapped) this.docSubscriptions.delete(docId);
|
|
2675
|
+
},
|
|
2676
|
+
firstSyncedWithRemote: subscription.firstSyncedWithRemote,
|
|
2677
|
+
get connected() {
|
|
2678
|
+
return subscription.connected;
|
|
2679
|
+
}
|
|
2680
|
+
};
|
|
2681
|
+
this.docSubscriptions.set(docId, wrapped);
|
|
2682
|
+
subscription.firstSyncedWithRemote.catch(logAsyncError(`doc ${docId} first sync`));
|
|
2683
|
+
return wrapped;
|
|
2684
|
+
}
|
|
2685
|
+
async destroy() {
|
|
2686
|
+
await this.docManager.close();
|
|
2687
|
+
this.metaRoomSubscription?.unsubscribe();
|
|
2688
|
+
this.metaRoomSubscription = void 0;
|
|
2689
|
+
for (const sub of this.docSubscriptions.values()) sub.unsubscribe();
|
|
2690
|
+
this.docSubscriptions.clear();
|
|
2691
|
+
if (this.unsubscribeMetaFlock) {
|
|
2692
|
+
this.unsubscribeMetaFlock();
|
|
2693
|
+
this.unsubscribeMetaFlock = void 0;
|
|
2088
2694
|
}
|
|
2089
|
-
this.
|
|
2090
|
-
|
|
2091
|
-
this.
|
|
2092
|
-
|
|
2093
|
-
this.
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
this.
|
|
2695
|
+
this.eventBus.clear();
|
|
2696
|
+
this.metadataManager.clear();
|
|
2697
|
+
this.assetManager.clear();
|
|
2698
|
+
this.readyPromise = void 0;
|
|
2699
|
+
await this.transport?.close();
|
|
2700
|
+
}
|
|
2701
|
+
async initialize() {
|
|
2702
|
+
if (this.storage) {
|
|
2703
|
+
const snapshot = await this.storage.loadMeta();
|
|
2704
|
+
if (snapshot) this.replaceMetaFlock(snapshot);
|
|
2098
2705
|
}
|
|
2099
|
-
this.
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2706
|
+
this.flockHydrator.hydrateAll("sync");
|
|
2707
|
+
}
|
|
2708
|
+
ensureMetaLiveMonitor() {
|
|
2709
|
+
if (this.unsubscribeMetaFlock) return;
|
|
2710
|
+
this.unsubscribeMetaFlock = this.metaFlock.subscribe((batch) => {
|
|
2711
|
+
if (batch.source === "local") return;
|
|
2712
|
+
const by = this.eventBus.resolveEventBy("live");
|
|
2713
|
+
(async () => {
|
|
2714
|
+
this.flockHydrator.applyEvents(batch.events, by);
|
|
2715
|
+
await this.persistMeta();
|
|
2716
|
+
})().catch(logAsyncError("meta live monitor sync"));
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
get metaFlock() {
|
|
2720
|
+
return this.getMetaFlock();
|
|
2721
|
+
}
|
|
2722
|
+
};
|
|
2723
|
+
|
|
2724
|
+
//#endregion
|
|
2725
|
+
//#region src/internal/repo-state.ts
|
|
2726
|
+
function createRepoState() {
|
|
2727
|
+
return {
|
|
2728
|
+
metadata: /* @__PURE__ */ new Map(),
|
|
2729
|
+
docAssets: /* @__PURE__ */ new Map(),
|
|
2730
|
+
assets: /* @__PURE__ */ new Map(),
|
|
2731
|
+
orphanedAssets: /* @__PURE__ */ new Map(),
|
|
2732
|
+
assetToDocRefs: /* @__PURE__ */ new Map(),
|
|
2733
|
+
docFrontierKeys: /* @__PURE__ */ new Map()
|
|
2734
|
+
};
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
//#endregion
|
|
2738
|
+
//#region src/index.ts
|
|
2739
|
+
const textEncoder = new TextEncoder();
|
|
2740
|
+
const DEFAULT_DOC_FRONTIER_DEBOUNCE_MS = 1e3;
|
|
2741
|
+
var LoroRepo = class LoroRepo {
|
|
2742
|
+
options;
|
|
2743
|
+
_destroyed = false;
|
|
2744
|
+
transport;
|
|
2745
|
+
storage;
|
|
2746
|
+
metaFlock = new __loro_dev_flock.Flock();
|
|
2747
|
+
eventBus;
|
|
2748
|
+
docManager;
|
|
2749
|
+
metadataManager;
|
|
2750
|
+
assetManager;
|
|
2751
|
+
assetTransport;
|
|
2752
|
+
flockHydrator;
|
|
2753
|
+
state;
|
|
2754
|
+
syncRunner;
|
|
2755
|
+
constructor(options) {
|
|
2756
|
+
this.options = options;
|
|
2757
|
+
this.transport = options.transportAdapter;
|
|
2758
|
+
this.storage = options.storageAdapter;
|
|
2759
|
+
this.assetTransport = options.assetTransportAdapter;
|
|
2760
|
+
this.eventBus = new RepoEventBus();
|
|
2761
|
+
this.state = createRepoState();
|
|
2762
|
+
const configuredDebounce = options.docFrontierDebounceMs;
|
|
2763
|
+
const docFrontierDebounceMs = typeof configuredDebounce === "number" && Number.isFinite(configuredDebounce) && configuredDebounce >= 0 ? configuredDebounce : DEFAULT_DOC_FRONTIER_DEBOUNCE_MS;
|
|
2764
|
+
this.docManager = new DocManager({
|
|
2765
|
+
storage: this.storage,
|
|
2766
|
+
docFrontierDebounceMs,
|
|
2767
|
+
getMetaFlock: () => this.metaFlock,
|
|
2768
|
+
eventBus: this.eventBus,
|
|
2769
|
+
persistMeta: () => this.persistMeta(),
|
|
2770
|
+
state: this.state
|
|
2771
|
+
});
|
|
2772
|
+
this.metadataManager = new MetadataManager({
|
|
2773
|
+
getMetaFlock: () => this.metaFlock,
|
|
2774
|
+
eventBus: this.eventBus,
|
|
2775
|
+
persistMeta: () => this.persistMeta(),
|
|
2776
|
+
state: this.state
|
|
2777
|
+
});
|
|
2778
|
+
this.assetManager = new AssetManager({
|
|
2779
|
+
storage: this.storage,
|
|
2780
|
+
assetTransport: this.assetTransport,
|
|
2781
|
+
getMetaFlock: () => this.metaFlock,
|
|
2782
|
+
eventBus: this.eventBus,
|
|
2783
|
+
persistMeta: () => this.persistMeta(),
|
|
2784
|
+
state: this.state
|
|
2785
|
+
});
|
|
2786
|
+
this.flockHydrator = new FlockHydrator({
|
|
2787
|
+
getMetaFlock: () => this.metaFlock,
|
|
2788
|
+
metadataManager: this.metadataManager,
|
|
2789
|
+
assetManager: this.assetManager,
|
|
2790
|
+
docManager: this.docManager
|
|
2791
|
+
});
|
|
2792
|
+
this.syncRunner = new SyncRunner({
|
|
2793
|
+
storage: this.storage,
|
|
2794
|
+
transport: this.transport,
|
|
2795
|
+
eventBus: this.eventBus,
|
|
2796
|
+
docManager: this.docManager,
|
|
2797
|
+
metadataManager: this.metadataManager,
|
|
2798
|
+
assetManager: this.assetManager,
|
|
2799
|
+
flockHydrator: this.flockHydrator,
|
|
2800
|
+
getMetaFlock: () => this.metaFlock,
|
|
2801
|
+
mergeFlock: (snapshot) => {
|
|
2802
|
+
this.metaFlock.merge(snapshot);
|
|
2803
|
+
},
|
|
2804
|
+
persistMeta: () => this.persistMeta()
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
static async create(options) {
|
|
2808
|
+
const repo = new LoroRepo(options);
|
|
2809
|
+
await repo.storage?.init?.();
|
|
2810
|
+
await repo.ready();
|
|
2811
|
+
return repo;
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Load meta from storage.
|
|
2815
|
+
*
|
|
2816
|
+
* You need to call this before all other operations to make the app functioning correctly.
|
|
2817
|
+
* Though we do that implicitly already
|
|
2818
|
+
*/
|
|
2819
|
+
async ready() {
|
|
2820
|
+
await this.syncRunner.ready();
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Sync selected data via the transport adaptor
|
|
2824
|
+
* @param options
|
|
2825
|
+
*/
|
|
2826
|
+
async sync(options = {}) {
|
|
2827
|
+
await this.syncRunner.sync(options);
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* Start syncing the metadata (Flock) room. It will establish a realtime connection to the transport adaptor.
|
|
2831
|
+
* All changes on the room will be synced to the Flock, and all changes on the Flock will be synced to the room.
|
|
2832
|
+
* @param params
|
|
2833
|
+
* @returns
|
|
2834
|
+
*/
|
|
2835
|
+
async joinMetaRoom(params) {
|
|
2836
|
+
return this.syncRunner.joinMetaRoom(params);
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Start syncing the given doc. It will establish a realtime connection to the transport adaptor.
|
|
2840
|
+
* All changes on the doc will be synced to the transport, and all changes on the transport will be synced to the doc.
|
|
2841
|
+
*
|
|
2842
|
+
* All the changes on the room will be reflected on the same doc you get from `repo.openCollaborativeDoc(docId)`
|
|
2843
|
+
* @param docId
|
|
2844
|
+
* @param params
|
|
2845
|
+
* @returns
|
|
2846
|
+
*/
|
|
2847
|
+
async joinDocRoom(docId, params) {
|
|
2848
|
+
return this.syncRunner.joinDocRoom(docId, params);
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Opens a document that is automatically persisted to the configured storage adapter.
|
|
2852
|
+
*
|
|
2853
|
+
* - Edits are saved to storage (debounced).
|
|
2854
|
+
* - Frontiers are synced to the metadata (Flock).
|
|
2855
|
+
* - Realtime collaboration is NOT enabled by default; use `joinDocRoom` to connect.
|
|
2856
|
+
*/
|
|
2857
|
+
async openPersistedDoc(docId) {
|
|
2858
|
+
return {
|
|
2859
|
+
doc: await this.docManager.openCollaborativeDoc(docId),
|
|
2860
|
+
syncOnce: () => {
|
|
2861
|
+
return this.sync({
|
|
2862
|
+
scope: "doc",
|
|
2863
|
+
docIds: [docId]
|
|
2113
2864
|
});
|
|
2114
|
-
|
|
2865
|
+
},
|
|
2866
|
+
joinRoom: (auth) => {
|
|
2867
|
+
return this.syncRunner.joinDocRoom(docId, { auth });
|
|
2115
2868
|
}
|
|
2116
|
-
|
|
2117
|
-
if (Object.keys(patch).length > 0) this.emit({
|
|
2118
|
-
kind: "doc-metadata",
|
|
2119
|
-
docId,
|
|
2120
|
-
patch,
|
|
2121
|
-
by
|
|
2122
|
-
});
|
|
2123
|
-
}
|
|
2124
|
-
for (const [assetId, record] of nextAssets) {
|
|
2125
|
-
const previous = prevAssets.get(assetId)?.metadata;
|
|
2126
|
-
if (!assetMetadataEqual(previous, record.metadata)) this.emit({
|
|
2127
|
-
kind: "asset-metadata",
|
|
2128
|
-
asset: this.createAssetDownload(assetId, record.metadata, record.data),
|
|
2129
|
-
by
|
|
2130
|
-
});
|
|
2131
|
-
}
|
|
2132
|
-
for (const [docId, assets] of nextDocAssets) {
|
|
2133
|
-
const previous = prevDocAssets.get(docId);
|
|
2134
|
-
for (const assetId of assets.keys()) if (!previous || !previous.has(assetId)) this.emit({
|
|
2135
|
-
kind: "asset-link",
|
|
2136
|
-
docId,
|
|
2137
|
-
assetId,
|
|
2138
|
-
by
|
|
2139
|
-
});
|
|
2140
|
-
}
|
|
2141
|
-
for (const [docId, assets] of prevDocAssets) {
|
|
2142
|
-
const current = nextDocAssets.get(docId);
|
|
2143
|
-
for (const assetId of assets.keys()) if (!current || !current.has(assetId)) this.emit({
|
|
2144
|
-
kind: "asset-unlink",
|
|
2145
|
-
docId,
|
|
2146
|
-
assetId,
|
|
2147
|
-
by
|
|
2148
|
-
});
|
|
2149
|
-
}
|
|
2869
|
+
};
|
|
2150
2870
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
return
|
|
2871
|
+
async upsertDocMeta(docId, patch) {
|
|
2872
|
+
await this.metadataManager.upsert(docId, patch);
|
|
2873
|
+
}
|
|
2874
|
+
async getDocMeta(docId) {
|
|
2875
|
+
return this.metadataManager.get(docId);
|
|
2876
|
+
}
|
|
2877
|
+
async listDoc(query) {
|
|
2878
|
+
return this.metadataManager.listDoc(query);
|
|
2879
|
+
}
|
|
2880
|
+
getMeta() {
|
|
2881
|
+
return this.metaFlock;
|
|
2882
|
+
}
|
|
2883
|
+
watch(listener, filter = {}) {
|
|
2884
|
+
return this.eventBus.watch(listener, filter);
|
|
2885
|
+
}
|
|
2886
|
+
/**
|
|
2887
|
+
* Opens a detached `LoroDoc` snapshot.
|
|
2888
|
+
*
|
|
2889
|
+
* - **No Persistence**: Edits to this document are NOT saved to storage.
|
|
2890
|
+
* - **No Sync**: This document does not participate in realtime updates.
|
|
2891
|
+
* - **Use Case**: Ideal for read-only history inspection, temporary drafts, or conflict resolution without affecting the main state.
|
|
2892
|
+
*/
|
|
2893
|
+
async openDetachedDoc(docId) {
|
|
2894
|
+
return this.docManager.openDetachedDoc(docId);
|
|
2895
|
+
}
|
|
2896
|
+
/**
|
|
2897
|
+
* Explicitly unloads a document from memory.
|
|
2898
|
+
*
|
|
2899
|
+
* - **Persists Immediately**: Forces a save of the document's current state to storage.
|
|
2900
|
+
* - **Frees Memory**: Removes the document from the internal cache.
|
|
2901
|
+
* - **Note**: If the document is currently being synced (via `joinDocRoom`), you should also unsubscribe from the room to fully release resources.
|
|
2902
|
+
*/
|
|
2903
|
+
async unloadDoc(docId) {
|
|
2904
|
+
await this.docManager.unloadDoc(docId);
|
|
2905
|
+
}
|
|
2906
|
+
async flush() {
|
|
2907
|
+
await this.docManager.flush();
|
|
2908
|
+
}
|
|
2909
|
+
async uploadAsset(params) {
|
|
2910
|
+
return this.assetManager.uploadAsset(params);
|
|
2911
|
+
}
|
|
2912
|
+
async linkAsset(docId, params) {
|
|
2913
|
+
return this.assetManager.linkAsset(docId, params);
|
|
2914
|
+
}
|
|
2915
|
+
async fetchAsset(assetId) {
|
|
2916
|
+
return this.assetManager.fetchAsset(assetId);
|
|
2917
|
+
}
|
|
2918
|
+
async unlinkAsset(docId, assetId) {
|
|
2919
|
+
await this.assetManager.unlinkAsset(docId, assetId);
|
|
2920
|
+
}
|
|
2921
|
+
async listAssets(docId) {
|
|
2922
|
+
return this.assetManager.listAssets(docId);
|
|
2923
|
+
}
|
|
2924
|
+
async ensureAsset(assetId) {
|
|
2925
|
+
return this.assetManager.ensureAsset(assetId);
|
|
2926
|
+
}
|
|
2927
|
+
async gcAssets(options = {}) {
|
|
2928
|
+
return this.assetManager.gcAssets(options);
|
|
2929
|
+
}
|
|
2930
|
+
async persistMeta() {
|
|
2931
|
+
if (!this.storage) return;
|
|
2932
|
+
const bundle = this.metaFlock.exportJson();
|
|
2933
|
+
const encoded = textEncoder.encode(JSON.stringify(bundle));
|
|
2934
|
+
await this.storage.save({
|
|
2935
|
+
type: "meta",
|
|
2936
|
+
update: encoded
|
|
2937
|
+
});
|
|
2938
|
+
}
|
|
2939
|
+
get destroyed() {
|
|
2940
|
+
return this._destroyed;
|
|
2941
|
+
}
|
|
2942
|
+
async destroy() {
|
|
2943
|
+
if (this._destroyed) return;
|
|
2944
|
+
this._destroyed = true;
|
|
2945
|
+
await this.syncRunner.destroy();
|
|
2946
|
+
this.assetTransport?.close?.();
|
|
2947
|
+
this.storage?.close?.();
|
|
2948
|
+
await this.transport?.close();
|
|
2165
2949
|
}
|
|
2166
2950
|
};
|
|
2167
2951
|
|
|
2168
2952
|
//#endregion
|
|
2169
2953
|
exports.BroadcastChannelTransportAdapter = BroadcastChannelTransportAdapter;
|
|
2954
|
+
exports.FileSystemStorageAdaptor = FileSystemStorageAdaptor;
|
|
2170
2955
|
exports.IndexedDBStorageAdaptor = IndexedDBStorageAdaptor;
|
|
2171
2956
|
exports.LoroRepo = LoroRepo;
|
|
2172
2957
|
exports.WebSocketTransportAdapter = WebSocketTransportAdapter;
|