loro-repo 0.4.0 → 0.5.1
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 +9 -9
- package/dist/chunk.cjs +30 -0
- package/dist/index.cjs +250 -1424
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -629
- package/dist/index.d.ts +5 -630
- package/dist/index.js +248 -1389
- package/dist/index.js.map +1 -1
- package/dist/storage/filesystem.cjs +164 -0
- package/dist/storage/filesystem.cjs.map +1 -0
- package/dist/storage/filesystem.d.cts +49 -0
- package/dist/storage/filesystem.d.ts +49 -0
- package/dist/storage/filesystem.js +158 -0
- package/dist/storage/filesystem.js.map +1 -0
- package/dist/storage/indexeddb.cjs +261 -0
- package/dist/storage/indexeddb.cjs.map +1 -0
- package/dist/storage/indexeddb.d.cts +54 -0
- package/dist/storage/indexeddb.d.ts +54 -0
- package/dist/storage/indexeddb.js +258 -0
- package/dist/storage/indexeddb.js.map +1 -0
- package/dist/transport/broadcast-channel.cjs +252 -0
- package/dist/transport/broadcast-channel.cjs.map +1 -0
- package/dist/transport/broadcast-channel.d.cts +45 -0
- package/dist/transport/broadcast-channel.d.ts +45 -0
- package/dist/transport/broadcast-channel.js +251 -0
- package/dist/transport/broadcast-channel.js.map +1 -0
- package/dist/transport/websocket.cjs +435 -0
- package/dist/transport/websocket.cjs.map +1 -0
- package/dist/transport/websocket.d.cts +69 -0
- package/dist/transport/websocket.d.ts +69 -0
- package/dist/transport/websocket.js +430 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/types.d.cts +419 -0
- package/dist/types.d.ts +419 -0
- package/package.json +28 -4
package/dist/index.js
CHANGED
|
@@ -1,1325 +1,53 @@
|
|
|
1
1
|
import { Flock } from "@loro-dev/flock";
|
|
2
|
-
import { LoroAdaptor } from "loro-adaptors/loro";
|
|
3
|
-
import { CrdtType, bytesToHex } from "loro-protocol";
|
|
4
|
-
import { LoroWebsocketClient } from "loro-websocket";
|
|
5
2
|
import { LoroDoc } from "loro-crdt";
|
|
6
|
-
import { FlockAdaptor } from "loro-adaptors/flock";
|
|
7
|
-
import { promises } from "node:fs";
|
|
8
|
-
import * as path from "node:path";
|
|
9
|
-
import { randomUUID } from "node:crypto";
|
|
10
3
|
|
|
11
|
-
//#region src/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (!
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
59
|
-
return new Promise((resolve, reject) => {
|
|
60
|
-
const timer = setTimeout(() => {
|
|
61
|
-
reject(/* @__PURE__ */ new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
62
|
-
}, timeoutMs);
|
|
63
|
-
promise.then((value) => {
|
|
64
|
-
clearTimeout(timer);
|
|
65
|
-
resolve(value);
|
|
66
|
-
}).catch((error) => {
|
|
67
|
-
clearTimeout(timer);
|
|
68
|
-
reject(error);
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
function normalizeRoomId(roomId, fallback) {
|
|
73
|
-
if (typeof roomId === "string" && roomId.length > 0) return roomId;
|
|
74
|
-
if (roomId instanceof Uint8Array && roomId.length > 0) try {
|
|
75
|
-
return bytesToHex(roomId);
|
|
76
|
-
} catch {
|
|
77
|
-
return fallback;
|
|
78
|
-
}
|
|
79
|
-
return fallback;
|
|
80
|
-
}
|
|
81
|
-
function bytesEqual(a, b) {
|
|
82
|
-
if (a === b) return true;
|
|
83
|
-
if (!a || !b) return false;
|
|
84
|
-
if (a.length !== b.length) return false;
|
|
85
|
-
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* loro-websocket backed {@link TransportAdapter} implementation for LoroRepo.
|
|
90
|
-
*/
|
|
91
|
-
var WebSocketTransportAdapter = class {
|
|
92
|
-
options;
|
|
93
|
-
client;
|
|
94
|
-
metadataSession;
|
|
95
|
-
docSessions = /* @__PURE__ */ new Map();
|
|
96
|
-
constructor(options) {
|
|
97
|
-
this.options = options;
|
|
98
|
-
}
|
|
99
|
-
async connect(_options) {
|
|
100
|
-
const client = this.ensureClient();
|
|
101
|
-
debug("connect requested", { status: client.getStatus() });
|
|
102
|
-
try {
|
|
103
|
-
await client.connect();
|
|
104
|
-
debug("client.connect resolved");
|
|
105
|
-
await client.waitConnected();
|
|
106
|
-
debug("client.waitConnected resolved", { status: client.getStatus() });
|
|
107
|
-
} catch (error) {
|
|
108
|
-
debug("connect failed", error);
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
async close() {
|
|
113
|
-
debug("close requested", {
|
|
114
|
-
docSessions: this.docSessions.size,
|
|
115
|
-
metadataSession: Boolean(this.metadataSession)
|
|
116
|
-
});
|
|
117
|
-
for (const [docId] of this.docSessions) await this.leaveDocSession(docId).catch(() => {});
|
|
118
|
-
this.docSessions.clear();
|
|
119
|
-
await this.teardownMetadataSession().catch(() => {});
|
|
120
|
-
if (this.client) {
|
|
121
|
-
const client = this.client;
|
|
122
|
-
this.client = void 0;
|
|
123
|
-
client.destroy();
|
|
124
|
-
debug("websocket client destroyed");
|
|
125
|
-
}
|
|
126
|
-
debug("close completed");
|
|
127
|
-
}
|
|
128
|
-
isConnected() {
|
|
129
|
-
return this.client?.getStatus() === "connected";
|
|
130
|
-
}
|
|
131
|
-
async syncMeta(flock, options) {
|
|
132
|
-
if (!this.options.metadataRoomId) {
|
|
133
|
-
debug("syncMeta skipped; metadata room not configured");
|
|
134
|
-
return { ok: true };
|
|
135
|
-
}
|
|
136
|
-
debug("syncMeta requested", { roomId: this.options.metadataRoomId });
|
|
137
|
-
try {
|
|
138
|
-
await withTimeout((await this.ensureMetadataSession(flock, {
|
|
139
|
-
roomId: this.options.metadataRoomId,
|
|
140
|
-
auth: this.options.metadataAuth
|
|
141
|
-
})).firstSynced, options?.timeout);
|
|
142
|
-
debug("syncMeta completed", { roomId: this.options.metadataRoomId });
|
|
143
|
-
return { ok: true };
|
|
144
|
-
} catch (error) {
|
|
145
|
-
debug("syncMeta failed", error);
|
|
146
|
-
return { ok: false };
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
joinMetaRoom(flock, params) {
|
|
150
|
-
const fallback = this.options.metadataRoomId ?? "";
|
|
151
|
-
const roomId = normalizeRoomId(params?.roomId, fallback);
|
|
152
|
-
if (!roomId) throw new Error("Metadata room id not configured");
|
|
153
|
-
const auth = params?.auth ?? this.options.metadataAuth;
|
|
154
|
-
debug("joinMetaRoom requested", {
|
|
155
|
-
roomId,
|
|
156
|
-
hasAuth: Boolean(auth && auth.length)
|
|
157
|
-
});
|
|
158
|
-
const ensure = this.ensureMetadataSession(flock, {
|
|
159
|
-
roomId,
|
|
160
|
-
auth
|
|
161
|
-
});
|
|
162
|
-
const firstSynced = ensure.then((session) => session.firstSynced);
|
|
163
|
-
const getConnected = () => this.isConnected();
|
|
164
|
-
const subscription = {
|
|
165
|
-
unsubscribe: () => {
|
|
166
|
-
ensure.then((session) => {
|
|
167
|
-
session.refCount = Math.max(0, session.refCount - 1);
|
|
168
|
-
debug("metadata session refCount decremented", {
|
|
169
|
-
roomId: session.roomId,
|
|
170
|
-
refCount: session.refCount
|
|
171
|
-
});
|
|
172
|
-
if (session.refCount === 0) {
|
|
173
|
-
debug("tearing down metadata session due to refCount=0", { roomId: session.roomId });
|
|
174
|
-
this.teardownMetadataSession(session).catch(() => {});
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
},
|
|
178
|
-
firstSyncedWithRemote: firstSynced,
|
|
179
|
-
get connected() {
|
|
180
|
-
return getConnected();
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
ensure.then((session) => {
|
|
184
|
-
session.refCount += 1;
|
|
185
|
-
debug("metadata session refCount incremented", {
|
|
186
|
-
roomId: session.roomId,
|
|
187
|
-
refCount: session.refCount
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
return subscription;
|
|
191
|
-
}
|
|
192
|
-
async syncDoc(docId, doc, options) {
|
|
193
|
-
debug("syncDoc requested", { docId });
|
|
194
|
-
try {
|
|
195
|
-
const session = await this.ensureDocSession(docId, doc, {});
|
|
196
|
-
await withTimeout(session.firstSynced, options?.timeout);
|
|
197
|
-
debug("syncDoc completed", {
|
|
198
|
-
docId,
|
|
199
|
-
roomId: session.roomId
|
|
200
|
-
});
|
|
201
|
-
return { ok: true };
|
|
202
|
-
} catch (error) {
|
|
203
|
-
debug("syncDoc failed", {
|
|
204
|
-
docId,
|
|
205
|
-
error
|
|
206
|
-
});
|
|
207
|
-
return { ok: false };
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
joinDocRoom(docId, doc, params) {
|
|
211
|
-
debug("joinDocRoom requested", {
|
|
212
|
-
docId,
|
|
213
|
-
roomParamType: params?.roomId ? typeof params.roomId === "string" ? "string" : "uint8array" : void 0,
|
|
214
|
-
hasAuthOverride: Boolean(params?.auth && params.auth.length)
|
|
215
|
-
});
|
|
216
|
-
const ensure = this.ensureDocSession(docId, doc, params ?? {});
|
|
217
|
-
const firstSynced = ensure.then((session) => session.firstSynced);
|
|
218
|
-
const getConnected = () => this.isConnected();
|
|
219
|
-
const subscription = {
|
|
220
|
-
unsubscribe: () => {
|
|
221
|
-
ensure.then((session) => {
|
|
222
|
-
session.refCount = Math.max(0, session.refCount - 1);
|
|
223
|
-
debug("doc session refCount decremented", {
|
|
224
|
-
docId,
|
|
225
|
-
roomId: session.roomId,
|
|
226
|
-
refCount: session.refCount
|
|
227
|
-
});
|
|
228
|
-
if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
|
|
229
|
-
});
|
|
230
|
-
},
|
|
231
|
-
firstSyncedWithRemote: firstSynced,
|
|
232
|
-
get connected() {
|
|
233
|
-
return getConnected();
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
ensure.then((session) => {
|
|
237
|
-
session.refCount += 1;
|
|
238
|
-
debug("doc session refCount incremented", {
|
|
239
|
-
docId,
|
|
240
|
-
roomId: session.roomId,
|
|
241
|
-
refCount: session.refCount
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
return subscription;
|
|
245
|
-
}
|
|
246
|
-
ensureClient() {
|
|
247
|
-
if (this.client) {
|
|
248
|
-
debug("reusing websocket client", { status: this.client.getStatus() });
|
|
249
|
-
return this.client;
|
|
250
|
-
}
|
|
251
|
-
const { url, client: clientOptions } = this.options;
|
|
252
|
-
debug("creating websocket client", {
|
|
253
|
-
url,
|
|
254
|
-
clientOptionsKeys: clientOptions ? Object.keys(clientOptions) : []
|
|
255
|
-
});
|
|
256
|
-
const client = new LoroWebsocketClient({
|
|
257
|
-
url,
|
|
258
|
-
...clientOptions
|
|
259
|
-
});
|
|
260
|
-
this.client = client;
|
|
261
|
-
return client;
|
|
262
|
-
}
|
|
263
|
-
async ensureMetadataSession(flock, params) {
|
|
264
|
-
debug("ensureMetadataSession invoked", {
|
|
265
|
-
roomId: params.roomId,
|
|
266
|
-
hasAuth: Boolean(params.auth && params.auth.length)
|
|
267
|
-
});
|
|
268
|
-
const client = this.ensureClient();
|
|
269
|
-
await client.waitConnected();
|
|
270
|
-
debug("websocket client ready for metadata session", { status: client.getStatus() });
|
|
271
|
-
if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) {
|
|
272
|
-
debug("reusing metadata session", {
|
|
273
|
-
roomId: this.metadataSession.roomId,
|
|
274
|
-
refCount: this.metadataSession.refCount
|
|
275
|
-
});
|
|
276
|
-
return this.metadataSession;
|
|
277
|
-
}
|
|
278
|
-
if (this.metadataSession) {
|
|
279
|
-
debug("tearing down previous metadata session", { roomId: this.metadataSession.roomId });
|
|
280
|
-
await this.teardownMetadataSession(this.metadataSession).catch(() => {});
|
|
281
|
-
}
|
|
282
|
-
const configuredType = this.options.metadataCrdtType;
|
|
283
|
-
if (configuredType && configuredType !== CrdtType.Flock) throw new Error(`metadataCrdtType must be ${CrdtType.Flock} when syncing Flock metadata`);
|
|
284
|
-
const adaptor = createRepoFlockAdaptorFromDoc(flock, this.options.metadataAdaptorConfig ?? {});
|
|
285
|
-
debug("joining metadata room", {
|
|
286
|
-
roomId: params.roomId,
|
|
287
|
-
hasAuth: Boolean(params.auth && params.auth.length)
|
|
288
|
-
});
|
|
289
|
-
const room = await client.join({
|
|
290
|
-
roomId: params.roomId,
|
|
291
|
-
crdtAdaptor: adaptor,
|
|
292
|
-
auth: params.auth
|
|
293
|
-
});
|
|
294
|
-
const firstSynced = room.waitForReachingServerVersion();
|
|
295
|
-
firstSynced.then(() => {
|
|
296
|
-
debug("metadata session firstSynced resolved", { roomId: params.roomId });
|
|
297
|
-
}, (error) => {
|
|
298
|
-
debug("metadata session firstSynced rejected", {
|
|
299
|
-
roomId: params.roomId,
|
|
300
|
-
error
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
const session = {
|
|
304
|
-
adaptor,
|
|
305
|
-
room,
|
|
306
|
-
firstSynced,
|
|
307
|
-
flock,
|
|
308
|
-
roomId: params.roomId,
|
|
309
|
-
auth: params.auth,
|
|
310
|
-
refCount: 0
|
|
311
|
-
};
|
|
312
|
-
this.metadataSession = session;
|
|
313
|
-
return session;
|
|
314
|
-
}
|
|
315
|
-
async teardownMetadataSession(session) {
|
|
316
|
-
const target = session ?? this.metadataSession;
|
|
317
|
-
if (!target) return;
|
|
318
|
-
debug("teardownMetadataSession invoked", { roomId: target.roomId });
|
|
319
|
-
if (this.metadataSession === target) this.metadataSession = void 0;
|
|
320
|
-
const { adaptor, room } = target;
|
|
321
|
-
try {
|
|
322
|
-
await room.leave();
|
|
323
|
-
debug("metadata room left", { roomId: target.roomId });
|
|
324
|
-
} catch (error) {
|
|
325
|
-
debug("metadata room leave failed; destroying", {
|
|
326
|
-
roomId: target.roomId,
|
|
327
|
-
error
|
|
328
|
-
});
|
|
329
|
-
await room.destroy().catch(() => {});
|
|
330
|
-
}
|
|
331
|
-
adaptor.destroy();
|
|
332
|
-
debug("metadata session destroyed", { roomId: target.roomId });
|
|
333
|
-
}
|
|
334
|
-
async ensureDocSession(docId, doc, params) {
|
|
335
|
-
debug("ensureDocSession invoked", { docId });
|
|
336
|
-
const client = this.ensureClient();
|
|
337
|
-
await client.waitConnected();
|
|
338
|
-
debug("websocket client ready for doc session", {
|
|
339
|
-
docId,
|
|
340
|
-
status: client.getStatus()
|
|
341
|
-
});
|
|
342
|
-
const existing = this.docSessions.get(docId);
|
|
343
|
-
const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
|
|
344
|
-
const roomId = normalizeRoomId(params.roomId, derivedRoomId);
|
|
345
|
-
const auth = params.auth ?? this.options.docAuth?.(docId);
|
|
346
|
-
debug("doc session params resolved", {
|
|
347
|
-
docId,
|
|
348
|
-
roomId,
|
|
349
|
-
hasAuth: Boolean(auth && auth.length)
|
|
350
|
-
});
|
|
351
|
-
if (existing && existing.doc === doc && existing.roomId === roomId) {
|
|
352
|
-
debug("reusing doc session", {
|
|
353
|
-
docId,
|
|
354
|
-
roomId,
|
|
355
|
-
refCount: existing.refCount
|
|
356
|
-
});
|
|
357
|
-
return existing;
|
|
358
|
-
}
|
|
359
|
-
if (existing) {
|
|
360
|
-
debug("doc session mismatch; leaving existing session", {
|
|
361
|
-
docId,
|
|
362
|
-
previousRoomId: existing.roomId,
|
|
363
|
-
nextRoomId: roomId
|
|
364
|
-
});
|
|
365
|
-
await this.leaveDocSession(docId).catch(() => {});
|
|
366
|
-
}
|
|
367
|
-
const adaptor = new LoroAdaptor(doc);
|
|
368
|
-
debug("joining doc room", {
|
|
369
|
-
docId,
|
|
370
|
-
roomId,
|
|
371
|
-
hasAuth: Boolean(auth && auth.length)
|
|
372
|
-
});
|
|
373
|
-
const room = await client.join({
|
|
374
|
-
roomId,
|
|
375
|
-
crdtAdaptor: adaptor,
|
|
376
|
-
auth
|
|
377
|
-
});
|
|
378
|
-
const firstSynced = room.waitForReachingServerVersion();
|
|
379
|
-
firstSynced.then(() => {
|
|
380
|
-
debug("doc session firstSynced resolved", {
|
|
381
|
-
docId,
|
|
382
|
-
roomId
|
|
383
|
-
});
|
|
384
|
-
}, (error) => {
|
|
385
|
-
debug("doc session firstSynced rejected", {
|
|
386
|
-
docId,
|
|
387
|
-
roomId,
|
|
388
|
-
error
|
|
389
|
-
});
|
|
390
|
-
});
|
|
391
|
-
const session = {
|
|
392
|
-
adaptor,
|
|
393
|
-
room,
|
|
394
|
-
firstSynced,
|
|
395
|
-
doc,
|
|
396
|
-
roomId,
|
|
397
|
-
refCount: 0
|
|
398
|
-
};
|
|
399
|
-
this.docSessions.set(docId, session);
|
|
400
|
-
return session;
|
|
401
|
-
}
|
|
402
|
-
async leaveDocSession(docId) {
|
|
403
|
-
const session = this.docSessions.get(docId);
|
|
404
|
-
if (!session) {
|
|
405
|
-
debug("leaveDocSession invoked but no session found", { docId });
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
this.docSessions.delete(docId);
|
|
409
|
-
debug("leaving doc session", {
|
|
410
|
-
docId,
|
|
411
|
-
roomId: session.roomId
|
|
412
|
-
});
|
|
413
|
-
try {
|
|
414
|
-
await session.room.leave();
|
|
415
|
-
debug("doc room left", {
|
|
416
|
-
docId,
|
|
417
|
-
roomId: session.roomId
|
|
418
|
-
});
|
|
419
|
-
} catch (error) {
|
|
420
|
-
debug("doc room leave failed; destroying", {
|
|
421
|
-
docId,
|
|
422
|
-
roomId: session.roomId,
|
|
423
|
-
error
|
|
424
|
-
});
|
|
425
|
-
await session.room.destroy().catch(() => {});
|
|
426
|
-
}
|
|
427
|
-
session.adaptor.destroy();
|
|
428
|
-
debug("doc session destroyed", {
|
|
429
|
-
docId,
|
|
430
|
-
roomId: session.roomId
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
//#endregion
|
|
436
|
-
//#region src/transport/broadcast-channel.ts
|
|
437
|
-
function deferred() {
|
|
438
|
-
let resolve;
|
|
439
|
-
return {
|
|
440
|
-
promise: new Promise((res) => {
|
|
441
|
-
resolve = res;
|
|
442
|
-
}),
|
|
443
|
-
resolve
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
function randomInstanceId() {
|
|
447
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
448
|
-
return Math.random().toString(36).slice(2);
|
|
449
|
-
}
|
|
450
|
-
function ensureBroadcastChannel() {
|
|
451
|
-
if (typeof BroadcastChannel === "undefined") throw new Error("BroadcastChannel API is not available in this environment");
|
|
452
|
-
return BroadcastChannel;
|
|
453
|
-
}
|
|
454
|
-
function encodeDocChannelId(docId) {
|
|
455
|
-
try {
|
|
456
|
-
return encodeURIComponent(docId);
|
|
457
|
-
} catch {
|
|
458
|
-
return docId.replace(/[^a-z0-9_-]/gi, "_");
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
function postChannelMessage(channel, message) {
|
|
462
|
-
channel.postMessage(message);
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* TransportAdapter that relies on the BroadcastChannel API to fan out metadata
|
|
466
|
-
* and document updates between browser tabs within the same origin.
|
|
467
|
-
*/
|
|
468
|
-
var BroadcastChannelTransportAdapter = class {
|
|
469
|
-
instanceId = randomInstanceId();
|
|
470
|
-
namespace;
|
|
471
|
-
metaChannelName;
|
|
472
|
-
connected = false;
|
|
473
|
-
metaState;
|
|
474
|
-
docStates = /* @__PURE__ */ new Map();
|
|
475
|
-
constructor(options = {}) {
|
|
476
|
-
ensureBroadcastChannel();
|
|
477
|
-
this.namespace = options.namespace ?? "loro-repo";
|
|
478
|
-
this.metaChannelName = options.metaChannelName ?? `${this.namespace}-meta`;
|
|
479
|
-
}
|
|
480
|
-
async connect() {
|
|
481
|
-
this.connected = true;
|
|
482
|
-
}
|
|
483
|
-
async close() {
|
|
484
|
-
this.connected = false;
|
|
485
|
-
if (this.metaState) {
|
|
486
|
-
for (const entry of this.metaState.listeners) entry.unsubscribe();
|
|
487
|
-
this.metaState.channel.close();
|
|
488
|
-
this.metaState = void 0;
|
|
489
|
-
}
|
|
490
|
-
for (const [docId] of this.docStates) this.teardownDocChannel(docId);
|
|
491
|
-
this.docStates.clear();
|
|
492
|
-
}
|
|
493
|
-
isConnected() {
|
|
494
|
-
return this.connected;
|
|
495
|
-
}
|
|
496
|
-
async syncMeta(flock, _options) {
|
|
497
|
-
const subscription = this.joinMetaRoom(flock);
|
|
498
|
-
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
499
|
-
await subscription.firstSyncedWithRemote;
|
|
500
|
-
subscription.unsubscribe();
|
|
501
|
-
return { ok: true };
|
|
502
|
-
}
|
|
503
|
-
joinMetaRoom(flock, _params) {
|
|
504
|
-
const state = this.ensureMetaChannel();
|
|
505
|
-
const { promise, resolve } = deferred();
|
|
506
|
-
const listener = {
|
|
507
|
-
flock,
|
|
508
|
-
muted: false,
|
|
509
|
-
unsubscribe: flock.subscribe(() => {
|
|
510
|
-
if (listener.muted) return;
|
|
511
|
-
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
512
|
-
postChannelMessage(state.channel, {
|
|
513
|
-
kind: "meta-export",
|
|
514
|
-
from: this.instanceId,
|
|
515
|
-
bundle
|
|
516
|
-
});
|
|
517
|
-
});
|
|
518
|
-
}),
|
|
519
|
-
resolveFirst: resolve,
|
|
520
|
-
firstSynced: promise
|
|
521
|
-
};
|
|
522
|
-
state.listeners.add(listener);
|
|
523
|
-
postChannelMessage(state.channel, {
|
|
524
|
-
kind: "meta-request",
|
|
525
|
-
from: this.instanceId
|
|
526
|
-
});
|
|
527
|
-
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
528
|
-
postChannelMessage(state.channel, {
|
|
529
|
-
kind: "meta-export",
|
|
530
|
-
from: this.instanceId,
|
|
531
|
-
bundle
|
|
532
|
-
});
|
|
533
|
-
});
|
|
534
|
-
queueMicrotask(() => resolve());
|
|
535
|
-
return {
|
|
536
|
-
unsubscribe: () => {
|
|
537
|
-
listener.unsubscribe();
|
|
538
|
-
state.listeners.delete(listener);
|
|
539
|
-
if (!state.listeners.size) {
|
|
540
|
-
state.channel.removeEventListener("message", state.onMessage);
|
|
541
|
-
state.channel.close();
|
|
542
|
-
this.metaState = void 0;
|
|
543
|
-
}
|
|
544
|
-
},
|
|
545
|
-
firstSyncedWithRemote: listener.firstSynced,
|
|
546
|
-
get connected() {
|
|
547
|
-
return true;
|
|
548
|
-
}
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
async syncDoc(docId, doc, _options) {
|
|
552
|
-
const subscription = this.joinDocRoom(docId, doc);
|
|
553
|
-
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
554
|
-
await subscription.firstSyncedWithRemote;
|
|
555
|
-
subscription.unsubscribe();
|
|
556
|
-
return { ok: true };
|
|
557
|
-
}
|
|
558
|
-
joinDocRoom(docId, doc, _params) {
|
|
559
|
-
const state = this.ensureDocChannel(docId);
|
|
560
|
-
const { promise, resolve } = deferred();
|
|
561
|
-
const listener = {
|
|
562
|
-
doc,
|
|
563
|
-
muted: false,
|
|
564
|
-
unsubscribe: doc.subscribe(() => {
|
|
565
|
-
if (listener.muted) return;
|
|
566
|
-
const payload = doc.export({ mode: "update" });
|
|
567
|
-
postChannelMessage(state.channel, {
|
|
568
|
-
kind: "doc-update",
|
|
569
|
-
docId,
|
|
570
|
-
from: this.instanceId,
|
|
571
|
-
mode: "update",
|
|
572
|
-
payload
|
|
573
|
-
});
|
|
574
|
-
}),
|
|
575
|
-
resolveFirst: resolve,
|
|
576
|
-
firstSynced: promise
|
|
577
|
-
};
|
|
578
|
-
state.listeners.add(listener);
|
|
579
|
-
postChannelMessage(state.channel, {
|
|
580
|
-
kind: "doc-request",
|
|
581
|
-
docId,
|
|
582
|
-
from: this.instanceId
|
|
583
|
-
});
|
|
584
|
-
postChannelMessage(state.channel, {
|
|
585
|
-
kind: "doc-update",
|
|
586
|
-
docId,
|
|
587
|
-
from: this.instanceId,
|
|
588
|
-
mode: "snapshot",
|
|
589
|
-
payload: doc.export({ mode: "snapshot" })
|
|
590
|
-
});
|
|
591
|
-
queueMicrotask(() => resolve());
|
|
592
|
-
return {
|
|
593
|
-
unsubscribe: () => {
|
|
594
|
-
listener.unsubscribe();
|
|
595
|
-
state.listeners.delete(listener);
|
|
596
|
-
if (!state.listeners.size) this.teardownDocChannel(docId);
|
|
597
|
-
},
|
|
598
|
-
firstSyncedWithRemote: listener.firstSynced,
|
|
599
|
-
get connected() {
|
|
600
|
-
return true;
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
ensureMetaChannel() {
|
|
605
|
-
if (this.metaState) return this.metaState;
|
|
606
|
-
const channel = new (ensureBroadcastChannel())(this.metaChannelName);
|
|
607
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
608
|
-
const onMessage = (event) => {
|
|
609
|
-
const message = event.data;
|
|
610
|
-
if (!message || message.from === this.instanceId) return;
|
|
611
|
-
if (message.kind === "meta-export") for (const entry of listeners) {
|
|
612
|
-
entry.muted = true;
|
|
613
|
-
entry.flock.importJson(message.bundle);
|
|
614
|
-
entry.muted = false;
|
|
615
|
-
entry.resolveFirst();
|
|
616
|
-
}
|
|
617
|
-
else if (message.kind === "meta-request") {
|
|
618
|
-
const first = listeners.values().next().value;
|
|
619
|
-
if (!first) return;
|
|
620
|
-
Promise.resolve(first.flock.exportJson()).then((bundle) => {
|
|
621
|
-
postChannelMessage(channel, {
|
|
622
|
-
kind: "meta-export",
|
|
623
|
-
from: this.instanceId,
|
|
624
|
-
bundle
|
|
625
|
-
});
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
};
|
|
629
|
-
channel.addEventListener("message", onMessage);
|
|
630
|
-
this.metaState = {
|
|
631
|
-
channel,
|
|
632
|
-
listeners,
|
|
633
|
-
onMessage
|
|
634
|
-
};
|
|
635
|
-
return this.metaState;
|
|
636
|
-
}
|
|
637
|
-
ensureDocChannel(docId) {
|
|
638
|
-
const existing = this.docStates.get(docId);
|
|
639
|
-
if (existing) return existing;
|
|
640
|
-
const channel = new (ensureBroadcastChannel())(`${this.namespace}-doc-${encodeDocChannelId(docId)}`);
|
|
641
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
642
|
-
const onMessage = (event) => {
|
|
643
|
-
const message = event.data;
|
|
644
|
-
if (!message || message.from === this.instanceId) return;
|
|
645
|
-
if (message.kind === "doc-update") for (const entry of listeners) {
|
|
646
|
-
entry.muted = true;
|
|
647
|
-
entry.doc.import(message.payload);
|
|
648
|
-
entry.muted = false;
|
|
649
|
-
entry.resolveFirst();
|
|
650
|
-
}
|
|
651
|
-
else if (message.kind === "doc-request") {
|
|
652
|
-
const first = listeners.values().next().value;
|
|
653
|
-
if (!first) return;
|
|
654
|
-
const payload = message.docId === docId ? first.doc.export({ mode: "snapshot" }) : void 0;
|
|
655
|
-
if (!payload) return;
|
|
656
|
-
postChannelMessage(channel, {
|
|
657
|
-
kind: "doc-update",
|
|
658
|
-
docId,
|
|
659
|
-
from: this.instanceId,
|
|
660
|
-
mode: "snapshot",
|
|
661
|
-
payload
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
};
|
|
665
|
-
channel.addEventListener("message", onMessage);
|
|
666
|
-
const state = {
|
|
667
|
-
channel,
|
|
668
|
-
listeners,
|
|
669
|
-
onMessage
|
|
670
|
-
};
|
|
671
|
-
this.docStates.set(docId, state);
|
|
672
|
-
return state;
|
|
673
|
-
}
|
|
674
|
-
teardownDocChannel(docId) {
|
|
675
|
-
const state = this.docStates.get(docId);
|
|
676
|
-
if (!state) return;
|
|
677
|
-
for (const entry of state.listeners) entry.unsubscribe();
|
|
678
|
-
state.channel.removeEventListener("message", state.onMessage);
|
|
679
|
-
state.channel.close();
|
|
680
|
-
this.docStates.delete(docId);
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
//#endregion
|
|
685
|
-
//#region src/storage/indexeddb.ts
|
|
686
|
-
const DEFAULT_DB_NAME = "loro-repo";
|
|
687
|
-
const DEFAULT_DB_VERSION = 1;
|
|
688
|
-
const DEFAULT_DOC_STORE = "docs";
|
|
689
|
-
const DEFAULT_META_STORE = "meta";
|
|
690
|
-
const DEFAULT_ASSET_STORE = "assets";
|
|
691
|
-
const DEFAULT_DOC_UPDATE_STORE = "doc-updates";
|
|
692
|
-
const DEFAULT_META_KEY = "snapshot";
|
|
693
|
-
const textDecoder$1 = new TextDecoder();
|
|
694
|
-
function describeUnknown(cause) {
|
|
695
|
-
if (typeof cause === "string") return cause;
|
|
696
|
-
if (typeof cause === "number" || typeof cause === "boolean") return String(cause);
|
|
697
|
-
if (typeof cause === "bigint") return cause.toString();
|
|
698
|
-
if (typeof cause === "symbol") return cause.description ?? cause.toString();
|
|
699
|
-
if (typeof cause === "function") return `[function ${cause.name ?? "anonymous"}]`;
|
|
700
|
-
if (cause && typeof cause === "object") try {
|
|
701
|
-
return JSON.stringify(cause);
|
|
702
|
-
} catch {
|
|
703
|
-
return "[object]";
|
|
704
|
-
}
|
|
705
|
-
return String(cause);
|
|
706
|
-
}
|
|
707
|
-
var IndexedDBStorageAdaptor = class {
|
|
708
|
-
idb;
|
|
709
|
-
dbName;
|
|
710
|
-
version;
|
|
711
|
-
docStore;
|
|
712
|
-
docUpdateStore;
|
|
713
|
-
metaStore;
|
|
714
|
-
assetStore;
|
|
715
|
-
metaKey;
|
|
716
|
-
dbPromise;
|
|
717
|
-
closed = false;
|
|
718
|
-
constructor(options = {}) {
|
|
719
|
-
const idbFactory = globalThis.indexedDB;
|
|
720
|
-
if (!idbFactory) throw new Error("IndexedDB is not available in this environment");
|
|
721
|
-
this.idb = idbFactory;
|
|
722
|
-
this.dbName = options.dbName ?? DEFAULT_DB_NAME;
|
|
723
|
-
this.version = options.version ?? DEFAULT_DB_VERSION;
|
|
724
|
-
this.docStore = options.docStoreName ?? DEFAULT_DOC_STORE;
|
|
725
|
-
this.docUpdateStore = options.docUpdateStoreName ?? DEFAULT_DOC_UPDATE_STORE;
|
|
726
|
-
this.metaStore = options.metaStoreName ?? DEFAULT_META_STORE;
|
|
727
|
-
this.assetStore = options.assetStoreName ?? DEFAULT_ASSET_STORE;
|
|
728
|
-
this.metaKey = options.metaKey ?? DEFAULT_META_KEY;
|
|
729
|
-
}
|
|
730
|
-
async save(payload) {
|
|
731
|
-
const db = await this.ensureDb();
|
|
732
|
-
switch (payload.type) {
|
|
733
|
-
case "doc-snapshot": {
|
|
734
|
-
const snapshot = payload.snapshot.slice();
|
|
735
|
-
await this.storeMergedSnapshot(db, payload.docId, snapshot);
|
|
736
|
-
break;
|
|
737
|
-
}
|
|
738
|
-
case "doc-update": {
|
|
739
|
-
const update = payload.update.slice();
|
|
740
|
-
await this.appendDocUpdate(db, payload.docId, update);
|
|
741
|
-
break;
|
|
742
|
-
}
|
|
743
|
-
case "asset": {
|
|
744
|
-
const bytes = payload.data.slice();
|
|
745
|
-
await this.putBinary(db, this.assetStore, payload.assetId, bytes);
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
case "meta": {
|
|
749
|
-
const bytes = payload.update.slice();
|
|
750
|
-
await this.putBinary(db, this.metaStore, this.metaKey, bytes);
|
|
751
|
-
break;
|
|
752
|
-
}
|
|
753
|
-
default: throw new Error("Unsupported storage payload type");
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
async deleteAsset(assetId) {
|
|
757
|
-
const db = await this.ensureDb();
|
|
758
|
-
await this.deleteKey(db, this.assetStore, assetId);
|
|
759
|
-
}
|
|
760
|
-
async loadDoc(docId) {
|
|
761
|
-
const db = await this.ensureDb();
|
|
762
|
-
const snapshot = await this.getBinaryFromDb(db, this.docStore, docId);
|
|
763
|
-
const pendingUpdates = await this.getDocUpdates(db, docId);
|
|
764
|
-
if (!snapshot && pendingUpdates.length === 0) return;
|
|
765
|
-
let doc;
|
|
766
|
-
try {
|
|
767
|
-
doc = snapshot ? LoroDoc.fromSnapshot(snapshot) : new LoroDoc();
|
|
768
|
-
} catch (error) {
|
|
769
|
-
throw this.createError(`Failed to hydrate document snapshot for "${docId}"`, error);
|
|
770
|
-
}
|
|
771
|
-
let appliedUpdates = false;
|
|
772
|
-
for (const update of pendingUpdates) try {
|
|
773
|
-
doc.import(update);
|
|
774
|
-
appliedUpdates = true;
|
|
775
|
-
} catch (error) {
|
|
776
|
-
throw this.createError(`Failed to apply queued document update for "${docId}"`, error);
|
|
777
|
-
}
|
|
778
|
-
if (appliedUpdates) {
|
|
779
|
-
let consolidated;
|
|
780
|
-
try {
|
|
781
|
-
consolidated = doc.export({ mode: "snapshot" });
|
|
782
|
-
} catch (error) {
|
|
783
|
-
throw this.createError(`Failed to export consolidated snapshot for "${docId}"`, error);
|
|
784
|
-
}
|
|
785
|
-
await this.writeSnapshot(db, docId, consolidated);
|
|
786
|
-
await this.clearDocUpdates(db, docId);
|
|
787
|
-
}
|
|
788
|
-
return doc;
|
|
789
|
-
}
|
|
790
|
-
async loadMeta() {
|
|
791
|
-
const bytes = await this.getBinary(this.metaStore, this.metaKey);
|
|
792
|
-
if (!bytes) return void 0;
|
|
793
|
-
try {
|
|
794
|
-
const json = textDecoder$1.decode(bytes);
|
|
795
|
-
const bundle = JSON.parse(json);
|
|
796
|
-
const flock = new Flock();
|
|
797
|
-
flock.importJson(bundle);
|
|
798
|
-
return flock;
|
|
799
|
-
} catch (error) {
|
|
800
|
-
throw this.createError("Failed to hydrate metadata snapshot", error);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
async loadAsset(assetId) {
|
|
804
|
-
return await this.getBinary(this.assetStore, assetId) ?? void 0;
|
|
805
|
-
}
|
|
806
|
-
async close() {
|
|
807
|
-
this.closed = true;
|
|
808
|
-
const db = await this.dbPromise;
|
|
809
|
-
if (db) db.close();
|
|
810
|
-
this.dbPromise = void 0;
|
|
811
|
-
}
|
|
812
|
-
async ensureDb() {
|
|
813
|
-
if (this.closed) throw new Error("IndexedDBStorageAdaptor has been closed");
|
|
814
|
-
if (!this.dbPromise) this.dbPromise = new Promise((resolve, reject) => {
|
|
815
|
-
const request = this.idb.open(this.dbName, this.version);
|
|
816
|
-
request.addEventListener("upgradeneeded", () => {
|
|
817
|
-
const db = request.result;
|
|
818
|
-
this.ensureStore(db, this.docStore);
|
|
819
|
-
this.ensureStore(db, this.docUpdateStore);
|
|
820
|
-
this.ensureStore(db, this.metaStore);
|
|
821
|
-
this.ensureStore(db, this.assetStore);
|
|
822
|
-
});
|
|
823
|
-
request.addEventListener("success", () => resolve(request.result), { once: true });
|
|
824
|
-
request.addEventListener("error", () => {
|
|
825
|
-
reject(this.createError(`Failed to open IndexedDB database "${this.dbName}"`, request.error));
|
|
826
|
-
}, { once: true });
|
|
827
|
-
});
|
|
828
|
-
return this.dbPromise;
|
|
829
|
-
}
|
|
830
|
-
ensureStore(db, storeName) {
|
|
831
|
-
const names = db.objectStoreNames;
|
|
832
|
-
if (this.storeExists(names, storeName)) return;
|
|
833
|
-
db.createObjectStore(storeName);
|
|
834
|
-
}
|
|
835
|
-
storeExists(names, storeName) {
|
|
836
|
-
if (typeof names.contains === "function") return names.contains(storeName);
|
|
837
|
-
const length = names.length ?? 0;
|
|
838
|
-
for (let index = 0; index < length; index += 1) if (names.item?.(index) === storeName) return true;
|
|
839
|
-
return false;
|
|
840
|
-
}
|
|
841
|
-
async storeMergedSnapshot(db, docId, incoming) {
|
|
842
|
-
await this.runInTransaction(db, this.docStore, "readwrite", async (store) => {
|
|
843
|
-
const existingRaw = await this.wrapRequest(store.get(docId), "read");
|
|
844
|
-
const existing = await this.normalizeBinary(existingRaw);
|
|
845
|
-
const merged = this.mergeSnapshots(docId, existing, incoming);
|
|
846
|
-
await this.wrapRequest(store.put(merged, docId), "write");
|
|
847
|
-
});
|
|
848
|
-
}
|
|
849
|
-
mergeSnapshots(docId, existing, incoming) {
|
|
850
|
-
try {
|
|
851
|
-
const doc = existing ? LoroDoc.fromSnapshot(existing) : new LoroDoc();
|
|
852
|
-
doc.import(incoming);
|
|
853
|
-
return doc.export({ mode: "snapshot" });
|
|
854
|
-
} catch (error) {
|
|
855
|
-
throw this.createError(`Failed to merge snapshot for "${docId}"`, error);
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
async appendDocUpdate(db, docId, update) {
|
|
859
|
-
await this.runInTransaction(db, this.docUpdateStore, "readwrite", async (store) => {
|
|
860
|
-
const raw = await this.wrapRequest(store.get(docId), "read");
|
|
861
|
-
const queue = await this.normalizeUpdateQueue(raw);
|
|
862
|
-
queue.push(update.slice());
|
|
863
|
-
await this.wrapRequest(store.put({ updates: queue }, docId), "write");
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
async getDocUpdates(db, docId) {
|
|
867
|
-
const raw = await this.runInTransaction(db, this.docUpdateStore, "readonly", (store) => this.wrapRequest(store.get(docId), "read"));
|
|
868
|
-
return this.normalizeUpdateQueue(raw);
|
|
869
|
-
}
|
|
870
|
-
async clearDocUpdates(db, docId) {
|
|
871
|
-
await this.runInTransaction(db, this.docUpdateStore, "readwrite", (store) => this.wrapRequest(store.delete(docId), "delete"));
|
|
872
|
-
}
|
|
873
|
-
async writeSnapshot(db, docId, snapshot) {
|
|
874
|
-
await this.putBinary(db, this.docStore, docId, snapshot.slice());
|
|
875
|
-
}
|
|
876
|
-
async getBinaryFromDb(db, storeName, key) {
|
|
877
|
-
const value = await this.runInTransaction(db, storeName, "readonly", (store) => this.wrapRequest(store.get(key), "read"));
|
|
878
|
-
return this.normalizeBinary(value);
|
|
879
|
-
}
|
|
880
|
-
async normalizeUpdateQueue(value) {
|
|
881
|
-
if (value == null) return [];
|
|
882
|
-
const list = Array.isArray(value) ? value : typeof value === "object" && value !== null ? value.updates : void 0;
|
|
883
|
-
if (!Array.isArray(list)) return [];
|
|
884
|
-
const queue = [];
|
|
885
|
-
for (const entry of list) {
|
|
886
|
-
const bytes = await this.normalizeBinary(entry);
|
|
887
|
-
if (bytes) queue.push(bytes);
|
|
888
|
-
}
|
|
889
|
-
return queue;
|
|
890
|
-
}
|
|
891
|
-
async putBinary(db, storeName, key, value) {
|
|
892
|
-
await this.runInTransaction(db, storeName, "readwrite", (store) => this.wrapRequest(store.put(value, key), "write"));
|
|
893
|
-
}
|
|
894
|
-
async deleteKey(db, storeName, key) {
|
|
895
|
-
await this.runInTransaction(db, storeName, "readwrite", (store) => this.wrapRequest(store.delete(key), "delete"));
|
|
896
|
-
}
|
|
897
|
-
async getBinary(storeName, key) {
|
|
898
|
-
const db = await this.ensureDb();
|
|
899
|
-
return this.getBinaryFromDb(db, storeName, key);
|
|
900
|
-
}
|
|
901
|
-
runInTransaction(db, storeName, mode, executor) {
|
|
902
|
-
const tx = db.transaction(storeName, mode);
|
|
903
|
-
const store = tx.objectStore(storeName);
|
|
904
|
-
const completion = new Promise((resolve, reject) => {
|
|
905
|
-
tx.addEventListener("complete", () => resolve(), { once: true });
|
|
906
|
-
tx.addEventListener("abort", () => reject(this.createError("IndexedDB transaction aborted", tx.error)), { once: true });
|
|
907
|
-
tx.addEventListener("error", () => reject(this.createError("IndexedDB transaction failed", tx.error)), { once: true });
|
|
908
|
-
});
|
|
909
|
-
return Promise.all([executor(store), completion]).then(([result]) => result);
|
|
910
|
-
}
|
|
911
|
-
wrapRequest(request, action) {
|
|
912
|
-
return new Promise((resolve, reject) => {
|
|
913
|
-
request.addEventListener("success", () => resolve(request.result), { once: true });
|
|
914
|
-
request.addEventListener("error", () => reject(this.createError(`IndexedDB request failed during ${action}`, request.error)), { once: true });
|
|
915
|
-
});
|
|
916
|
-
}
|
|
917
|
-
async normalizeBinary(value) {
|
|
918
|
-
if (value == null) return void 0;
|
|
919
|
-
if (value instanceof Uint8Array) return value.slice();
|
|
920
|
-
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
|
|
921
|
-
if (value instanceof ArrayBuffer) return new Uint8Array(value.slice(0));
|
|
922
|
-
if (typeof value === "object" && value !== null && "arrayBuffer" in value) {
|
|
923
|
-
const candidate = value;
|
|
924
|
-
if (typeof candidate.arrayBuffer === "function") {
|
|
925
|
-
const buffer = await candidate.arrayBuffer();
|
|
926
|
-
return new Uint8Array(buffer);
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
createError(message, cause) {
|
|
931
|
-
if (cause instanceof Error) return new Error(`${message}: ${cause.message}`, { cause });
|
|
932
|
-
if (cause !== void 0 && cause !== null) return /* @__PURE__ */ new Error(`${message}: ${describeUnknown(cause)}`);
|
|
933
|
-
return new Error(message);
|
|
934
|
-
}
|
|
935
|
-
};
|
|
936
|
-
|
|
937
|
-
//#endregion
|
|
938
|
-
//#region src/storage/filesystem.ts
|
|
939
|
-
const textDecoder = new TextDecoder();
|
|
940
|
-
var FileSystemStorageAdaptor = class {
|
|
941
|
-
baseDir;
|
|
942
|
-
docsDir;
|
|
943
|
-
assetsDir;
|
|
944
|
-
metaPath;
|
|
945
|
-
initPromise;
|
|
946
|
-
updateCounter = 0;
|
|
947
|
-
constructor(options = {}) {
|
|
948
|
-
this.baseDir = path.resolve(options.baseDir ?? path.join(process.cwd(), ".loro-repo"));
|
|
949
|
-
this.docsDir = path.join(this.baseDir, options.docsDirName ?? "docs");
|
|
950
|
-
this.assetsDir = path.join(this.baseDir, options.assetsDirName ?? "assets");
|
|
951
|
-
this.metaPath = path.join(this.baseDir, options.metaFileName ?? "meta.json");
|
|
952
|
-
this.initPromise = this.ensureLayout();
|
|
953
|
-
}
|
|
954
|
-
async save(payload) {
|
|
955
|
-
await this.initPromise;
|
|
956
|
-
switch (payload.type) {
|
|
957
|
-
case "doc-snapshot":
|
|
958
|
-
await this.writeDocSnapshot(payload.docId, payload.snapshot);
|
|
959
|
-
return;
|
|
960
|
-
case "doc-update":
|
|
961
|
-
await this.enqueueDocUpdate(payload.docId, payload.update);
|
|
962
|
-
return;
|
|
963
|
-
case "asset":
|
|
964
|
-
await this.writeAsset(payload.assetId, payload.data);
|
|
965
|
-
return;
|
|
966
|
-
case "meta":
|
|
967
|
-
await writeFileAtomic(this.metaPath, payload.update);
|
|
968
|
-
return;
|
|
969
|
-
default: throw new Error(`Unsupported payload type: ${payload.type}`);
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
async deleteAsset(assetId) {
|
|
973
|
-
await this.initPromise;
|
|
974
|
-
await removeIfExists(this.assetPath(assetId));
|
|
975
|
-
}
|
|
976
|
-
async loadDoc(docId) {
|
|
977
|
-
await this.initPromise;
|
|
978
|
-
const snapshotBytes = await readFileIfExists(this.docSnapshotPath(docId));
|
|
979
|
-
const updateDir = this.docUpdatesDir(docId);
|
|
980
|
-
const updateFiles = await listFiles(updateDir);
|
|
981
|
-
if (!snapshotBytes && updateFiles.length === 0) return;
|
|
982
|
-
const doc = snapshotBytes ? LoroDoc.fromSnapshot(snapshotBytes) : new LoroDoc();
|
|
983
|
-
if (updateFiles.length === 0) return doc;
|
|
984
|
-
const updatePaths = updateFiles.map((file) => path.join(updateDir, file));
|
|
985
|
-
for (const updatePath of updatePaths) {
|
|
986
|
-
const update = await readFileIfExists(updatePath);
|
|
987
|
-
if (!update) continue;
|
|
988
|
-
doc.import(update);
|
|
989
|
-
}
|
|
990
|
-
await Promise.all(updatePaths.map((filePath) => removeIfExists(filePath)));
|
|
991
|
-
const consolidated = doc.export({ mode: "snapshot" });
|
|
992
|
-
await this.writeDocSnapshot(docId, consolidated);
|
|
993
|
-
return doc;
|
|
994
|
-
}
|
|
995
|
-
async loadMeta() {
|
|
996
|
-
await this.initPromise;
|
|
997
|
-
const bytes = await readFileIfExists(this.metaPath);
|
|
998
|
-
if (!bytes) return void 0;
|
|
999
|
-
try {
|
|
1000
|
-
const bundle = JSON.parse(textDecoder.decode(bytes));
|
|
1001
|
-
const flock = new Flock();
|
|
1002
|
-
flock.importJson(bundle);
|
|
1003
|
-
return flock;
|
|
1004
|
-
} catch (error) {
|
|
1005
|
-
throw new Error("Failed to hydrate metadata snapshot", { cause: error });
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
async loadAsset(assetId) {
|
|
1009
|
-
await this.initPromise;
|
|
1010
|
-
return readFileIfExists(this.assetPath(assetId));
|
|
1011
|
-
}
|
|
1012
|
-
async ensureLayout() {
|
|
1013
|
-
await Promise.all([
|
|
1014
|
-
ensureDir(this.baseDir),
|
|
1015
|
-
ensureDir(this.docsDir),
|
|
1016
|
-
ensureDir(this.assetsDir)
|
|
1017
|
-
]);
|
|
1018
|
-
}
|
|
1019
|
-
async writeDocSnapshot(docId, snapshot) {
|
|
1020
|
-
await ensureDir(this.docDir(docId));
|
|
1021
|
-
await writeFileAtomic(this.docSnapshotPath(docId), snapshot);
|
|
1022
|
-
}
|
|
1023
|
-
async enqueueDocUpdate(docId, update) {
|
|
1024
|
-
const dir = this.docUpdatesDir(docId);
|
|
1025
|
-
await ensureDir(dir);
|
|
1026
|
-
const counter = this.updateCounter = (this.updateCounter + 1) % 1e6;
|
|
1027
|
-
const fileName = `${Date.now().toString().padStart(13, "0")}-${counter.toString().padStart(6, "0")}.bin`;
|
|
1028
|
-
await writeFileAtomic(path.join(dir, fileName), update);
|
|
1029
|
-
}
|
|
1030
|
-
async writeAsset(assetId, data) {
|
|
1031
|
-
const filePath = this.assetPath(assetId);
|
|
1032
|
-
await ensureDir(path.dirname(filePath));
|
|
1033
|
-
await writeFileAtomic(filePath, data);
|
|
1034
|
-
}
|
|
1035
|
-
docDir(docId) {
|
|
1036
|
-
return path.join(this.docsDir, encodeComponent(docId));
|
|
1037
|
-
}
|
|
1038
|
-
docSnapshotPath(docId) {
|
|
1039
|
-
return path.join(this.docDir(docId), "snapshot.bin");
|
|
1040
|
-
}
|
|
1041
|
-
docUpdatesDir(docId) {
|
|
1042
|
-
return path.join(this.docDir(docId), "updates");
|
|
1043
|
-
}
|
|
1044
|
-
assetPath(assetId) {
|
|
1045
|
-
return path.join(this.assetsDir, encodeComponent(assetId));
|
|
1046
|
-
}
|
|
1047
|
-
};
|
|
1048
|
-
function encodeComponent(value) {
|
|
1049
|
-
return Buffer.from(value, "utf8").toString("base64url");
|
|
1050
|
-
}
|
|
1051
|
-
async function ensureDir(dir) {
|
|
1052
|
-
await promises.mkdir(dir, { recursive: true });
|
|
1053
|
-
}
|
|
1054
|
-
async function readFileIfExists(filePath) {
|
|
1055
|
-
try {
|
|
1056
|
-
const data = await promises.readFile(filePath);
|
|
1057
|
-
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
if (error.code === "ENOENT") return;
|
|
1060
|
-
throw error;
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
async function removeIfExists(filePath) {
|
|
1064
|
-
try {
|
|
1065
|
-
await promises.rm(filePath);
|
|
1066
|
-
} catch (error) {
|
|
1067
|
-
if (error.code === "ENOENT") return;
|
|
1068
|
-
throw error;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
async function listFiles(dir) {
|
|
1072
|
-
try {
|
|
1073
|
-
return (await promises.readdir(dir)).sort();
|
|
1074
|
-
} catch (error) {
|
|
1075
|
-
if (error.code === "ENOENT") return [];
|
|
1076
|
-
throw error;
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
async function writeFileAtomic(targetPath, data) {
|
|
1080
|
-
const dir = path.dirname(targetPath);
|
|
1081
|
-
await ensureDir(dir);
|
|
1082
|
-
const tempPath = path.join(dir, `.tmp-${randomUUID()}`);
|
|
1083
|
-
await promises.writeFile(tempPath, data);
|
|
1084
|
-
await promises.rename(tempPath, targetPath);
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
//#endregion
|
|
1088
|
-
//#region src/internal/event-bus.ts
|
|
1089
|
-
var RepoEventBus = class {
|
|
1090
|
-
watchers = /* @__PURE__ */ new Set();
|
|
1091
|
-
eventByStack = [];
|
|
1092
|
-
watch(listener, filter = {}) {
|
|
1093
|
-
const entry = {
|
|
1094
|
-
listener,
|
|
1095
|
-
filter
|
|
1096
|
-
};
|
|
1097
|
-
this.watchers.add(entry);
|
|
1098
|
-
return { unsubscribe: () => {
|
|
1099
|
-
this.watchers.delete(entry);
|
|
1100
|
-
} };
|
|
1101
|
-
}
|
|
1102
|
-
emit(event) {
|
|
1103
|
-
for (const entry of this.watchers) if (this.shouldNotify(entry.filter, event)) entry.listener(event);
|
|
1104
|
-
}
|
|
1105
|
-
clear() {
|
|
1106
|
-
this.watchers.clear();
|
|
1107
|
-
this.eventByStack.length = 0;
|
|
1108
|
-
}
|
|
1109
|
-
pushEventBy(by) {
|
|
1110
|
-
this.eventByStack.push(by);
|
|
1111
|
-
}
|
|
1112
|
-
popEventBy() {
|
|
1113
|
-
this.eventByStack.pop();
|
|
1114
|
-
}
|
|
1115
|
-
resolveEventBy(defaultBy) {
|
|
1116
|
-
const index = this.eventByStack.length - 1;
|
|
1117
|
-
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
1118
|
-
}
|
|
1119
|
-
shouldNotify(filter, event) {
|
|
1120
|
-
if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
|
|
1121
|
-
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
1122
|
-
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
1123
|
-
const docId = (() => {
|
|
1124
|
-
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
1125
|
-
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
1126
|
-
})();
|
|
1127
|
-
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
1128
|
-
if (filter.docIds && !docId) return false;
|
|
1129
|
-
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
1130
|
-
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
1131
|
-
}
|
|
1132
|
-
return true;
|
|
1133
|
-
}
|
|
1134
|
-
};
|
|
1135
|
-
|
|
1136
|
-
//#endregion
|
|
1137
|
-
//#region src/utils.ts
|
|
1138
|
-
async function streamToUint8Array(stream) {
|
|
1139
|
-
const reader = stream.getReader();
|
|
1140
|
-
const chunks = [];
|
|
1141
|
-
let total = 0;
|
|
1142
|
-
while (true) {
|
|
1143
|
-
const { done, value } = await reader.read();
|
|
1144
|
-
if (done) break;
|
|
1145
|
-
if (value) {
|
|
1146
|
-
chunks.push(value);
|
|
1147
|
-
total += value.byteLength;
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
const buffer = new Uint8Array(total);
|
|
1151
|
-
let offset = 0;
|
|
1152
|
-
for (const chunk of chunks) {
|
|
1153
|
-
buffer.set(chunk, offset);
|
|
1154
|
-
offset += chunk.byteLength;
|
|
1155
|
-
}
|
|
1156
|
-
return buffer;
|
|
1157
|
-
}
|
|
1158
|
-
async function assetContentToUint8Array(content) {
|
|
1159
|
-
if (content instanceof Uint8Array) return content;
|
|
1160
|
-
if (ArrayBuffer.isView(content)) return new Uint8Array(content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
|
|
1161
|
-
if (typeof Blob !== "undefined" && content instanceof Blob) return new Uint8Array(await content.arrayBuffer());
|
|
1162
|
-
if (typeof ReadableStream !== "undefined" && content instanceof ReadableStream) return streamToUint8Array(content);
|
|
1163
|
-
throw new TypeError("Unsupported asset content type");
|
|
1164
|
-
}
|
|
1165
|
-
function bytesToHex$1(bytes) {
|
|
1166
|
-
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1167
|
-
}
|
|
1168
|
-
async function computeSha256(bytes) {
|
|
1169
|
-
const globalCrypto = globalThis.crypto;
|
|
1170
|
-
if (globalCrypto?.subtle && typeof globalCrypto.subtle.digest === "function") {
|
|
1171
|
-
const digest = await globalCrypto.subtle.digest("SHA-256", bytes);
|
|
1172
|
-
return bytesToHex$1(new Uint8Array(digest));
|
|
1173
|
-
}
|
|
1174
|
-
try {
|
|
1175
|
-
const { createHash } = await import("node:crypto");
|
|
1176
|
-
const hash = createHash("sha256");
|
|
1177
|
-
hash.update(bytes);
|
|
1178
|
-
return hash.digest("hex");
|
|
1179
|
-
} catch {
|
|
1180
|
-
throw new Error("SHA-256 digest is not available in this environment");
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
function cloneJsonValue(value) {
|
|
1184
|
-
if (value === null) return null;
|
|
1185
|
-
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
1186
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
1187
|
-
if (Array.isArray(value)) {
|
|
1188
|
-
const arr = [];
|
|
1189
|
-
for (const entry of value) {
|
|
1190
|
-
const cloned = cloneJsonValue(entry);
|
|
1191
|
-
if (cloned !== void 0) arr.push(cloned);
|
|
1192
|
-
}
|
|
1193
|
-
return arr;
|
|
1194
|
-
}
|
|
1195
|
-
if (value && typeof value === "object") {
|
|
1196
|
-
const input = value;
|
|
1197
|
-
const obj = {};
|
|
1198
|
-
for (const [key, entry] of Object.entries(input)) {
|
|
1199
|
-
const cloned = cloneJsonValue(entry);
|
|
1200
|
-
if (cloned !== void 0) obj[key] = cloned;
|
|
1201
|
-
}
|
|
1202
|
-
return obj;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
function cloneJsonObject(value) {
|
|
1206
|
-
const cloned = cloneJsonValue(value);
|
|
1207
|
-
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1208
|
-
return {};
|
|
1209
|
-
}
|
|
1210
|
-
function asJsonObject(value) {
|
|
1211
|
-
const cloned = cloneJsonValue(value);
|
|
1212
|
-
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1213
|
-
}
|
|
1214
|
-
function isJsonObjectValue(value) {
|
|
1215
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1216
|
-
}
|
|
1217
|
-
function stableStringify(value) {
|
|
1218
|
-
if (value === null) return "null";
|
|
1219
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
1220
|
-
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
1221
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1222
|
-
if (!isJsonObjectValue(value)) return "null";
|
|
1223
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
1224
|
-
}
|
|
1225
|
-
function jsonEquals(a, b) {
|
|
1226
|
-
if (a === void 0 && b === void 0) return true;
|
|
1227
|
-
if (a === void 0 || b === void 0) return false;
|
|
1228
|
-
return stableStringify(a) === stableStringify(b);
|
|
1229
|
-
}
|
|
1230
|
-
function diffJsonObjects(previous, next) {
|
|
1231
|
-
const patch = {};
|
|
1232
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1233
|
-
if (previous) for (const key of Object.keys(previous)) keys.add(key);
|
|
1234
|
-
for (const key of Object.keys(next)) keys.add(key);
|
|
1235
|
-
for (const key of keys) {
|
|
1236
|
-
const prevValue = previous ? previous[key] : void 0;
|
|
1237
|
-
const nextValue = next[key];
|
|
1238
|
-
if (!jsonEquals(prevValue, nextValue)) {
|
|
1239
|
-
if (nextValue === void 0 && previous && key in previous) {
|
|
1240
|
-
patch[key] = null;
|
|
1241
|
-
continue;
|
|
1242
|
-
}
|
|
1243
|
-
const cloned = cloneJsonValue(nextValue);
|
|
1244
|
-
if (cloned !== void 0) patch[key] = cloned;
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
return patch;
|
|
1248
|
-
}
|
|
1249
|
-
function assetMetaToJson(meta) {
|
|
1250
|
-
const json = {
|
|
1251
|
-
assetId: meta.assetId,
|
|
1252
|
-
size: meta.size,
|
|
1253
|
-
createdAt: meta.createdAt
|
|
1254
|
-
};
|
|
1255
|
-
if (meta.mime !== void 0) json.mime = meta.mime;
|
|
1256
|
-
if (meta.policy !== void 0) json.policy = meta.policy;
|
|
1257
|
-
if (meta.tag !== void 0) json.tag = meta.tag;
|
|
1258
|
-
return json;
|
|
1259
|
-
}
|
|
1260
|
-
function assetMetaFromJson(value) {
|
|
1261
|
-
const obj = asJsonObject(value);
|
|
1262
|
-
if (!obj) return void 0;
|
|
1263
|
-
const assetId = typeof obj.assetId === "string" ? obj.assetId : void 0;
|
|
1264
|
-
if (!assetId) return void 0;
|
|
1265
|
-
const size = typeof obj.size === "number" ? obj.size : void 0;
|
|
1266
|
-
const createdAt = typeof obj.createdAt === "number" ? obj.createdAt : void 0;
|
|
1267
|
-
if (size === void 0 || createdAt === void 0) return void 0;
|
|
1268
|
-
return {
|
|
1269
|
-
assetId,
|
|
1270
|
-
size,
|
|
1271
|
-
createdAt,
|
|
1272
|
-
...typeof obj.mime === "string" ? { mime: obj.mime } : {},
|
|
1273
|
-
...typeof obj.policy === "string" ? { policy: obj.policy } : {},
|
|
1274
|
-
...typeof obj.tag === "string" ? { tag: obj.tag } : {}
|
|
1275
|
-
};
|
|
1276
|
-
}
|
|
1277
|
-
function assetMetadataEqual(a, b) {
|
|
1278
|
-
if (!a && !b) return true;
|
|
1279
|
-
if (!a || !b) return false;
|
|
1280
|
-
return stableStringify(assetMetaToJson(a)) === stableStringify(assetMetaToJson(b));
|
|
1281
|
-
}
|
|
1282
|
-
function cloneRepoAssetMetadata(meta) {
|
|
1283
|
-
return {
|
|
1284
|
-
assetId: meta.assetId,
|
|
1285
|
-
size: meta.size,
|
|
1286
|
-
createdAt: meta.createdAt,
|
|
1287
|
-
...meta.mime !== void 0 ? { mime: meta.mime } : {},
|
|
1288
|
-
...meta.policy !== void 0 ? { policy: meta.policy } : {},
|
|
1289
|
-
...meta.tag !== void 0 ? { tag: meta.tag } : {}
|
|
1290
|
-
};
|
|
1291
|
-
}
|
|
1292
|
-
function toReadableStream(bytes) {
|
|
1293
|
-
return new ReadableStream({ start(controller) {
|
|
1294
|
-
controller.enqueue(bytes);
|
|
1295
|
-
controller.close();
|
|
1296
|
-
} });
|
|
1297
|
-
}
|
|
1298
|
-
function canonicalizeFrontiers(frontiers) {
|
|
1299
|
-
const json = [...frontiers].sort((a, b) => {
|
|
1300
|
-
if (a.peer < b.peer) return -1;
|
|
1301
|
-
if (a.peer > b.peer) return 1;
|
|
1302
|
-
return a.counter - b.counter;
|
|
1303
|
-
}).map((f) => ({
|
|
1304
|
-
peer: f.peer,
|
|
1305
|
-
counter: f.counter
|
|
1306
|
-
}));
|
|
1307
|
-
return {
|
|
1308
|
-
json,
|
|
1309
|
-
key: stableStringify(json)
|
|
1310
|
-
};
|
|
1311
|
-
}
|
|
1312
|
-
function includesFrontiers(vv, frontiers) {
|
|
1313
|
-
for (const { peer, counter } of frontiers) if ((vv.get(peer) ?? 0) <= counter) return false;
|
|
1314
|
-
return true;
|
|
1315
|
-
}
|
|
1316
|
-
function matchesQuery(docId, _metadata, query) {
|
|
1317
|
-
if (!query) return true;
|
|
1318
|
-
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
1319
|
-
if (query.start && docId < query.start) return false;
|
|
1320
|
-
if (query.end && docId > query.end) return false;
|
|
1321
|
-
return true;
|
|
1322
|
-
}
|
|
4
|
+
//#region src/internal/event-bus.ts
|
|
5
|
+
var RepoEventBus = class {
|
|
6
|
+
watchers = /* @__PURE__ */ new Set();
|
|
7
|
+
eventByStack = [];
|
|
8
|
+
watch(listener, filter = {}) {
|
|
9
|
+
const entry = {
|
|
10
|
+
listener,
|
|
11
|
+
filter
|
|
12
|
+
};
|
|
13
|
+
this.watchers.add(entry);
|
|
14
|
+
return { unsubscribe: () => {
|
|
15
|
+
this.watchers.delete(entry);
|
|
16
|
+
} };
|
|
17
|
+
}
|
|
18
|
+
emit(event) {
|
|
19
|
+
for (const entry of this.watchers) if (this.shouldNotify(entry.filter, event)) entry.listener(event);
|
|
20
|
+
}
|
|
21
|
+
clear() {
|
|
22
|
+
this.watchers.clear();
|
|
23
|
+
this.eventByStack.length = 0;
|
|
24
|
+
}
|
|
25
|
+
pushEventBy(by) {
|
|
26
|
+
this.eventByStack.push(by);
|
|
27
|
+
}
|
|
28
|
+
popEventBy() {
|
|
29
|
+
this.eventByStack.pop();
|
|
30
|
+
}
|
|
31
|
+
resolveEventBy(defaultBy) {
|
|
32
|
+
const index = this.eventByStack.length - 1;
|
|
33
|
+
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
34
|
+
}
|
|
35
|
+
shouldNotify(filter, event) {
|
|
36
|
+
if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
|
|
37
|
+
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
38
|
+
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
39
|
+
const docId = (() => {
|
|
40
|
+
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
41
|
+
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
42
|
+
})();
|
|
43
|
+
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
44
|
+
if (filter.docIds && !docId) return false;
|
|
45
|
+
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
46
|
+
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
1323
51
|
|
|
1324
52
|
//#endregion
|
|
1325
53
|
//#region src/internal/logging.ts
|
|
@@ -1338,23 +66,18 @@ var DocManager = class {
|
|
|
1338
66
|
getMetaFlock;
|
|
1339
67
|
eventBus;
|
|
1340
68
|
persistMeta;
|
|
1341
|
-
state;
|
|
1342
69
|
docs = /* @__PURE__ */ new Map();
|
|
1343
70
|
docSubscriptions = /* @__PURE__ */ new Map();
|
|
1344
71
|
docFrontierUpdates = /* @__PURE__ */ new Map();
|
|
1345
72
|
docPersistedVersions = /* @__PURE__ */ new Map();
|
|
1346
|
-
get docFrontierKeys() {
|
|
1347
|
-
return this.state.docFrontierKeys;
|
|
1348
|
-
}
|
|
1349
73
|
constructor(options) {
|
|
1350
74
|
this.storage = options.storage;
|
|
1351
75
|
this.docFrontierDebounceMs = options.docFrontierDebounceMs;
|
|
1352
76
|
this.getMetaFlock = options.getMetaFlock;
|
|
1353
77
|
this.eventBus = options.eventBus;
|
|
1354
78
|
this.persistMeta = options.persistMeta;
|
|
1355
|
-
this.state = options.state;
|
|
1356
79
|
}
|
|
1357
|
-
async
|
|
80
|
+
async openPersistedDoc(docId) {
|
|
1358
81
|
return await this.ensureDoc(docId);
|
|
1359
82
|
}
|
|
1360
83
|
async openDetachedDoc(docId) {
|
|
@@ -1401,38 +124,27 @@ var DocManager = class {
|
|
|
1401
124
|
}
|
|
1402
125
|
async updateDocFrontiers(docId, doc, defaultBy) {
|
|
1403
126
|
const frontiers = doc.oplogFrontiers();
|
|
1404
|
-
const
|
|
1405
|
-
const
|
|
127
|
+
const vv = doc.version();
|
|
128
|
+
const existingFrontiers = this.readFrontiersFromFlock(docId);
|
|
1406
129
|
let mutated = false;
|
|
1407
130
|
const metaFlock = this.metaFlock;
|
|
1408
|
-
const
|
|
1409
|
-
for (const entry of existingKeys) {
|
|
1410
|
-
if (entry === key) continue;
|
|
1411
|
-
let oldFrontiers;
|
|
1412
|
-
try {
|
|
1413
|
-
oldFrontiers = JSON.parse(entry);
|
|
1414
|
-
} catch {
|
|
1415
|
-
continue;
|
|
1416
|
-
}
|
|
1417
|
-
if (includesFrontiers(vv, oldFrontiers)) {
|
|
1418
|
-
metaFlock.delete([
|
|
1419
|
-
"f",
|
|
1420
|
-
docId,
|
|
1421
|
-
entry
|
|
1422
|
-
]);
|
|
1423
|
-
mutated = true;
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
if (!existingKeys.has(key)) {
|
|
131
|
+
for (const f of frontiers) if (existingFrontiers.get(f.peer) !== f.counter) {
|
|
1427
132
|
metaFlock.put([
|
|
1428
133
|
"f",
|
|
1429
134
|
docId,
|
|
1430
|
-
|
|
1431
|
-
],
|
|
135
|
+
f.peer
|
|
136
|
+
], f.counter);
|
|
1432
137
|
mutated = true;
|
|
1433
138
|
}
|
|
1434
139
|
if (mutated) {
|
|
1435
|
-
|
|
140
|
+
for (const [peer, counter] of existingFrontiers) {
|
|
141
|
+
const docCounterEnd = vv.get(peer);
|
|
142
|
+
if (docCounterEnd != null && docCounterEnd > counter) metaFlock.delete([
|
|
143
|
+
"f",
|
|
144
|
+
docId,
|
|
145
|
+
peer
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
1436
148
|
await this.persistMeta();
|
|
1437
149
|
}
|
|
1438
150
|
const by = this.eventBus.resolveEventBy(defaultBy);
|
|
@@ -1468,12 +180,12 @@ var DocManager = class {
|
|
|
1468
180
|
this.docPersistedVersions.delete(docId);
|
|
1469
181
|
}
|
|
1470
182
|
async flush() {
|
|
1471
|
-
const promises
|
|
1472
|
-
for (const [docId, doc] of this.docs) promises
|
|
183
|
+
const promises = [];
|
|
184
|
+
for (const [docId, doc] of this.docs) promises.push((async () => {
|
|
1473
185
|
await this.persistDocUpdate(docId, doc);
|
|
1474
186
|
await this.flushScheduledDocFrontierUpdate(docId);
|
|
1475
187
|
})());
|
|
1476
|
-
await Promise.all(promises
|
|
188
|
+
await Promise.all(promises);
|
|
1477
189
|
}
|
|
1478
190
|
async close() {
|
|
1479
191
|
await this.flush();
|
|
@@ -1484,37 +196,22 @@ var DocManager = class {
|
|
|
1484
196
|
this.docFrontierUpdates.clear();
|
|
1485
197
|
this.docs.clear();
|
|
1486
198
|
this.docPersistedVersions.clear();
|
|
1487
|
-
this.docFrontierKeys.clear();
|
|
1488
199
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
const frontierRows = this.metaFlock.scan({ prefix: ["f"] });
|
|
1492
|
-
for (const row of frontierRows) {
|
|
1493
|
-
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1494
|
-
const docId = row.key[1];
|
|
1495
|
-
const frontierKey = row.key[2];
|
|
1496
|
-
if (typeof docId !== "string" || typeof frontierKey !== "string") continue;
|
|
1497
|
-
const set = nextFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1498
|
-
set.add(frontierKey);
|
|
1499
|
-
nextFrontierKeys.set(docId, set);
|
|
1500
|
-
}
|
|
1501
|
-
this.docFrontierKeys.clear();
|
|
1502
|
-
for (const [docId, keys] of nextFrontierKeys) this.docFrontierKeys.set(docId, keys);
|
|
200
|
+
get metaFlock() {
|
|
201
|
+
return this.getMetaFlock();
|
|
1503
202
|
}
|
|
1504
|
-
|
|
203
|
+
readFrontiersFromFlock(docId) {
|
|
1505
204
|
const rows = this.metaFlock.scan({ prefix: ["f", docId] });
|
|
1506
|
-
const
|
|
205
|
+
const frontiers = /* @__PURE__ */ new Map();
|
|
1507
206
|
for (const row of rows) {
|
|
1508
207
|
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1509
|
-
|
|
1510
|
-
const
|
|
1511
|
-
if (typeof
|
|
208
|
+
const peer = row.key[2];
|
|
209
|
+
const counter = row.value;
|
|
210
|
+
if (typeof peer !== "string") continue;
|
|
211
|
+
if (typeof counter !== "number" || !Number.isFinite(counter)) continue;
|
|
212
|
+
frontiers.set(peer, counter);
|
|
1512
213
|
}
|
|
1513
|
-
|
|
1514
|
-
else this.docFrontierKeys.delete(docId);
|
|
1515
|
-
}
|
|
1516
|
-
get metaFlock() {
|
|
1517
|
-
return this.getMetaFlock();
|
|
214
|
+
return frontiers;
|
|
1518
215
|
}
|
|
1519
216
|
registerDoc(docId, doc) {
|
|
1520
217
|
this.docs.set(docId, doc);
|
|
@@ -1627,6 +324,176 @@ var DocManager = class {
|
|
|
1627
324
|
}
|
|
1628
325
|
};
|
|
1629
326
|
|
|
327
|
+
//#endregion
|
|
328
|
+
//#region src/utils.ts
|
|
329
|
+
async function streamToUint8Array(stream) {
|
|
330
|
+
const reader = stream.getReader();
|
|
331
|
+
const chunks = [];
|
|
332
|
+
let total = 0;
|
|
333
|
+
while (true) {
|
|
334
|
+
const { done, value } = await reader.read();
|
|
335
|
+
if (done) break;
|
|
336
|
+
if (value) {
|
|
337
|
+
chunks.push(value);
|
|
338
|
+
total += value.byteLength;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const buffer = new Uint8Array(total);
|
|
342
|
+
let offset = 0;
|
|
343
|
+
for (const chunk of chunks) {
|
|
344
|
+
buffer.set(chunk, offset);
|
|
345
|
+
offset += chunk.byteLength;
|
|
346
|
+
}
|
|
347
|
+
return buffer;
|
|
348
|
+
}
|
|
349
|
+
async function assetContentToUint8Array(content) {
|
|
350
|
+
if (content instanceof Uint8Array) return content;
|
|
351
|
+
if (ArrayBuffer.isView(content)) return new Uint8Array(content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
|
|
352
|
+
if (typeof Blob !== "undefined" && content instanceof Blob) return new Uint8Array(await content.arrayBuffer());
|
|
353
|
+
if (typeof ReadableStream !== "undefined" && content instanceof ReadableStream) return streamToUint8Array(content);
|
|
354
|
+
throw new TypeError("Unsupported asset content type");
|
|
355
|
+
}
|
|
356
|
+
function bytesToHex(bytes) {
|
|
357
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
358
|
+
}
|
|
359
|
+
async function computeSha256(bytes) {
|
|
360
|
+
const globalCrypto = globalThis.crypto;
|
|
361
|
+
if (globalCrypto?.subtle && typeof globalCrypto.subtle.digest === "function") {
|
|
362
|
+
const digest = await globalCrypto.subtle.digest("SHA-256", bytes);
|
|
363
|
+
return bytesToHex(new Uint8Array(digest));
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
const { createHash } = await import("node:crypto");
|
|
367
|
+
const hash = createHash("sha256");
|
|
368
|
+
hash.update(bytes);
|
|
369
|
+
return hash.digest("hex");
|
|
370
|
+
} catch {
|
|
371
|
+
throw new Error("SHA-256 digest is not available in this environment");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function cloneJsonValue(value) {
|
|
375
|
+
if (value === null) return null;
|
|
376
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
377
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
378
|
+
if (Array.isArray(value)) {
|
|
379
|
+
const arr = [];
|
|
380
|
+
for (const entry of value) {
|
|
381
|
+
const cloned = cloneJsonValue(entry);
|
|
382
|
+
if (cloned !== void 0) arr.push(cloned);
|
|
383
|
+
}
|
|
384
|
+
return arr;
|
|
385
|
+
}
|
|
386
|
+
if (value && typeof value === "object") {
|
|
387
|
+
const input = value;
|
|
388
|
+
const obj = {};
|
|
389
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
390
|
+
const cloned = cloneJsonValue(entry);
|
|
391
|
+
if (cloned !== void 0) obj[key] = cloned;
|
|
392
|
+
}
|
|
393
|
+
return obj;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function cloneJsonObject(value) {
|
|
397
|
+
const cloned = cloneJsonValue(value);
|
|
398
|
+
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
399
|
+
return {};
|
|
400
|
+
}
|
|
401
|
+
function asJsonObject(value) {
|
|
402
|
+
const cloned = cloneJsonValue(value);
|
|
403
|
+
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
404
|
+
}
|
|
405
|
+
function isJsonObjectValue(value) {
|
|
406
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
407
|
+
}
|
|
408
|
+
function stableStringify(value) {
|
|
409
|
+
if (value === null) return "null";
|
|
410
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
411
|
+
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
412
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
413
|
+
if (!isJsonObjectValue(value)) return "null";
|
|
414
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
415
|
+
}
|
|
416
|
+
function jsonEquals(a, b) {
|
|
417
|
+
if (a === void 0 && b === void 0) return true;
|
|
418
|
+
if (a === void 0 || b === void 0) return false;
|
|
419
|
+
return stableStringify(a) === stableStringify(b);
|
|
420
|
+
}
|
|
421
|
+
function diffJsonObjects(previous, next) {
|
|
422
|
+
const patch = {};
|
|
423
|
+
const keys = /* @__PURE__ */ new Set();
|
|
424
|
+
if (previous) for (const key of Object.keys(previous)) keys.add(key);
|
|
425
|
+
for (const key of Object.keys(next)) keys.add(key);
|
|
426
|
+
for (const key of keys) {
|
|
427
|
+
const prevValue = previous ? previous[key] : void 0;
|
|
428
|
+
const nextValue = next[key];
|
|
429
|
+
if (!jsonEquals(prevValue, nextValue)) {
|
|
430
|
+
if (nextValue === void 0 && previous && key in previous) {
|
|
431
|
+
patch[key] = null;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const cloned = cloneJsonValue(nextValue);
|
|
435
|
+
if (cloned !== void 0) patch[key] = cloned;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return patch;
|
|
439
|
+
}
|
|
440
|
+
function assetMetaToJson(meta) {
|
|
441
|
+
const json = {
|
|
442
|
+
assetId: meta.assetId,
|
|
443
|
+
size: meta.size,
|
|
444
|
+
createdAt: meta.createdAt
|
|
445
|
+
};
|
|
446
|
+
if (meta.mime !== void 0) json.mime = meta.mime;
|
|
447
|
+
if (meta.policy !== void 0) json.policy = meta.policy;
|
|
448
|
+
if (meta.tag !== void 0) json.tag = meta.tag;
|
|
449
|
+
return json;
|
|
450
|
+
}
|
|
451
|
+
function assetMetaFromJson(value) {
|
|
452
|
+
const obj = asJsonObject(value);
|
|
453
|
+
if (!obj) return void 0;
|
|
454
|
+
const assetId = typeof obj.assetId === "string" ? obj.assetId : void 0;
|
|
455
|
+
if (!assetId) return void 0;
|
|
456
|
+
const size = typeof obj.size === "number" ? obj.size : void 0;
|
|
457
|
+
const createdAt = typeof obj.createdAt === "number" ? obj.createdAt : void 0;
|
|
458
|
+
if (size === void 0 || createdAt === void 0) return void 0;
|
|
459
|
+
return {
|
|
460
|
+
assetId,
|
|
461
|
+
size,
|
|
462
|
+
createdAt,
|
|
463
|
+
...typeof obj.mime === "string" ? { mime: obj.mime } : {},
|
|
464
|
+
...typeof obj.policy === "string" ? { policy: obj.policy } : {},
|
|
465
|
+
...typeof obj.tag === "string" ? { tag: obj.tag } : {}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function assetMetadataEqual(a, b) {
|
|
469
|
+
if (!a && !b) return true;
|
|
470
|
+
if (!a || !b) return false;
|
|
471
|
+
return stableStringify(assetMetaToJson(a)) === stableStringify(assetMetaToJson(b));
|
|
472
|
+
}
|
|
473
|
+
function cloneRepoAssetMetadata(meta) {
|
|
474
|
+
return {
|
|
475
|
+
assetId: meta.assetId,
|
|
476
|
+
size: meta.size,
|
|
477
|
+
createdAt: meta.createdAt,
|
|
478
|
+
...meta.mime !== void 0 ? { mime: meta.mime } : {},
|
|
479
|
+
...meta.policy !== void 0 ? { policy: meta.policy } : {},
|
|
480
|
+
...meta.tag !== void 0 ? { tag: meta.tag } : {}
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function toReadableStream(bytes) {
|
|
484
|
+
return new ReadableStream({ start(controller) {
|
|
485
|
+
controller.enqueue(bytes);
|
|
486
|
+
controller.close();
|
|
487
|
+
} });
|
|
488
|
+
}
|
|
489
|
+
function matchesQuery(docId, _metadata, query) {
|
|
490
|
+
if (!query) return true;
|
|
491
|
+
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
492
|
+
if (query.start && docId < query.start) return false;
|
|
493
|
+
if (query.end && docId > query.end) return false;
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
|
|
1630
497
|
//#endregion
|
|
1631
498
|
//#region src/internal/metadata-manager.ts
|
|
1632
499
|
var MetadataManager = class {
|
|
@@ -2424,13 +1291,11 @@ var FlockHydrator = class {
|
|
|
2424
1291
|
const nextMetadata = this.readAllDocMetadata();
|
|
2425
1292
|
this.metadataManager.replaceAll(nextMetadata, by);
|
|
2426
1293
|
this.assetManager.hydrateFromFlock(by);
|
|
2427
|
-
this.docManager.hydrateFrontierKeys();
|
|
2428
1294
|
}
|
|
2429
1295
|
applyEvents(events, by) {
|
|
2430
1296
|
if (!events.length) return;
|
|
2431
1297
|
const docMetadataIds = /* @__PURE__ */ new Set();
|
|
2432
1298
|
const docAssetIds = /* @__PURE__ */ new Set();
|
|
2433
|
-
const docFrontiersIds = /* @__PURE__ */ new Set();
|
|
2434
1299
|
const assetIds = /* @__PURE__ */ new Set();
|
|
2435
1300
|
for (const event of events) {
|
|
2436
1301
|
const key = event.key;
|
|
@@ -2447,15 +1312,11 @@ var FlockHydrator = class {
|
|
|
2447
1312
|
const assetId = key[2];
|
|
2448
1313
|
if (typeof docId === "string") docAssetIds.add(docId);
|
|
2449
1314
|
if (typeof assetId === "string") assetIds.add(assetId);
|
|
2450
|
-
} else if (root === "f") {
|
|
2451
|
-
const docId = key[1];
|
|
2452
|
-
if (typeof docId === "string") docFrontiersIds.add(docId);
|
|
2453
1315
|
}
|
|
2454
1316
|
}
|
|
2455
1317
|
for (const assetId of assetIds) this.assetManager.refreshAssetMetadataEntry(assetId, by);
|
|
2456
1318
|
for (const docId of docMetadataIds) this.metadataManager.refreshFromFlock(docId, by);
|
|
2457
1319
|
for (const docId of docAssetIds) this.assetManager.refreshDocAssetsEntry(docId, by);
|
|
2458
|
-
for (const docId of docFrontiersIds) this.docManager.refreshDocFrontierKeys(docId);
|
|
2459
1320
|
}
|
|
2460
1321
|
readAllDocMetadata() {
|
|
2461
1322
|
const nextMetadata = /* @__PURE__ */ new Map();
|
|
@@ -2666,8 +1527,7 @@ function createRepoState() {
|
|
|
2666
1527
|
docAssets: /* @__PURE__ */ new Map(),
|
|
2667
1528
|
assets: /* @__PURE__ */ new Map(),
|
|
2668
1529
|
orphanedAssets: /* @__PURE__ */ new Map(),
|
|
2669
|
-
assetToDocRefs: /* @__PURE__ */ new Map()
|
|
2670
|
-
docFrontierKeys: /* @__PURE__ */ new Map()
|
|
1530
|
+
assetToDocRefs: /* @__PURE__ */ new Map()
|
|
2671
1531
|
};
|
|
2672
1532
|
}
|
|
2673
1533
|
|
|
@@ -2703,8 +1563,7 @@ var LoroRepo = class LoroRepo {
|
|
|
2703
1563
|
docFrontierDebounceMs,
|
|
2704
1564
|
getMetaFlock: () => this.metaFlock,
|
|
2705
1565
|
eventBus: this.eventBus,
|
|
2706
|
-
persistMeta: () => this.persistMeta()
|
|
2707
|
-
state: this.state
|
|
1566
|
+
persistMeta: () => this.persistMeta()
|
|
2708
1567
|
});
|
|
2709
1568
|
this.metadataManager = new MetadataManager({
|
|
2710
1569
|
getMetaFlock: () => this.metaFlock,
|
|
@@ -2793,7 +1652,7 @@ var LoroRepo = class LoroRepo {
|
|
|
2793
1652
|
*/
|
|
2794
1653
|
async openPersistedDoc(docId) {
|
|
2795
1654
|
return {
|
|
2796
|
-
doc: await this.docManager.
|
|
1655
|
+
doc: await this.docManager.openPersistedDoc(docId),
|
|
2797
1656
|
syncOnce: () => {
|
|
2798
1657
|
return this.sync({
|
|
2799
1658
|
scope: "doc",
|
|
@@ -2887,5 +1746,5 @@ var LoroRepo = class LoroRepo {
|
|
|
2887
1746
|
};
|
|
2888
1747
|
|
|
2889
1748
|
//#endregion
|
|
2890
|
-
export {
|
|
1749
|
+
export { LoroRepo };
|
|
2891
1750
|
//# sourceMappingURL=index.js.map
|