loro-repo 0.5.0 → 0.5.2
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 +10 -1130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -608
- package/dist/index.d.ts +3 -608
- package/dist/index.js +13 -1100
- 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 +27 -2
package/dist/index.cjs
CHANGED
|
@@ -1,1120 +1,9 @@
|
|
|
1
|
-
|
|
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") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
-
key = keys[i];
|
|
11
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
-
get: ((k) => from[k]).bind(null, key),
|
|
13
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
return to;
|
|
17
|
-
};
|
|
18
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
-
value: mod,
|
|
20
|
-
enumerable: true
|
|
21
|
-
}) : target, mod));
|
|
22
|
-
|
|
23
|
-
//#endregion
|
|
1
|
+
const require_chunk = require('./chunk.cjs');
|
|
24
2
|
let __loro_dev_flock = require("@loro-dev/flock");
|
|
25
|
-
__loro_dev_flock = __toESM(__loro_dev_flock);
|
|
3
|
+
__loro_dev_flock = require_chunk.__toESM(__loro_dev_flock);
|
|
26
4
|
let loro_crdt = require("loro-crdt");
|
|
27
|
-
loro_crdt = __toESM(loro_crdt);
|
|
28
|
-
let node_fs = require("node:fs");
|
|
29
|
-
node_fs = __toESM(node_fs);
|
|
30
|
-
let node_path = require("node:path");
|
|
31
|
-
node_path = __toESM(node_path);
|
|
32
|
-
let node_crypto = require("node:crypto");
|
|
33
|
-
node_crypto = __toESM(node_crypto);
|
|
34
|
-
let loro_adaptors_loro = require("loro-adaptors/loro");
|
|
35
|
-
loro_adaptors_loro = __toESM(loro_adaptors_loro);
|
|
36
|
-
let loro_protocol = require("loro-protocol");
|
|
37
|
-
loro_protocol = __toESM(loro_protocol);
|
|
38
|
-
let loro_websocket = require("loro-websocket");
|
|
39
|
-
loro_websocket = __toESM(loro_websocket);
|
|
40
|
-
let loro_adaptors_flock = require("loro-adaptors/flock");
|
|
41
|
-
loro_adaptors_flock = __toESM(loro_adaptors_flock);
|
|
42
|
-
|
|
43
|
-
//#region src/transport/broadcast-channel.ts
|
|
44
|
-
function deferred() {
|
|
45
|
-
let resolve;
|
|
46
|
-
return {
|
|
47
|
-
promise: new Promise((res) => {
|
|
48
|
-
resolve = res;
|
|
49
|
-
}),
|
|
50
|
-
resolve
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
function randomInstanceId() {
|
|
54
|
-
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
55
|
-
return Math.random().toString(36).slice(2);
|
|
56
|
-
}
|
|
57
|
-
function ensureBroadcastChannel() {
|
|
58
|
-
if (typeof BroadcastChannel === "undefined") throw new Error("BroadcastChannel API is not available in this environment");
|
|
59
|
-
return BroadcastChannel;
|
|
60
|
-
}
|
|
61
|
-
function encodeDocChannelId(docId) {
|
|
62
|
-
try {
|
|
63
|
-
return encodeURIComponent(docId);
|
|
64
|
-
} catch {
|
|
65
|
-
return docId.replace(/[^a-z0-9_-]/gi, "_");
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
function postChannelMessage(channel, message) {
|
|
69
|
-
channel.postMessage(message);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* TransportAdapter that relies on the BroadcastChannel API to fan out metadata
|
|
73
|
-
* and document updates between browser tabs within the same origin.
|
|
74
|
-
*/
|
|
75
|
-
var BroadcastChannelTransportAdapter = class {
|
|
76
|
-
instanceId = randomInstanceId();
|
|
77
|
-
namespace;
|
|
78
|
-
metaChannelName;
|
|
79
|
-
connected = false;
|
|
80
|
-
metaState;
|
|
81
|
-
docStates = /* @__PURE__ */ new Map();
|
|
82
|
-
constructor(options = {}) {
|
|
83
|
-
ensureBroadcastChannel();
|
|
84
|
-
this.namespace = options.namespace ?? "loro-repo";
|
|
85
|
-
this.metaChannelName = options.metaChannelName ?? `${this.namespace}-meta`;
|
|
86
|
-
}
|
|
87
|
-
async connect() {
|
|
88
|
-
this.connected = true;
|
|
89
|
-
}
|
|
90
|
-
async close() {
|
|
91
|
-
this.connected = false;
|
|
92
|
-
if (this.metaState) {
|
|
93
|
-
for (const entry of this.metaState.listeners) entry.unsubscribe();
|
|
94
|
-
this.metaState.channel.close();
|
|
95
|
-
this.metaState = void 0;
|
|
96
|
-
}
|
|
97
|
-
for (const [docId] of this.docStates) this.teardownDocChannel(docId);
|
|
98
|
-
this.docStates.clear();
|
|
99
|
-
}
|
|
100
|
-
isConnected() {
|
|
101
|
-
return this.connected;
|
|
102
|
-
}
|
|
103
|
-
async syncMeta(flock, _options) {
|
|
104
|
-
const subscription = this.joinMetaRoom(flock);
|
|
105
|
-
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
106
|
-
await subscription.firstSyncedWithRemote;
|
|
107
|
-
subscription.unsubscribe();
|
|
108
|
-
return { ok: true };
|
|
109
|
-
}
|
|
110
|
-
joinMetaRoom(flock, _params) {
|
|
111
|
-
const state = this.ensureMetaChannel();
|
|
112
|
-
const { promise, resolve } = deferred();
|
|
113
|
-
const listener = {
|
|
114
|
-
flock,
|
|
115
|
-
muted: false,
|
|
116
|
-
unsubscribe: flock.subscribe(() => {
|
|
117
|
-
if (listener.muted) return;
|
|
118
|
-
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
119
|
-
postChannelMessage(state.channel, {
|
|
120
|
-
kind: "meta-export",
|
|
121
|
-
from: this.instanceId,
|
|
122
|
-
bundle
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
}),
|
|
126
|
-
resolveFirst: resolve,
|
|
127
|
-
firstSynced: promise
|
|
128
|
-
};
|
|
129
|
-
state.listeners.add(listener);
|
|
130
|
-
postChannelMessage(state.channel, {
|
|
131
|
-
kind: "meta-request",
|
|
132
|
-
from: this.instanceId
|
|
133
|
-
});
|
|
134
|
-
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
135
|
-
postChannelMessage(state.channel, {
|
|
136
|
-
kind: "meta-export",
|
|
137
|
-
from: this.instanceId,
|
|
138
|
-
bundle
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
queueMicrotask(() => resolve());
|
|
142
|
-
return {
|
|
143
|
-
unsubscribe: () => {
|
|
144
|
-
listener.unsubscribe();
|
|
145
|
-
state.listeners.delete(listener);
|
|
146
|
-
if (!state.listeners.size) {
|
|
147
|
-
state.channel.removeEventListener("message", state.onMessage);
|
|
148
|
-
state.channel.close();
|
|
149
|
-
this.metaState = void 0;
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
firstSyncedWithRemote: listener.firstSynced,
|
|
153
|
-
get connected() {
|
|
154
|
-
return true;
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
async syncDoc(docId, doc, _options) {
|
|
159
|
-
const subscription = this.joinDocRoom(docId, doc);
|
|
160
|
-
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
161
|
-
await subscription.firstSyncedWithRemote;
|
|
162
|
-
subscription.unsubscribe();
|
|
163
|
-
return { ok: true };
|
|
164
|
-
}
|
|
165
|
-
joinDocRoom(docId, doc, _params) {
|
|
166
|
-
const state = this.ensureDocChannel(docId);
|
|
167
|
-
const { promise, resolve } = deferred();
|
|
168
|
-
const listener = {
|
|
169
|
-
doc,
|
|
170
|
-
muted: false,
|
|
171
|
-
unsubscribe: doc.subscribe(() => {
|
|
172
|
-
if (listener.muted) return;
|
|
173
|
-
const payload = doc.export({ mode: "update" });
|
|
174
|
-
postChannelMessage(state.channel, {
|
|
175
|
-
kind: "doc-update",
|
|
176
|
-
docId,
|
|
177
|
-
from: this.instanceId,
|
|
178
|
-
mode: "update",
|
|
179
|
-
payload
|
|
180
|
-
});
|
|
181
|
-
}),
|
|
182
|
-
resolveFirst: resolve,
|
|
183
|
-
firstSynced: promise
|
|
184
|
-
};
|
|
185
|
-
state.listeners.add(listener);
|
|
186
|
-
postChannelMessage(state.channel, {
|
|
187
|
-
kind: "doc-request",
|
|
188
|
-
docId,
|
|
189
|
-
from: this.instanceId
|
|
190
|
-
});
|
|
191
|
-
postChannelMessage(state.channel, {
|
|
192
|
-
kind: "doc-update",
|
|
193
|
-
docId,
|
|
194
|
-
from: this.instanceId,
|
|
195
|
-
mode: "snapshot",
|
|
196
|
-
payload: doc.export({ mode: "snapshot" })
|
|
197
|
-
});
|
|
198
|
-
queueMicrotask(() => resolve());
|
|
199
|
-
return {
|
|
200
|
-
unsubscribe: () => {
|
|
201
|
-
listener.unsubscribe();
|
|
202
|
-
state.listeners.delete(listener);
|
|
203
|
-
if (!state.listeners.size) this.teardownDocChannel(docId);
|
|
204
|
-
},
|
|
205
|
-
firstSyncedWithRemote: listener.firstSynced,
|
|
206
|
-
get connected() {
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
ensureMetaChannel() {
|
|
212
|
-
if (this.metaState) return this.metaState;
|
|
213
|
-
const channel = new (ensureBroadcastChannel())(this.metaChannelName);
|
|
214
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
215
|
-
const onMessage = (event) => {
|
|
216
|
-
const message = event.data;
|
|
217
|
-
if (!message || message.from === this.instanceId) return;
|
|
218
|
-
if (message.kind === "meta-export") for (const entry of listeners) {
|
|
219
|
-
entry.muted = true;
|
|
220
|
-
entry.flock.importJson(message.bundle);
|
|
221
|
-
entry.muted = false;
|
|
222
|
-
entry.resolveFirst();
|
|
223
|
-
}
|
|
224
|
-
else if (message.kind === "meta-request") {
|
|
225
|
-
const first = listeners.values().next().value;
|
|
226
|
-
if (!first) return;
|
|
227
|
-
Promise.resolve(first.flock.exportJson()).then((bundle) => {
|
|
228
|
-
postChannelMessage(channel, {
|
|
229
|
-
kind: "meta-export",
|
|
230
|
-
from: this.instanceId,
|
|
231
|
-
bundle
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
channel.addEventListener("message", onMessage);
|
|
237
|
-
this.metaState = {
|
|
238
|
-
channel,
|
|
239
|
-
listeners,
|
|
240
|
-
onMessage
|
|
241
|
-
};
|
|
242
|
-
return this.metaState;
|
|
243
|
-
}
|
|
244
|
-
ensureDocChannel(docId) {
|
|
245
|
-
const existing = this.docStates.get(docId);
|
|
246
|
-
if (existing) return existing;
|
|
247
|
-
const channel = new (ensureBroadcastChannel())(`${this.namespace}-doc-${encodeDocChannelId(docId)}`);
|
|
248
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
249
|
-
const onMessage = (event) => {
|
|
250
|
-
const message = event.data;
|
|
251
|
-
if (!message || message.from === this.instanceId) return;
|
|
252
|
-
if (message.kind === "doc-update") for (const entry of listeners) {
|
|
253
|
-
entry.muted = true;
|
|
254
|
-
entry.doc.import(message.payload);
|
|
255
|
-
entry.muted = false;
|
|
256
|
-
entry.resolveFirst();
|
|
257
|
-
}
|
|
258
|
-
else if (message.kind === "doc-request") {
|
|
259
|
-
const first = listeners.values().next().value;
|
|
260
|
-
if (!first) return;
|
|
261
|
-
const payload = message.docId === docId ? first.doc.export({ mode: "snapshot" }) : void 0;
|
|
262
|
-
if (!payload) return;
|
|
263
|
-
postChannelMessage(channel, {
|
|
264
|
-
kind: "doc-update",
|
|
265
|
-
docId,
|
|
266
|
-
from: this.instanceId,
|
|
267
|
-
mode: "snapshot",
|
|
268
|
-
payload
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
channel.addEventListener("message", onMessage);
|
|
273
|
-
const state = {
|
|
274
|
-
channel,
|
|
275
|
-
listeners,
|
|
276
|
-
onMessage
|
|
277
|
-
};
|
|
278
|
-
this.docStates.set(docId, state);
|
|
279
|
-
return state;
|
|
280
|
-
}
|
|
281
|
-
teardownDocChannel(docId) {
|
|
282
|
-
const state = this.docStates.get(docId);
|
|
283
|
-
if (!state) return;
|
|
284
|
-
for (const entry of state.listeners) entry.unsubscribe();
|
|
285
|
-
state.channel.removeEventListener("message", state.onMessage);
|
|
286
|
-
state.channel.close();
|
|
287
|
-
this.docStates.delete(docId);
|
|
288
|
-
}
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
//#endregion
|
|
292
|
-
//#region src/storage/indexeddb.ts
|
|
293
|
-
const DEFAULT_DB_NAME = "loro-repo";
|
|
294
|
-
const DEFAULT_DB_VERSION = 1;
|
|
295
|
-
const DEFAULT_DOC_STORE = "docs";
|
|
296
|
-
const DEFAULT_META_STORE = "meta";
|
|
297
|
-
const DEFAULT_ASSET_STORE = "assets";
|
|
298
|
-
const DEFAULT_DOC_UPDATE_STORE = "doc-updates";
|
|
299
|
-
const DEFAULT_META_KEY = "snapshot";
|
|
300
|
-
const textDecoder$1 = new TextDecoder();
|
|
301
|
-
function describeUnknown(cause) {
|
|
302
|
-
if (typeof cause === "string") return cause;
|
|
303
|
-
if (typeof cause === "number" || typeof cause === "boolean") return String(cause);
|
|
304
|
-
if (typeof cause === "bigint") return cause.toString();
|
|
305
|
-
if (typeof cause === "symbol") return cause.description ?? cause.toString();
|
|
306
|
-
if (typeof cause === "function") return `[function ${cause.name ?? "anonymous"}]`;
|
|
307
|
-
if (cause && typeof cause === "object") try {
|
|
308
|
-
return JSON.stringify(cause);
|
|
309
|
-
} catch {
|
|
310
|
-
return "[object]";
|
|
311
|
-
}
|
|
312
|
-
return String(cause);
|
|
313
|
-
}
|
|
314
|
-
var IndexedDBStorageAdaptor = class {
|
|
315
|
-
idb;
|
|
316
|
-
dbName;
|
|
317
|
-
version;
|
|
318
|
-
docStore;
|
|
319
|
-
docUpdateStore;
|
|
320
|
-
metaStore;
|
|
321
|
-
assetStore;
|
|
322
|
-
metaKey;
|
|
323
|
-
dbPromise;
|
|
324
|
-
closed = false;
|
|
325
|
-
constructor(options = {}) {
|
|
326
|
-
const idbFactory = globalThis.indexedDB;
|
|
327
|
-
if (!idbFactory) throw new Error("IndexedDB is not available in this environment");
|
|
328
|
-
this.idb = idbFactory;
|
|
329
|
-
this.dbName = options.dbName ?? DEFAULT_DB_NAME;
|
|
330
|
-
this.version = options.version ?? DEFAULT_DB_VERSION;
|
|
331
|
-
this.docStore = options.docStoreName ?? DEFAULT_DOC_STORE;
|
|
332
|
-
this.docUpdateStore = options.docUpdateStoreName ?? DEFAULT_DOC_UPDATE_STORE;
|
|
333
|
-
this.metaStore = options.metaStoreName ?? DEFAULT_META_STORE;
|
|
334
|
-
this.assetStore = options.assetStoreName ?? DEFAULT_ASSET_STORE;
|
|
335
|
-
this.metaKey = options.metaKey ?? DEFAULT_META_KEY;
|
|
336
|
-
}
|
|
337
|
-
async save(payload) {
|
|
338
|
-
const db = await this.ensureDb();
|
|
339
|
-
switch (payload.type) {
|
|
340
|
-
case "doc-snapshot": {
|
|
341
|
-
const snapshot = payload.snapshot.slice();
|
|
342
|
-
await this.storeMergedSnapshot(db, payload.docId, snapshot);
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
case "doc-update": {
|
|
346
|
-
const update = payload.update.slice();
|
|
347
|
-
await this.appendDocUpdate(db, payload.docId, update);
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
case "asset": {
|
|
351
|
-
const bytes = payload.data.slice();
|
|
352
|
-
await this.putBinary(db, this.assetStore, payload.assetId, bytes);
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
355
|
-
case "meta": {
|
|
356
|
-
const bytes = payload.update.slice();
|
|
357
|
-
await this.putBinary(db, this.metaStore, this.metaKey, bytes);
|
|
358
|
-
break;
|
|
359
|
-
}
|
|
360
|
-
default: throw new Error("Unsupported storage payload type");
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
async deleteAsset(assetId) {
|
|
364
|
-
const db = await this.ensureDb();
|
|
365
|
-
await this.deleteKey(db, this.assetStore, assetId);
|
|
366
|
-
}
|
|
367
|
-
async loadDoc(docId) {
|
|
368
|
-
const db = await this.ensureDb();
|
|
369
|
-
const snapshot = await this.getBinaryFromDb(db, this.docStore, docId);
|
|
370
|
-
const pendingUpdates = await this.getDocUpdates(db, docId);
|
|
371
|
-
if (!snapshot && pendingUpdates.length === 0) return;
|
|
372
|
-
let doc;
|
|
373
|
-
try {
|
|
374
|
-
doc = snapshot ? loro_crdt.LoroDoc.fromSnapshot(snapshot) : new loro_crdt.LoroDoc();
|
|
375
|
-
} catch (error) {
|
|
376
|
-
throw this.createError(`Failed to hydrate document snapshot for "${docId}"`, error);
|
|
377
|
-
}
|
|
378
|
-
let appliedUpdates = false;
|
|
379
|
-
for (const update of pendingUpdates) try {
|
|
380
|
-
doc.import(update);
|
|
381
|
-
appliedUpdates = true;
|
|
382
|
-
} catch (error) {
|
|
383
|
-
throw this.createError(`Failed to apply queued document update for "${docId}"`, error);
|
|
384
|
-
}
|
|
385
|
-
if (appliedUpdates) {
|
|
386
|
-
let consolidated;
|
|
387
|
-
try {
|
|
388
|
-
consolidated = doc.export({ mode: "snapshot" });
|
|
389
|
-
} catch (error) {
|
|
390
|
-
throw this.createError(`Failed to export consolidated snapshot for "${docId}"`, error);
|
|
391
|
-
}
|
|
392
|
-
await this.writeSnapshot(db, docId, consolidated);
|
|
393
|
-
await this.clearDocUpdates(db, docId);
|
|
394
|
-
}
|
|
395
|
-
return doc;
|
|
396
|
-
}
|
|
397
|
-
async loadMeta() {
|
|
398
|
-
const bytes = await this.getBinary(this.metaStore, this.metaKey);
|
|
399
|
-
if (!bytes) return void 0;
|
|
400
|
-
try {
|
|
401
|
-
const json = textDecoder$1.decode(bytes);
|
|
402
|
-
const bundle = JSON.parse(json);
|
|
403
|
-
const flock = new __loro_dev_flock.Flock();
|
|
404
|
-
flock.importJson(bundle);
|
|
405
|
-
return flock;
|
|
406
|
-
} catch (error) {
|
|
407
|
-
throw this.createError("Failed to hydrate metadata snapshot", error);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
async loadAsset(assetId) {
|
|
411
|
-
return await this.getBinary(this.assetStore, assetId) ?? void 0;
|
|
412
|
-
}
|
|
413
|
-
async close() {
|
|
414
|
-
this.closed = true;
|
|
415
|
-
const db = await this.dbPromise;
|
|
416
|
-
if (db) db.close();
|
|
417
|
-
this.dbPromise = void 0;
|
|
418
|
-
}
|
|
419
|
-
async ensureDb() {
|
|
420
|
-
if (this.closed) throw new Error("IndexedDBStorageAdaptor has been closed");
|
|
421
|
-
if (!this.dbPromise) this.dbPromise = new Promise((resolve, reject) => {
|
|
422
|
-
const request = this.idb.open(this.dbName, this.version);
|
|
423
|
-
request.addEventListener("upgradeneeded", () => {
|
|
424
|
-
const db = request.result;
|
|
425
|
-
this.ensureStore(db, this.docStore);
|
|
426
|
-
this.ensureStore(db, this.docUpdateStore);
|
|
427
|
-
this.ensureStore(db, this.metaStore);
|
|
428
|
-
this.ensureStore(db, this.assetStore);
|
|
429
|
-
});
|
|
430
|
-
request.addEventListener("success", () => resolve(request.result), { once: true });
|
|
431
|
-
request.addEventListener("error", () => {
|
|
432
|
-
reject(this.createError(`Failed to open IndexedDB database "${this.dbName}"`, request.error));
|
|
433
|
-
}, { once: true });
|
|
434
|
-
});
|
|
435
|
-
return this.dbPromise;
|
|
436
|
-
}
|
|
437
|
-
ensureStore(db, storeName) {
|
|
438
|
-
const names = db.objectStoreNames;
|
|
439
|
-
if (this.storeExists(names, storeName)) return;
|
|
440
|
-
db.createObjectStore(storeName);
|
|
441
|
-
}
|
|
442
|
-
storeExists(names, storeName) {
|
|
443
|
-
if (typeof names.contains === "function") return names.contains(storeName);
|
|
444
|
-
const length = names.length ?? 0;
|
|
445
|
-
for (let index = 0; index < length; index += 1) if (names.item?.(index) === storeName) return true;
|
|
446
|
-
return false;
|
|
447
|
-
}
|
|
448
|
-
async storeMergedSnapshot(db, docId, incoming) {
|
|
449
|
-
await this.runInTransaction(db, this.docStore, "readwrite", async (store) => {
|
|
450
|
-
const existingRaw = await this.wrapRequest(store.get(docId), "read");
|
|
451
|
-
const existing = await this.normalizeBinary(existingRaw);
|
|
452
|
-
const merged = this.mergeSnapshots(docId, existing, incoming);
|
|
453
|
-
await this.wrapRequest(store.put(merged, docId), "write");
|
|
454
|
-
});
|
|
455
|
-
}
|
|
456
|
-
mergeSnapshots(docId, existing, incoming) {
|
|
457
|
-
try {
|
|
458
|
-
const doc = existing ? loro_crdt.LoroDoc.fromSnapshot(existing) : new loro_crdt.LoroDoc();
|
|
459
|
-
doc.import(incoming);
|
|
460
|
-
return doc.export({ mode: "snapshot" });
|
|
461
|
-
} catch (error) {
|
|
462
|
-
throw this.createError(`Failed to merge snapshot for "${docId}"`, error);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
async appendDocUpdate(db, docId, update) {
|
|
466
|
-
await this.runInTransaction(db, this.docUpdateStore, "readwrite", async (store) => {
|
|
467
|
-
const raw = await this.wrapRequest(store.get(docId), "read");
|
|
468
|
-
const queue = await this.normalizeUpdateQueue(raw);
|
|
469
|
-
queue.push(update.slice());
|
|
470
|
-
await this.wrapRequest(store.put({ updates: queue }, docId), "write");
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
async getDocUpdates(db, docId) {
|
|
474
|
-
const raw = await this.runInTransaction(db, this.docUpdateStore, "readonly", (store) => this.wrapRequest(store.get(docId), "read"));
|
|
475
|
-
return this.normalizeUpdateQueue(raw);
|
|
476
|
-
}
|
|
477
|
-
async clearDocUpdates(db, docId) {
|
|
478
|
-
await this.runInTransaction(db, this.docUpdateStore, "readwrite", (store) => this.wrapRequest(store.delete(docId), "delete"));
|
|
479
|
-
}
|
|
480
|
-
async writeSnapshot(db, docId, snapshot) {
|
|
481
|
-
await this.putBinary(db, this.docStore, docId, snapshot.slice());
|
|
482
|
-
}
|
|
483
|
-
async getBinaryFromDb(db, storeName, key) {
|
|
484
|
-
const value = await this.runInTransaction(db, storeName, "readonly", (store) => this.wrapRequest(store.get(key), "read"));
|
|
485
|
-
return this.normalizeBinary(value);
|
|
486
|
-
}
|
|
487
|
-
async normalizeUpdateQueue(value) {
|
|
488
|
-
if (value == null) return [];
|
|
489
|
-
const list = Array.isArray(value) ? value : typeof value === "object" && value !== null ? value.updates : void 0;
|
|
490
|
-
if (!Array.isArray(list)) return [];
|
|
491
|
-
const queue = [];
|
|
492
|
-
for (const entry of list) {
|
|
493
|
-
const bytes = await this.normalizeBinary(entry);
|
|
494
|
-
if (bytes) queue.push(bytes);
|
|
495
|
-
}
|
|
496
|
-
return queue;
|
|
497
|
-
}
|
|
498
|
-
async putBinary(db, storeName, key, value) {
|
|
499
|
-
await this.runInTransaction(db, storeName, "readwrite", (store) => this.wrapRequest(store.put(value, key), "write"));
|
|
500
|
-
}
|
|
501
|
-
async deleteKey(db, storeName, key) {
|
|
502
|
-
await this.runInTransaction(db, storeName, "readwrite", (store) => this.wrapRequest(store.delete(key), "delete"));
|
|
503
|
-
}
|
|
504
|
-
async getBinary(storeName, key) {
|
|
505
|
-
const db = await this.ensureDb();
|
|
506
|
-
return this.getBinaryFromDb(db, storeName, key);
|
|
507
|
-
}
|
|
508
|
-
runInTransaction(db, storeName, mode, executor) {
|
|
509
|
-
const tx = db.transaction(storeName, mode);
|
|
510
|
-
const store = tx.objectStore(storeName);
|
|
511
|
-
const completion = new Promise((resolve, reject) => {
|
|
512
|
-
tx.addEventListener("complete", () => resolve(), { once: true });
|
|
513
|
-
tx.addEventListener("abort", () => reject(this.createError("IndexedDB transaction aborted", tx.error)), { once: true });
|
|
514
|
-
tx.addEventListener("error", () => reject(this.createError("IndexedDB transaction failed", tx.error)), { once: true });
|
|
515
|
-
});
|
|
516
|
-
return Promise.all([executor(store), completion]).then(([result]) => result);
|
|
517
|
-
}
|
|
518
|
-
wrapRequest(request, action) {
|
|
519
|
-
return new Promise((resolve, reject) => {
|
|
520
|
-
request.addEventListener("success", () => resolve(request.result), { once: true });
|
|
521
|
-
request.addEventListener("error", () => reject(this.createError(`IndexedDB request failed during ${action}`, request.error)), { once: true });
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
async normalizeBinary(value) {
|
|
525
|
-
if (value == null) return void 0;
|
|
526
|
-
if (value instanceof Uint8Array) return value.slice();
|
|
527
|
-
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength).slice();
|
|
528
|
-
if (value instanceof ArrayBuffer) return new Uint8Array(value.slice(0));
|
|
529
|
-
if (typeof value === "object" && value !== null && "arrayBuffer" in value) {
|
|
530
|
-
const candidate = value;
|
|
531
|
-
if (typeof candidate.arrayBuffer === "function") {
|
|
532
|
-
const buffer = await candidate.arrayBuffer();
|
|
533
|
-
return new Uint8Array(buffer);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
createError(message, cause) {
|
|
538
|
-
if (cause instanceof Error) return new Error(`${message}: ${cause.message}`, { cause });
|
|
539
|
-
if (cause !== void 0 && cause !== null) return /* @__PURE__ */ new Error(`${message}: ${describeUnknown(cause)}`);
|
|
540
|
-
return new Error(message);
|
|
541
|
-
}
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
//#endregion
|
|
545
|
-
//#region src/storage/filesystem.ts
|
|
546
|
-
const textDecoder = new TextDecoder();
|
|
547
|
-
var FileSystemStorageAdaptor = class {
|
|
548
|
-
baseDir;
|
|
549
|
-
docsDir;
|
|
550
|
-
assetsDir;
|
|
551
|
-
metaPath;
|
|
552
|
-
initPromise;
|
|
553
|
-
updateCounter = 0;
|
|
554
|
-
constructor(options = {}) {
|
|
555
|
-
this.baseDir = node_path.resolve(options.baseDir ?? node_path.join(process.cwd(), ".loro-repo"));
|
|
556
|
-
this.docsDir = node_path.join(this.baseDir, options.docsDirName ?? "docs");
|
|
557
|
-
this.assetsDir = node_path.join(this.baseDir, options.assetsDirName ?? "assets");
|
|
558
|
-
this.metaPath = node_path.join(this.baseDir, options.metaFileName ?? "meta.json");
|
|
559
|
-
this.initPromise = this.ensureLayout();
|
|
560
|
-
}
|
|
561
|
-
async save(payload) {
|
|
562
|
-
await this.initPromise;
|
|
563
|
-
switch (payload.type) {
|
|
564
|
-
case "doc-snapshot":
|
|
565
|
-
await this.writeDocSnapshot(payload.docId, payload.snapshot);
|
|
566
|
-
return;
|
|
567
|
-
case "doc-update":
|
|
568
|
-
await this.enqueueDocUpdate(payload.docId, payload.update);
|
|
569
|
-
return;
|
|
570
|
-
case "asset":
|
|
571
|
-
await this.writeAsset(payload.assetId, payload.data);
|
|
572
|
-
return;
|
|
573
|
-
case "meta":
|
|
574
|
-
await writeFileAtomic(this.metaPath, payload.update);
|
|
575
|
-
return;
|
|
576
|
-
default: throw new Error(`Unsupported payload type: ${payload.type}`);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
async deleteAsset(assetId) {
|
|
580
|
-
await this.initPromise;
|
|
581
|
-
await removeIfExists(this.assetPath(assetId));
|
|
582
|
-
}
|
|
583
|
-
async loadDoc(docId) {
|
|
584
|
-
await this.initPromise;
|
|
585
|
-
const snapshotBytes = await readFileIfExists(this.docSnapshotPath(docId));
|
|
586
|
-
const updateDir = this.docUpdatesDir(docId);
|
|
587
|
-
const updateFiles = await listFiles(updateDir);
|
|
588
|
-
if (!snapshotBytes && updateFiles.length === 0) return;
|
|
589
|
-
const doc = snapshotBytes ? loro_crdt.LoroDoc.fromSnapshot(snapshotBytes) : new loro_crdt.LoroDoc();
|
|
590
|
-
if (updateFiles.length === 0) return doc;
|
|
591
|
-
const updatePaths = updateFiles.map((file) => node_path.join(updateDir, file));
|
|
592
|
-
for (const updatePath of updatePaths) {
|
|
593
|
-
const update = await readFileIfExists(updatePath);
|
|
594
|
-
if (!update) continue;
|
|
595
|
-
doc.import(update);
|
|
596
|
-
}
|
|
597
|
-
await Promise.all(updatePaths.map((filePath) => removeIfExists(filePath)));
|
|
598
|
-
const consolidated = doc.export({ mode: "snapshot" });
|
|
599
|
-
await this.writeDocSnapshot(docId, consolidated);
|
|
600
|
-
return doc;
|
|
601
|
-
}
|
|
602
|
-
async loadMeta() {
|
|
603
|
-
await this.initPromise;
|
|
604
|
-
const bytes = await readFileIfExists(this.metaPath);
|
|
605
|
-
if (!bytes) return void 0;
|
|
606
|
-
try {
|
|
607
|
-
const bundle = JSON.parse(textDecoder.decode(bytes));
|
|
608
|
-
const flock = new __loro_dev_flock.Flock();
|
|
609
|
-
flock.importJson(bundle);
|
|
610
|
-
return flock;
|
|
611
|
-
} catch (error) {
|
|
612
|
-
throw new Error("Failed to hydrate metadata snapshot", { cause: error });
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
async loadAsset(assetId) {
|
|
616
|
-
await this.initPromise;
|
|
617
|
-
return readFileIfExists(this.assetPath(assetId));
|
|
618
|
-
}
|
|
619
|
-
async ensureLayout() {
|
|
620
|
-
await Promise.all([
|
|
621
|
-
ensureDir(this.baseDir),
|
|
622
|
-
ensureDir(this.docsDir),
|
|
623
|
-
ensureDir(this.assetsDir)
|
|
624
|
-
]);
|
|
625
|
-
}
|
|
626
|
-
async writeDocSnapshot(docId, snapshot) {
|
|
627
|
-
await ensureDir(this.docDir(docId));
|
|
628
|
-
await writeFileAtomic(this.docSnapshotPath(docId), snapshot);
|
|
629
|
-
}
|
|
630
|
-
async enqueueDocUpdate(docId, update) {
|
|
631
|
-
const dir = this.docUpdatesDir(docId);
|
|
632
|
-
await ensureDir(dir);
|
|
633
|
-
const counter = this.updateCounter = (this.updateCounter + 1) % 1e6;
|
|
634
|
-
const fileName = `${Date.now().toString().padStart(13, "0")}-${counter.toString().padStart(6, "0")}.bin`;
|
|
635
|
-
await writeFileAtomic(node_path.join(dir, fileName), update);
|
|
636
|
-
}
|
|
637
|
-
async writeAsset(assetId, data) {
|
|
638
|
-
const filePath = this.assetPath(assetId);
|
|
639
|
-
await ensureDir(node_path.dirname(filePath));
|
|
640
|
-
await writeFileAtomic(filePath, data);
|
|
641
|
-
}
|
|
642
|
-
docDir(docId) {
|
|
643
|
-
return node_path.join(this.docsDir, encodeComponent(docId));
|
|
644
|
-
}
|
|
645
|
-
docSnapshotPath(docId) {
|
|
646
|
-
return node_path.join(this.docDir(docId), "snapshot.bin");
|
|
647
|
-
}
|
|
648
|
-
docUpdatesDir(docId) {
|
|
649
|
-
return node_path.join(this.docDir(docId), "updates");
|
|
650
|
-
}
|
|
651
|
-
assetPath(assetId) {
|
|
652
|
-
return node_path.join(this.assetsDir, encodeComponent(assetId));
|
|
653
|
-
}
|
|
654
|
-
};
|
|
655
|
-
function encodeComponent(value) {
|
|
656
|
-
return Buffer.from(value, "utf8").toString("base64url");
|
|
657
|
-
}
|
|
658
|
-
async function ensureDir(dir) {
|
|
659
|
-
await node_fs.promises.mkdir(dir, { recursive: true });
|
|
660
|
-
}
|
|
661
|
-
async function readFileIfExists(filePath) {
|
|
662
|
-
try {
|
|
663
|
-
const data = await node_fs.promises.readFile(filePath);
|
|
664
|
-
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice();
|
|
665
|
-
} catch (error) {
|
|
666
|
-
if (error.code === "ENOENT") return;
|
|
667
|
-
throw error;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
async function removeIfExists(filePath) {
|
|
671
|
-
try {
|
|
672
|
-
await node_fs.promises.rm(filePath);
|
|
673
|
-
} catch (error) {
|
|
674
|
-
if (error.code === "ENOENT") return;
|
|
675
|
-
throw error;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
async function listFiles(dir) {
|
|
679
|
-
try {
|
|
680
|
-
return (await node_fs.promises.readdir(dir)).sort();
|
|
681
|
-
} catch (error) {
|
|
682
|
-
if (error.code === "ENOENT") return [];
|
|
683
|
-
throw error;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
async function writeFileAtomic(targetPath, data) {
|
|
687
|
-
const dir = node_path.dirname(targetPath);
|
|
688
|
-
await ensureDir(dir);
|
|
689
|
-
const tempPath = node_path.join(dir, `.tmp-${(0, node_crypto.randomUUID)()}`);
|
|
690
|
-
await node_fs.promises.writeFile(tempPath, data);
|
|
691
|
-
await node_fs.promises.rename(tempPath, targetPath);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
//#endregion
|
|
695
|
-
//#region src/internal/debug.ts
|
|
696
|
-
const getEnv = () => {
|
|
697
|
-
if (typeof globalThis !== "object" || globalThis === null) return;
|
|
698
|
-
return globalThis.process?.env;
|
|
699
|
-
};
|
|
700
|
-
const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
|
|
701
|
-
const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
|
|
702
|
-
const wildcardTokens = new Set([
|
|
703
|
-
"*",
|
|
704
|
-
"1",
|
|
705
|
-
"true",
|
|
706
|
-
"all"
|
|
707
|
-
]);
|
|
708
|
-
const namespaceSet = new Set(normalizedNamespaces);
|
|
709
|
-
const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
|
|
710
|
-
const isDebugEnabled = (namespace) => {
|
|
711
|
-
if (!namespaceSet.size) return false;
|
|
712
|
-
if (!namespace) return hasWildcard;
|
|
713
|
-
const normalized = namespace.toLowerCase();
|
|
714
|
-
if (hasWildcard) return true;
|
|
715
|
-
if (namespaceSet.has(normalized)) return true;
|
|
716
|
-
const [root] = normalized.split(":");
|
|
717
|
-
return namespaceSet.has(root);
|
|
718
|
-
};
|
|
719
|
-
const createDebugLogger = (namespace) => {
|
|
720
|
-
const normalized = namespace.toLowerCase();
|
|
721
|
-
return (...args) => {
|
|
722
|
-
if (!isDebugEnabled(normalized)) return;
|
|
723
|
-
const prefix = `[loro-repo:${namespace}]`;
|
|
724
|
-
if (args.length === 0) {
|
|
725
|
-
console.info(prefix);
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
console.info(prefix, ...args);
|
|
729
|
-
};
|
|
730
|
-
};
|
|
731
|
-
|
|
732
|
-
//#endregion
|
|
733
|
-
//#region src/transport/websocket.ts
|
|
734
|
-
const debug = createDebugLogger("transport:websocket");
|
|
735
|
-
function withTimeout(promise, timeoutMs) {
|
|
736
|
-
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
737
|
-
return new Promise((resolve, reject) => {
|
|
738
|
-
const timer = setTimeout(() => {
|
|
739
|
-
reject(/* @__PURE__ */ new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
740
|
-
}, timeoutMs);
|
|
741
|
-
promise.then((value) => {
|
|
742
|
-
clearTimeout(timer);
|
|
743
|
-
resolve(value);
|
|
744
|
-
}).catch((error) => {
|
|
745
|
-
clearTimeout(timer);
|
|
746
|
-
reject(error);
|
|
747
|
-
});
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
function normalizeRoomId(roomId, fallback) {
|
|
751
|
-
if (typeof roomId === "string" && roomId.length > 0) return roomId;
|
|
752
|
-
if (roomId instanceof Uint8Array && roomId.length > 0) try {
|
|
753
|
-
return (0, loro_protocol.bytesToHex)(roomId);
|
|
754
|
-
} catch {
|
|
755
|
-
return fallback;
|
|
756
|
-
}
|
|
757
|
-
return fallback;
|
|
758
|
-
}
|
|
759
|
-
function bytesEqual(a, b) {
|
|
760
|
-
if (a === b) return true;
|
|
761
|
-
if (!a || !b) return false;
|
|
762
|
-
if (a.length !== b.length) return false;
|
|
763
|
-
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
764
|
-
return true;
|
|
765
|
-
}
|
|
766
|
-
/**
|
|
767
|
-
* loro-websocket backed {@link TransportAdapter} implementation for LoroRepo.
|
|
768
|
-
* It uses loro-protocol as the underlying protocol for the transport.
|
|
769
|
-
*/
|
|
770
|
-
var WebSocketTransportAdapter = class {
|
|
771
|
-
options;
|
|
772
|
-
client;
|
|
773
|
-
metadataSession;
|
|
774
|
-
docSessions = /* @__PURE__ */ new Map();
|
|
775
|
-
constructor(options) {
|
|
776
|
-
this.options = options;
|
|
777
|
-
}
|
|
778
|
-
async connect(_options) {
|
|
779
|
-
const client = this.ensureClient();
|
|
780
|
-
debug("connect requested", { status: client.getStatus() });
|
|
781
|
-
try {
|
|
782
|
-
await client.connect();
|
|
783
|
-
debug("client.connect resolved");
|
|
784
|
-
await client.waitConnected();
|
|
785
|
-
debug("client.waitConnected resolved", { status: client.getStatus() });
|
|
786
|
-
} catch (error) {
|
|
787
|
-
debug("connect failed", error);
|
|
788
|
-
throw error;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
async close() {
|
|
792
|
-
debug("close requested", {
|
|
793
|
-
docSessions: this.docSessions.size,
|
|
794
|
-
metadataSession: Boolean(this.metadataSession)
|
|
795
|
-
});
|
|
796
|
-
for (const [docId] of this.docSessions) await this.leaveDocSession(docId).catch(() => {});
|
|
797
|
-
this.docSessions.clear();
|
|
798
|
-
await this.teardownMetadataSession().catch(() => {});
|
|
799
|
-
if (this.client) {
|
|
800
|
-
const client = this.client;
|
|
801
|
-
this.client = void 0;
|
|
802
|
-
client.destroy();
|
|
803
|
-
debug("websocket client destroyed");
|
|
804
|
-
}
|
|
805
|
-
debug("close completed");
|
|
806
|
-
}
|
|
807
|
-
isConnected() {
|
|
808
|
-
return this.client?.getStatus() === "connected";
|
|
809
|
-
}
|
|
810
|
-
async syncMeta(flock, options) {
|
|
811
|
-
debug("syncMeta requested", { roomId: this.options.metadataRoomId });
|
|
812
|
-
try {
|
|
813
|
-
let auth;
|
|
814
|
-
if (this.options.metadataAuth) if (typeof this.options.metadataAuth === "function") auth = await this.options.metadataAuth();
|
|
815
|
-
else auth = this.options.metadataAuth;
|
|
816
|
-
await withTimeout((await this.ensureMetadataSession(flock, {
|
|
817
|
-
roomId: this.options.metadataRoomId ?? "repo:meta",
|
|
818
|
-
auth
|
|
819
|
-
})).firstSynced, options?.timeout);
|
|
820
|
-
debug("syncMeta completed", { roomId: this.options.metadataRoomId });
|
|
821
|
-
return { ok: true };
|
|
822
|
-
} catch (error) {
|
|
823
|
-
debug("syncMeta failed", error);
|
|
824
|
-
return { ok: false };
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
joinMetaRoom(flock, params) {
|
|
828
|
-
const fallback = this.options.metadataRoomId ?? "";
|
|
829
|
-
const roomId = normalizeRoomId(params?.roomId, fallback);
|
|
830
|
-
if (!roomId) throw new Error("Metadata room id not configured");
|
|
831
|
-
const session = (async () => {
|
|
832
|
-
let auth;
|
|
833
|
-
const authWay = params?.auth ?? this.options.metadataAuth;
|
|
834
|
-
if (typeof authWay === "function") auth = await authWay();
|
|
835
|
-
else auth = authWay;
|
|
836
|
-
debug("joinMetaRoom requested", {
|
|
837
|
-
roomId,
|
|
838
|
-
hasAuth: Boolean(auth && auth.length)
|
|
839
|
-
});
|
|
840
|
-
return this.ensureMetadataSession(flock, {
|
|
841
|
-
roomId,
|
|
842
|
-
auth
|
|
843
|
-
});
|
|
844
|
-
})();
|
|
845
|
-
const firstSynced = session.then((session$1) => session$1.firstSynced);
|
|
846
|
-
const getConnected = () => this.isConnected();
|
|
847
|
-
const subscription = {
|
|
848
|
-
unsubscribe: () => {
|
|
849
|
-
session.then((session$1) => {
|
|
850
|
-
session$1.refCount = Math.max(0, session$1.refCount - 1);
|
|
851
|
-
debug("metadata session refCount decremented", {
|
|
852
|
-
roomId: session$1.roomId,
|
|
853
|
-
refCount: session$1.refCount
|
|
854
|
-
});
|
|
855
|
-
if (session$1.refCount === 0) {
|
|
856
|
-
debug("tearing down metadata session due to refCount=0", { roomId: session$1.roomId });
|
|
857
|
-
this.teardownMetadataSession(session$1).catch(() => {});
|
|
858
|
-
}
|
|
859
|
-
});
|
|
860
|
-
},
|
|
861
|
-
firstSyncedWithRemote: firstSynced,
|
|
862
|
-
get connected() {
|
|
863
|
-
return getConnected();
|
|
864
|
-
}
|
|
865
|
-
};
|
|
866
|
-
session.then((session$1) => {
|
|
867
|
-
session$1.refCount += 1;
|
|
868
|
-
debug("metadata session refCount incremented", {
|
|
869
|
-
roomId: session$1.roomId,
|
|
870
|
-
refCount: session$1.refCount
|
|
871
|
-
});
|
|
872
|
-
});
|
|
873
|
-
return subscription;
|
|
874
|
-
}
|
|
875
|
-
async syncDoc(docId, doc, options) {
|
|
876
|
-
debug("syncDoc requested", { docId });
|
|
877
|
-
try {
|
|
878
|
-
const session = await this.ensureDocSession(docId, doc, {});
|
|
879
|
-
await withTimeout(session.firstSynced, options?.timeout);
|
|
880
|
-
debug("syncDoc completed", {
|
|
881
|
-
docId,
|
|
882
|
-
roomId: session.roomId
|
|
883
|
-
});
|
|
884
|
-
return { ok: true };
|
|
885
|
-
} catch (error) {
|
|
886
|
-
debug("syncDoc failed", {
|
|
887
|
-
docId,
|
|
888
|
-
error
|
|
889
|
-
});
|
|
890
|
-
return { ok: false };
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
joinDocRoom(docId, doc, params) {
|
|
894
|
-
debug("joinDocRoom requested", {
|
|
895
|
-
docId,
|
|
896
|
-
roomParamType: params?.roomId ? typeof params.roomId === "string" ? "string" : "uint8array" : void 0,
|
|
897
|
-
hasAuthOverride: Boolean(params?.auth && params.auth.length)
|
|
898
|
-
});
|
|
899
|
-
const ensure = this.ensureDocSession(docId, doc, params ?? {});
|
|
900
|
-
const firstSynced = ensure.then((session) => session.firstSynced);
|
|
901
|
-
const getConnected = () => this.isConnected();
|
|
902
|
-
const subscription = {
|
|
903
|
-
unsubscribe: () => {
|
|
904
|
-
ensure.then((session) => {
|
|
905
|
-
session.refCount = Math.max(0, session.refCount - 1);
|
|
906
|
-
debug("doc session refCount decremented", {
|
|
907
|
-
docId,
|
|
908
|
-
roomId: session.roomId,
|
|
909
|
-
refCount: session.refCount
|
|
910
|
-
});
|
|
911
|
-
if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
|
|
912
|
-
});
|
|
913
|
-
},
|
|
914
|
-
firstSyncedWithRemote: firstSynced,
|
|
915
|
-
get connected() {
|
|
916
|
-
return getConnected();
|
|
917
|
-
}
|
|
918
|
-
};
|
|
919
|
-
ensure.then((session) => {
|
|
920
|
-
session.refCount += 1;
|
|
921
|
-
debug("doc session refCount incremented", {
|
|
922
|
-
docId,
|
|
923
|
-
roomId: session.roomId,
|
|
924
|
-
refCount: session.refCount
|
|
925
|
-
});
|
|
926
|
-
});
|
|
927
|
-
return subscription;
|
|
928
|
-
}
|
|
929
|
-
ensureClient() {
|
|
930
|
-
if (this.client) {
|
|
931
|
-
debug("reusing websocket client", { status: this.client.getStatus() });
|
|
932
|
-
return this.client;
|
|
933
|
-
}
|
|
934
|
-
const { url, client: clientOptions } = this.options;
|
|
935
|
-
debug("creating websocket client", {
|
|
936
|
-
url,
|
|
937
|
-
clientOptionsKeys: clientOptions ? Object.keys(clientOptions) : []
|
|
938
|
-
});
|
|
939
|
-
const client = new loro_websocket.LoroWebsocketClient({
|
|
940
|
-
url,
|
|
941
|
-
...clientOptions
|
|
942
|
-
});
|
|
943
|
-
this.client = client;
|
|
944
|
-
return client;
|
|
945
|
-
}
|
|
946
|
-
async ensureMetadataSession(flock, params) {
|
|
947
|
-
debug("ensureMetadataSession invoked", {
|
|
948
|
-
roomId: params.roomId,
|
|
949
|
-
hasAuth: Boolean(params.auth && params.auth.length)
|
|
950
|
-
});
|
|
951
|
-
const client = this.ensureClient();
|
|
952
|
-
await client.waitConnected();
|
|
953
|
-
debug("websocket client ready for metadata session", { status: client.getStatus() });
|
|
954
|
-
if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) {
|
|
955
|
-
debug("reusing metadata session", {
|
|
956
|
-
roomId: this.metadataSession.roomId,
|
|
957
|
-
refCount: this.metadataSession.refCount
|
|
958
|
-
});
|
|
959
|
-
return this.metadataSession;
|
|
960
|
-
}
|
|
961
|
-
if (this.metadataSession) {
|
|
962
|
-
debug("tearing down previous metadata session", { roomId: this.metadataSession.roomId });
|
|
963
|
-
await this.teardownMetadataSession(this.metadataSession).catch(() => {});
|
|
964
|
-
}
|
|
965
|
-
const adaptor = new loro_adaptors_flock.FlockAdaptor(flock, this.options.metadataAdaptorConfig);
|
|
966
|
-
debug("joining metadata room", {
|
|
967
|
-
roomId: params.roomId,
|
|
968
|
-
hasAuth: Boolean(params.auth && params.auth.length)
|
|
969
|
-
});
|
|
970
|
-
const room = await client.join({
|
|
971
|
-
roomId: params.roomId,
|
|
972
|
-
crdtAdaptor: adaptor,
|
|
973
|
-
auth: params.auth
|
|
974
|
-
});
|
|
975
|
-
const firstSynced = room.waitForReachingServerVersion();
|
|
976
|
-
firstSynced.then(() => {
|
|
977
|
-
debug("metadata session firstSynced resolved", { roomId: params.roomId });
|
|
978
|
-
}, (error) => {
|
|
979
|
-
debug("metadata session firstSynced rejected", {
|
|
980
|
-
roomId: params.roomId,
|
|
981
|
-
error
|
|
982
|
-
});
|
|
983
|
-
});
|
|
984
|
-
const session = {
|
|
985
|
-
adaptor,
|
|
986
|
-
room,
|
|
987
|
-
firstSynced,
|
|
988
|
-
flock,
|
|
989
|
-
roomId: params.roomId,
|
|
990
|
-
auth: params.auth,
|
|
991
|
-
refCount: 0
|
|
992
|
-
};
|
|
993
|
-
this.metadataSession = session;
|
|
994
|
-
return session;
|
|
995
|
-
}
|
|
996
|
-
async teardownMetadataSession(session) {
|
|
997
|
-
const target = session ?? this.metadataSession;
|
|
998
|
-
if (!target) return;
|
|
999
|
-
debug("teardownMetadataSession invoked", { roomId: target.roomId });
|
|
1000
|
-
if (this.metadataSession === target) this.metadataSession = void 0;
|
|
1001
|
-
const { adaptor, room } = target;
|
|
1002
|
-
try {
|
|
1003
|
-
await room.leave();
|
|
1004
|
-
debug("metadata room left", { roomId: target.roomId });
|
|
1005
|
-
} catch (error) {
|
|
1006
|
-
debug("metadata room leave failed; destroying", {
|
|
1007
|
-
roomId: target.roomId,
|
|
1008
|
-
error
|
|
1009
|
-
});
|
|
1010
|
-
await room.destroy().catch(() => {});
|
|
1011
|
-
}
|
|
1012
|
-
adaptor.destroy();
|
|
1013
|
-
debug("metadata session destroyed", { roomId: target.roomId });
|
|
1014
|
-
}
|
|
1015
|
-
async ensureDocSession(docId, doc, params) {
|
|
1016
|
-
debug("ensureDocSession invoked", { docId });
|
|
1017
|
-
const client = this.ensureClient();
|
|
1018
|
-
await client.waitConnected();
|
|
1019
|
-
debug("websocket client ready for doc session", {
|
|
1020
|
-
docId,
|
|
1021
|
-
status: client.getStatus()
|
|
1022
|
-
});
|
|
1023
|
-
const existing = this.docSessions.get(docId);
|
|
1024
|
-
const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
|
|
1025
|
-
const roomId = normalizeRoomId(params.roomId, derivedRoomId);
|
|
1026
|
-
let auth;
|
|
1027
|
-
auth = await (params.auth ?? this.options.docAuth?.(docId));
|
|
1028
|
-
debug("doc session params resolved", {
|
|
1029
|
-
docId,
|
|
1030
|
-
roomId,
|
|
1031
|
-
hasAuth: Boolean(auth && auth.length)
|
|
1032
|
-
});
|
|
1033
|
-
if (existing && existing.doc === doc && existing.roomId === roomId) {
|
|
1034
|
-
debug("reusing doc session", {
|
|
1035
|
-
docId,
|
|
1036
|
-
roomId,
|
|
1037
|
-
refCount: existing.refCount
|
|
1038
|
-
});
|
|
1039
|
-
return existing;
|
|
1040
|
-
}
|
|
1041
|
-
if (existing) {
|
|
1042
|
-
debug("doc session mismatch; leaving existing session", {
|
|
1043
|
-
docId,
|
|
1044
|
-
previousRoomId: existing.roomId,
|
|
1045
|
-
nextRoomId: roomId
|
|
1046
|
-
});
|
|
1047
|
-
await this.leaveDocSession(docId).catch(() => {});
|
|
1048
|
-
}
|
|
1049
|
-
const adaptor = new loro_adaptors_loro.LoroAdaptor(doc);
|
|
1050
|
-
debug("joining doc room", {
|
|
1051
|
-
docId,
|
|
1052
|
-
roomId,
|
|
1053
|
-
hasAuth: Boolean(auth && auth.length)
|
|
1054
|
-
});
|
|
1055
|
-
const room = await client.join({
|
|
1056
|
-
roomId,
|
|
1057
|
-
crdtAdaptor: adaptor,
|
|
1058
|
-
auth
|
|
1059
|
-
});
|
|
1060
|
-
const firstSynced = room.waitForReachingServerVersion();
|
|
1061
|
-
firstSynced.then(() => {
|
|
1062
|
-
debug("doc session firstSynced resolved", {
|
|
1063
|
-
docId,
|
|
1064
|
-
roomId
|
|
1065
|
-
});
|
|
1066
|
-
}, (error) => {
|
|
1067
|
-
debug("doc session firstSynced rejected", {
|
|
1068
|
-
docId,
|
|
1069
|
-
roomId,
|
|
1070
|
-
error
|
|
1071
|
-
});
|
|
1072
|
-
});
|
|
1073
|
-
const session = {
|
|
1074
|
-
adaptor,
|
|
1075
|
-
room,
|
|
1076
|
-
firstSynced,
|
|
1077
|
-
doc,
|
|
1078
|
-
roomId,
|
|
1079
|
-
refCount: 0
|
|
1080
|
-
};
|
|
1081
|
-
this.docSessions.set(docId, session);
|
|
1082
|
-
return session;
|
|
1083
|
-
}
|
|
1084
|
-
async leaveDocSession(docId) {
|
|
1085
|
-
const session = this.docSessions.get(docId);
|
|
1086
|
-
if (!session) {
|
|
1087
|
-
debug("leaveDocSession invoked but no session found", { docId });
|
|
1088
|
-
return;
|
|
1089
|
-
}
|
|
1090
|
-
this.docSessions.delete(docId);
|
|
1091
|
-
debug("leaving doc session", {
|
|
1092
|
-
docId,
|
|
1093
|
-
roomId: session.roomId
|
|
1094
|
-
});
|
|
1095
|
-
try {
|
|
1096
|
-
await session.room.leave();
|
|
1097
|
-
debug("doc room left", {
|
|
1098
|
-
docId,
|
|
1099
|
-
roomId: session.roomId
|
|
1100
|
-
});
|
|
1101
|
-
} catch (error) {
|
|
1102
|
-
debug("doc room leave failed; destroying", {
|
|
1103
|
-
docId,
|
|
1104
|
-
roomId: session.roomId,
|
|
1105
|
-
error
|
|
1106
|
-
});
|
|
1107
|
-
await session.room.destroy().catch(() => {});
|
|
1108
|
-
}
|
|
1109
|
-
session.adaptor.destroy();
|
|
1110
|
-
debug("doc session destroyed", {
|
|
1111
|
-
docId,
|
|
1112
|
-
roomId: session.roomId
|
|
1113
|
-
});
|
|
1114
|
-
}
|
|
1115
|
-
};
|
|
5
|
+
loro_crdt = require_chunk.__toESM(loro_crdt);
|
|
1116
6
|
|
|
1117
|
-
//#endregion
|
|
1118
7
|
//#region src/internal/event-bus.ts
|
|
1119
8
|
var RepoEventBus = class {
|
|
1120
9
|
watchers = /* @__PURE__ */ new Set();
|
|
@@ -1628,8 +517,7 @@ var MetadataManager = class {
|
|
|
1628
517
|
return this.state.metadata.entries();
|
|
1629
518
|
}
|
|
1630
519
|
get(docId) {
|
|
1631
|
-
|
|
1632
|
-
return metadata ? cloneJsonObject(metadata) : void 0;
|
|
520
|
+
return this.state.metadata.get(docId);
|
|
1633
521
|
}
|
|
1634
522
|
listDoc(query) {
|
|
1635
523
|
if (query?.limit !== void 0 && query.limit <= 0) return [];
|
|
@@ -1674,20 +562,16 @@ var MetadataManager = class {
|
|
|
1674
562
|
for (const key of Object.keys(patchObject)) {
|
|
1675
563
|
const rawValue = patchObject[key];
|
|
1676
564
|
if (rawValue === void 0) continue;
|
|
1677
|
-
|
|
1678
|
-
if (key === "tombstone") canonical = Boolean(rawValue);
|
|
1679
|
-
else canonical = cloneJsonValue(rawValue);
|
|
1680
|
-
if (canonical === void 0) continue;
|
|
1681
|
-
if (jsonEquals(base ? base[key] : void 0, canonical)) continue;
|
|
565
|
+
if (jsonEquals(base ? base[key] : void 0, rawValue)) continue;
|
|
1682
566
|
const storageKey = key === "tombstone" ? "$tombstone" : key;
|
|
567
|
+
console.log("upserting", rawValue);
|
|
1683
568
|
this.metaFlock.put([
|
|
1684
569
|
"m",
|
|
1685
570
|
docId,
|
|
1686
571
|
storageKey
|
|
1687
|
-
],
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
outPatch[key] = cloneJsonValue(stored) ?? stored;
|
|
572
|
+
], rawValue);
|
|
573
|
+
next[key] = rawValue;
|
|
574
|
+
outPatch[key] = rawValue;
|
|
1691
575
|
changed = true;
|
|
1692
576
|
}
|
|
1693
577
|
if (!changed) {
|
|
@@ -1699,7 +583,7 @@ var MetadataManager = class {
|
|
|
1699
583
|
this.eventBus.emit({
|
|
1700
584
|
kind: "doc-metadata",
|
|
1701
585
|
docId,
|
|
1702
|
-
patch:
|
|
586
|
+
patch: outPatch,
|
|
1703
587
|
by: "local"
|
|
1704
588
|
});
|
|
1705
589
|
}
|
|
@@ -2860,9 +1744,5 @@ var LoroRepo = class LoroRepo {
|
|
|
2860
1744
|
};
|
|
2861
1745
|
|
|
2862
1746
|
//#endregion
|
|
2863
|
-
exports.BroadcastChannelTransportAdapter = BroadcastChannelTransportAdapter;
|
|
2864
|
-
exports.FileSystemStorageAdaptor = FileSystemStorageAdaptor;
|
|
2865
|
-
exports.IndexedDBStorageAdaptor = IndexedDBStorageAdaptor;
|
|
2866
1747
|
exports.LoroRepo = LoroRepo;
|
|
2867
|
-
exports.WebSocketTransportAdapter = WebSocketTransportAdapter;
|
|
2868
1748
|
//# sourceMappingURL=index.cjs.map
|