loro-repo 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1042 -1101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -134
- package/dist/index.d.ts +116 -136
- package/dist/index.js +1039 -1098
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -23,891 +23,466 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
//#endregion
|
|
24
24
|
let __loro_dev_flock = require("@loro-dev/flock");
|
|
25
25
|
__loro_dev_flock = __toESM(__loro_dev_flock);
|
|
26
|
-
let loro_adaptors_loro = require("loro-adaptors/loro");
|
|
27
|
-
loro_adaptors_loro = __toESM(loro_adaptors_loro);
|
|
28
|
-
let loro_protocol = require("loro-protocol");
|
|
29
|
-
loro_protocol = __toESM(loro_protocol);
|
|
30
|
-
let loro_websocket = require("loro-websocket");
|
|
31
|
-
loro_websocket = __toESM(loro_websocket);
|
|
32
26
|
let loro_crdt = require("loro-crdt");
|
|
33
27
|
loro_crdt = __toESM(loro_crdt);
|
|
34
|
-
let loro_adaptors_flock = require("loro-adaptors/flock");
|
|
35
|
-
loro_adaptors_flock = __toESM(loro_adaptors_flock);
|
|
36
28
|
let node_fs = require("node:fs");
|
|
37
29
|
node_fs = __toESM(node_fs);
|
|
38
30
|
let node_path = require("node:path");
|
|
39
31
|
node_path = __toESM(node_path);
|
|
40
32
|
let node_crypto = require("node:crypto");
|
|
41
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
42
|
|
|
43
|
-
//#region src/
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (typeof globalThis !== "object" || globalThis === null) return;
|
|
52
|
-
return globalThis.process?.env;
|
|
53
|
-
};
|
|
54
|
-
const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
|
|
55
|
-
const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
|
|
56
|
-
const wildcardTokens = new Set([
|
|
57
|
-
"*",
|
|
58
|
-
"1",
|
|
59
|
-
"true",
|
|
60
|
-
"all"
|
|
61
|
-
]);
|
|
62
|
-
const namespaceSet = new Set(normalizedNamespaces);
|
|
63
|
-
const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
|
|
64
|
-
const isDebugEnabled = (namespace) => {
|
|
65
|
-
if (!namespaceSet.size) return false;
|
|
66
|
-
if (!namespace) return hasWildcard;
|
|
67
|
-
const normalized = namespace.toLowerCase();
|
|
68
|
-
if (hasWildcard) return true;
|
|
69
|
-
if (namespaceSet.has(normalized)) return true;
|
|
70
|
-
const [root] = normalized.split(":");
|
|
71
|
-
return namespaceSet.has(root);
|
|
72
|
-
};
|
|
73
|
-
const createDebugLogger = (namespace) => {
|
|
74
|
-
const normalized = namespace.toLowerCase();
|
|
75
|
-
return (...args) => {
|
|
76
|
-
if (!isDebugEnabled(normalized)) return;
|
|
77
|
-
const prefix = `[loro-repo:${namespace}]`;
|
|
78
|
-
if (args.length === 0) {
|
|
79
|
-
console.info(prefix);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
console.info(prefix, ...args);
|
|
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
|
|
83
51
|
};
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
//#endregion
|
|
87
|
-
//#region src/transport/websocket.ts
|
|
88
|
-
const debug = createDebugLogger("transport:websocket");
|
|
89
|
-
function withTimeout(promise, timeoutMs) {
|
|
90
|
-
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
const timer = setTimeout(() => {
|
|
93
|
-
reject(/* @__PURE__ */ new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
94
|
-
}, timeoutMs);
|
|
95
|
-
promise.then((value) => {
|
|
96
|
-
clearTimeout(timer);
|
|
97
|
-
resolve(value);
|
|
98
|
-
}).catch((error) => {
|
|
99
|
-
clearTimeout(timer);
|
|
100
|
-
reject(error);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
52
|
}
|
|
104
|
-
function
|
|
105
|
-
if (typeof
|
|
106
|
-
|
|
107
|
-
|
|
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);
|
|
108
64
|
} catch {
|
|
109
|
-
return
|
|
65
|
+
return docId.replace(/[^a-z0-9_-]/gi, "_");
|
|
110
66
|
}
|
|
111
|
-
return fallback;
|
|
112
67
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
if (!a || !b) return false;
|
|
116
|
-
if (a.length !== b.length) return false;
|
|
117
|
-
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
118
|
-
return true;
|
|
68
|
+
function postChannelMessage(channel, message) {
|
|
69
|
+
channel.postMessage(message);
|
|
119
70
|
}
|
|
120
71
|
/**
|
|
121
|
-
*
|
|
72
|
+
* TransportAdapter that relies on the BroadcastChannel API to fan out metadata
|
|
73
|
+
* and document updates between browser tabs within the same origin.
|
|
122
74
|
*/
|
|
123
|
-
var
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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`;
|
|
130
86
|
}
|
|
131
|
-
async connect(
|
|
132
|
-
|
|
133
|
-
debug("connect requested", { status: client.getStatus() });
|
|
134
|
-
try {
|
|
135
|
-
await client.connect();
|
|
136
|
-
debug("client.connect resolved");
|
|
137
|
-
await client.waitConnected();
|
|
138
|
-
debug("client.waitConnected resolved", { status: client.getStatus() });
|
|
139
|
-
} catch (error) {
|
|
140
|
-
debug("connect failed", error);
|
|
141
|
-
throw error;
|
|
142
|
-
}
|
|
87
|
+
async connect() {
|
|
88
|
+
this.connected = true;
|
|
143
89
|
}
|
|
144
90
|
async close() {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
this.docSessions.clear();
|
|
151
|
-
await this.teardownMetadataSession().catch(() => {});
|
|
152
|
-
if (this.client) {
|
|
153
|
-
const client = this.client;
|
|
154
|
-
this.client = void 0;
|
|
155
|
-
client.destroy();
|
|
156
|
-
debug("websocket client destroyed");
|
|
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;
|
|
157
96
|
}
|
|
158
|
-
|
|
97
|
+
for (const [docId] of this.docStates) this.teardownDocChannel(docId);
|
|
98
|
+
this.docStates.clear();
|
|
159
99
|
}
|
|
160
100
|
isConnected() {
|
|
161
|
-
return this.
|
|
101
|
+
return this.connected;
|
|
162
102
|
}
|
|
163
|
-
async syncMeta(flock,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
await withTimeout((await this.ensureMetadataSession(flock, {
|
|
171
|
-
roomId: this.options.metadataRoomId,
|
|
172
|
-
auth: this.options.metadataAuth
|
|
173
|
-
})).firstSynced, options?.timeout);
|
|
174
|
-
debug("syncMeta completed", { roomId: this.options.metadataRoomId });
|
|
175
|
-
return { ok: true };
|
|
176
|
-
} catch (error) {
|
|
177
|
-
debug("syncMeta failed", error);
|
|
178
|
-
return { ok: false };
|
|
179
|
-
}
|
|
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 };
|
|
180
109
|
}
|
|
181
|
-
joinMetaRoom(flock,
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
189
133
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
134
|
+
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
135
|
+
postChannelMessage(state.channel, {
|
|
136
|
+
kind: "meta-export",
|
|
137
|
+
from: this.instanceId,
|
|
138
|
+
bundle
|
|
139
|
+
});
|
|
193
140
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const subscription = {
|
|
141
|
+
queueMicrotask(() => resolve());
|
|
142
|
+
return {
|
|
197
143
|
unsubscribe: () => {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
debug("tearing down metadata session due to refCount=0", { roomId: session.roomId });
|
|
206
|
-
this.teardownMetadataSession(session).catch(() => {});
|
|
207
|
-
}
|
|
208
|
-
});
|
|
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
|
+
}
|
|
209
151
|
},
|
|
210
|
-
firstSyncedWithRemote: firstSynced,
|
|
152
|
+
firstSyncedWithRemote: listener.firstSynced,
|
|
211
153
|
get connected() {
|
|
212
|
-
return
|
|
154
|
+
return true;
|
|
213
155
|
}
|
|
214
156
|
};
|
|
215
|
-
ensure.then((session) => {
|
|
216
|
-
session.refCount += 1;
|
|
217
|
-
debug("metadata session refCount incremented", {
|
|
218
|
-
roomId: session.roomId,
|
|
219
|
-
refCount: session.refCount
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
return subscription;
|
|
223
157
|
}
|
|
224
|
-
async syncDoc(docId, doc,
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
docId,
|
|
231
|
-
roomId: session.roomId
|
|
232
|
-
});
|
|
233
|
-
return { ok: true };
|
|
234
|
-
} catch (error) {
|
|
235
|
-
debug("syncDoc failed", {
|
|
236
|
-
docId,
|
|
237
|
-
error
|
|
238
|
-
});
|
|
239
|
-
return { ok: false };
|
|
240
|
-
}
|
|
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 };
|
|
241
164
|
}
|
|
242
|
-
joinDocRoom(docId, doc,
|
|
243
|
-
|
|
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",
|
|
244
188
|
docId,
|
|
245
|
-
|
|
246
|
-
hasAuthOverride: Boolean(params?.auth && params.auth.length)
|
|
189
|
+
from: this.instanceId
|
|
247
190
|
});
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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 {
|
|
252
200
|
unsubscribe: () => {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
docId,
|
|
257
|
-
roomId: session.roomId,
|
|
258
|
-
refCount: session.refCount
|
|
259
|
-
});
|
|
260
|
-
if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
|
|
261
|
-
});
|
|
201
|
+
listener.unsubscribe();
|
|
202
|
+
state.listeners.delete(listener);
|
|
203
|
+
if (!state.listeners.size) this.teardownDocChannel(docId);
|
|
262
204
|
},
|
|
263
|
-
firstSyncedWithRemote: firstSynced,
|
|
205
|
+
firstSyncedWithRemote: listener.firstSynced,
|
|
264
206
|
get connected() {
|
|
265
|
-
return
|
|
207
|
+
return true;
|
|
266
208
|
}
|
|
267
209
|
};
|
|
268
|
-
ensure.then((session) => {
|
|
269
|
-
session.refCount += 1;
|
|
270
|
-
debug("doc session refCount incremented", {
|
|
271
|
-
docId,
|
|
272
|
-
roomId: session.roomId,
|
|
273
|
-
refCount: session.refCount
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
return subscription;
|
|
277
210
|
}
|
|
278
|
-
|
|
279
|
-
if (this.
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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;
|
|
294
243
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
roomId: params.roomId,
|
|
323
|
-
crdtAdaptor: adaptor,
|
|
324
|
-
auth: params.auth
|
|
325
|
-
});
|
|
326
|
-
const firstSynced = room.waitForReachingServerVersion();
|
|
327
|
-
firstSynced.then(() => {
|
|
328
|
-
debug("metadata session firstSynced resolved", { roomId: params.roomId });
|
|
329
|
-
}, (error) => {
|
|
330
|
-
debug("metadata session firstSynced rejected", {
|
|
331
|
-
roomId: params.roomId,
|
|
332
|
-
error
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
const session = {
|
|
336
|
-
adaptor,
|
|
337
|
-
room,
|
|
338
|
-
firstSynced,
|
|
339
|
-
flock,
|
|
340
|
-
roomId: params.roomId,
|
|
341
|
-
auth: params.auth,
|
|
342
|
-
refCount: 0
|
|
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
|
+
}
|
|
343
271
|
};
|
|
344
|
-
|
|
345
|
-
|
|
272
|
+
channel.addEventListener("message", onMessage);
|
|
273
|
+
const state = {
|
|
274
|
+
channel,
|
|
275
|
+
listeners,
|
|
276
|
+
onMessage
|
|
277
|
+
};
|
|
278
|
+
this.docStates.set(docId, state);
|
|
279
|
+
return state;
|
|
346
280
|
}
|
|
347
|
-
|
|
348
|
-
const
|
|
349
|
-
if (!
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
await room.leave();
|
|
355
|
-
debug("metadata room left", { roomId: target.roomId });
|
|
356
|
-
} catch (error) {
|
|
357
|
-
debug("metadata room leave failed; destroying", {
|
|
358
|
-
roomId: target.roomId,
|
|
359
|
-
error
|
|
360
|
-
});
|
|
361
|
-
await room.destroy().catch(() => {});
|
|
362
|
-
}
|
|
363
|
-
adaptor.destroy();
|
|
364
|
-
debug("metadata session destroyed", { roomId: target.roomId });
|
|
365
|
-
}
|
|
366
|
-
async ensureDocSession(docId, doc, params) {
|
|
367
|
-
debug("ensureDocSession invoked", { docId });
|
|
368
|
-
const client = this.ensureClient();
|
|
369
|
-
await client.waitConnected();
|
|
370
|
-
debug("websocket client ready for doc session", {
|
|
371
|
-
docId,
|
|
372
|
-
status: client.getStatus()
|
|
373
|
-
});
|
|
374
|
-
const existing = this.docSessions.get(docId);
|
|
375
|
-
const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
|
|
376
|
-
const roomId = normalizeRoomId(params.roomId, derivedRoomId);
|
|
377
|
-
const auth = params.auth ?? this.options.docAuth?.(docId);
|
|
378
|
-
debug("doc session params resolved", {
|
|
379
|
-
docId,
|
|
380
|
-
roomId,
|
|
381
|
-
hasAuth: Boolean(auth && auth.length)
|
|
382
|
-
});
|
|
383
|
-
if (existing && existing.doc === doc && existing.roomId === roomId) {
|
|
384
|
-
debug("reusing doc session", {
|
|
385
|
-
docId,
|
|
386
|
-
roomId,
|
|
387
|
-
refCount: existing.refCount
|
|
388
|
-
});
|
|
389
|
-
return existing;
|
|
390
|
-
}
|
|
391
|
-
if (existing) {
|
|
392
|
-
debug("doc session mismatch; leaving existing session", {
|
|
393
|
-
docId,
|
|
394
|
-
previousRoomId: existing.roomId,
|
|
395
|
-
nextRoomId: roomId
|
|
396
|
-
});
|
|
397
|
-
await this.leaveDocSession(docId).catch(() => {});
|
|
398
|
-
}
|
|
399
|
-
const adaptor = new loro_adaptors_loro.LoroAdaptor(doc);
|
|
400
|
-
debug("joining doc room", {
|
|
401
|
-
docId,
|
|
402
|
-
roomId,
|
|
403
|
-
hasAuth: Boolean(auth && auth.length)
|
|
404
|
-
});
|
|
405
|
-
const room = await client.join({
|
|
406
|
-
roomId,
|
|
407
|
-
crdtAdaptor: adaptor,
|
|
408
|
-
auth
|
|
409
|
-
});
|
|
410
|
-
const firstSynced = room.waitForReachingServerVersion();
|
|
411
|
-
firstSynced.then(() => {
|
|
412
|
-
debug("doc session firstSynced resolved", {
|
|
413
|
-
docId,
|
|
414
|
-
roomId
|
|
415
|
-
});
|
|
416
|
-
}, (error) => {
|
|
417
|
-
debug("doc session firstSynced rejected", {
|
|
418
|
-
docId,
|
|
419
|
-
roomId,
|
|
420
|
-
error
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
const session = {
|
|
424
|
-
adaptor,
|
|
425
|
-
room,
|
|
426
|
-
firstSynced,
|
|
427
|
-
doc,
|
|
428
|
-
roomId,
|
|
429
|
-
refCount: 0
|
|
430
|
-
};
|
|
431
|
-
this.docSessions.set(docId, session);
|
|
432
|
-
return session;
|
|
433
|
-
}
|
|
434
|
-
async leaveDocSession(docId) {
|
|
435
|
-
const session = this.docSessions.get(docId);
|
|
436
|
-
if (!session) {
|
|
437
|
-
debug("leaveDocSession invoked but no session found", { docId });
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
this.docSessions.delete(docId);
|
|
441
|
-
debug("leaving doc session", {
|
|
442
|
-
docId,
|
|
443
|
-
roomId: session.roomId
|
|
444
|
-
});
|
|
445
|
-
try {
|
|
446
|
-
await session.room.leave();
|
|
447
|
-
debug("doc room left", {
|
|
448
|
-
docId,
|
|
449
|
-
roomId: session.roomId
|
|
450
|
-
});
|
|
451
|
-
} catch (error) {
|
|
452
|
-
debug("doc room leave failed; destroying", {
|
|
453
|
-
docId,
|
|
454
|
-
roomId: session.roomId,
|
|
455
|
-
error
|
|
456
|
-
});
|
|
457
|
-
await session.room.destroy().catch(() => {});
|
|
458
|
-
}
|
|
459
|
-
session.adaptor.destroy();
|
|
460
|
-
debug("doc session destroyed", {
|
|
461
|
-
docId,
|
|
462
|
-
roomId: session.roomId
|
|
463
|
-
});
|
|
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);
|
|
464
288
|
}
|
|
465
289
|
};
|
|
466
290
|
|
|
467
291
|
//#endregion
|
|
468
|
-
//#region src/
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (typeof
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
if (typeof
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
function encodeDocChannelId(docId) {
|
|
487
|
-
try {
|
|
488
|
-
return encodeURIComponent(docId);
|
|
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);
|
|
489
309
|
} catch {
|
|
490
|
-
return
|
|
310
|
+
return "[object]";
|
|
491
311
|
}
|
|
312
|
+
return String(cause);
|
|
492
313
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
connected = false;
|
|
505
|
-
metaState;
|
|
506
|
-
docStates = /* @__PURE__ */ new Map();
|
|
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;
|
|
507
325
|
constructor(options = {}) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
this.
|
|
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;
|
|
511
336
|
}
|
|
512
|
-
async
|
|
513
|
-
|
|
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
|
+
}
|
|
514
362
|
}
|
|
515
|
-
async
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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);
|
|
521
377
|
}
|
|
522
|
-
|
|
523
|
-
|
|
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;
|
|
524
396
|
}
|
|
525
|
-
|
|
526
|
-
|
|
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
|
+
}
|
|
527
409
|
}
|
|
528
|
-
async
|
|
529
|
-
|
|
530
|
-
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
531
|
-
await subscription.firstSyncedWithRemote;
|
|
532
|
-
subscription.unsubscribe();
|
|
533
|
-
return { ok: true };
|
|
410
|
+
async loadAsset(assetId) {
|
|
411
|
+
return await this.getBinary(this.assetStore, assetId) ?? void 0;
|
|
534
412
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
resolveFirst: resolve,
|
|
552
|
-
firstSynced: promise
|
|
553
|
-
};
|
|
554
|
-
state.listeners.add(listener);
|
|
555
|
-
postChannelMessage(state.channel, {
|
|
556
|
-
kind: "meta-request",
|
|
557
|
-
from: this.instanceId
|
|
558
|
-
});
|
|
559
|
-
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
560
|
-
postChannelMessage(state.channel, {
|
|
561
|
-
kind: "meta-export",
|
|
562
|
-
from: this.instanceId,
|
|
563
|
-
bundle
|
|
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);
|
|
564
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 });
|
|
565
434
|
});
|
|
566
|
-
|
|
567
|
-
return {
|
|
568
|
-
unsubscribe: () => {
|
|
569
|
-
listener.unsubscribe();
|
|
570
|
-
state.listeners.delete(listener);
|
|
571
|
-
if (!state.listeners.size) {
|
|
572
|
-
state.channel.removeEventListener("message", state.onMessage);
|
|
573
|
-
state.channel.close();
|
|
574
|
-
this.metaState = void 0;
|
|
575
|
-
}
|
|
576
|
-
},
|
|
577
|
-
firstSyncedWithRemote: listener.firstSynced,
|
|
578
|
-
get connected() {
|
|
579
|
-
return true;
|
|
580
|
-
}
|
|
581
|
-
};
|
|
435
|
+
return this.dbPromise;
|
|
582
436
|
}
|
|
583
|
-
|
|
584
|
-
const
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
subscription.unsubscribe();
|
|
588
|
-
return { ok: true };
|
|
437
|
+
ensureStore(db, storeName) {
|
|
438
|
+
const names = db.objectStoreNames;
|
|
439
|
+
if (this.storeExists(names, storeName)) return;
|
|
440
|
+
db.createObjectStore(storeName);
|
|
589
441
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
from: this.instanceId,
|
|
603
|
-
mode: "update",
|
|
604
|
-
payload
|
|
605
|
-
});
|
|
606
|
-
}),
|
|
607
|
-
resolveFirst: resolve,
|
|
608
|
-
firstSynced: promise
|
|
609
|
-
};
|
|
610
|
-
state.listeners.add(listener);
|
|
611
|
-
postChannelMessage(state.channel, {
|
|
612
|
-
kind: "doc-request",
|
|
613
|
-
docId,
|
|
614
|
-
from: this.instanceId
|
|
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");
|
|
615
454
|
});
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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");
|
|
622
471
|
});
|
|
623
|
-
queueMicrotask(() => resolve());
|
|
624
|
-
return {
|
|
625
|
-
unsubscribe: () => {
|
|
626
|
-
listener.unsubscribe();
|
|
627
|
-
state.listeners.delete(listener);
|
|
628
|
-
if (!state.listeners.size) this.teardownDocChannel(docId);
|
|
629
|
-
},
|
|
630
|
-
firstSyncedWithRemote: listener.firstSynced,
|
|
631
|
-
get connected() {
|
|
632
|
-
return true;
|
|
633
|
-
}
|
|
634
|
-
};
|
|
635
472
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
640
|
-
const onMessage = (event) => {
|
|
641
|
-
const message = event.data;
|
|
642
|
-
if (!message || message.from === this.instanceId) return;
|
|
643
|
-
if (message.kind === "meta-export") for (const entry of listeners) {
|
|
644
|
-
entry.muted = true;
|
|
645
|
-
entry.flock.importJson(message.bundle);
|
|
646
|
-
entry.muted = false;
|
|
647
|
-
entry.resolveFirst();
|
|
648
|
-
}
|
|
649
|
-
else if (message.kind === "meta-request") {
|
|
650
|
-
const first = listeners.values().next().value;
|
|
651
|
-
if (!first) return;
|
|
652
|
-
Promise.resolve(first.flock.exportJson()).then((bundle) => {
|
|
653
|
-
postChannelMessage(channel, {
|
|
654
|
-
kind: "meta-export",
|
|
655
|
-
from: this.instanceId,
|
|
656
|
-
bundle
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
channel.addEventListener("message", onMessage);
|
|
662
|
-
this.metaState = {
|
|
663
|
-
channel,
|
|
664
|
-
listeners,
|
|
665
|
-
onMessage
|
|
666
|
-
};
|
|
667
|
-
return this.metaState;
|
|
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);
|
|
668
476
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
if (existing) return existing;
|
|
672
|
-
const channel = new (ensureBroadcastChannel())(`${this.namespace}-doc-${encodeDocChannelId(docId)}`);
|
|
673
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
674
|
-
const onMessage = (event) => {
|
|
675
|
-
const message = event.data;
|
|
676
|
-
if (!message || message.from === this.instanceId) return;
|
|
677
|
-
if (message.kind === "doc-update") for (const entry of listeners) {
|
|
678
|
-
entry.muted = true;
|
|
679
|
-
entry.doc.import(message.payload);
|
|
680
|
-
entry.muted = false;
|
|
681
|
-
entry.resolveFirst();
|
|
682
|
-
}
|
|
683
|
-
else if (message.kind === "doc-request") {
|
|
684
|
-
const first = listeners.values().next().value;
|
|
685
|
-
if (!first) return;
|
|
686
|
-
const payload = message.docId === docId ? first.doc.export({ mode: "snapshot" }) : void 0;
|
|
687
|
-
if (!payload) return;
|
|
688
|
-
postChannelMessage(channel, {
|
|
689
|
-
kind: "doc-update",
|
|
690
|
-
docId,
|
|
691
|
-
from: this.instanceId,
|
|
692
|
-
mode: "snapshot",
|
|
693
|
-
payload
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
};
|
|
697
|
-
channel.addEventListener("message", onMessage);
|
|
698
|
-
const state = {
|
|
699
|
-
channel,
|
|
700
|
-
listeners,
|
|
701
|
-
onMessage
|
|
702
|
-
};
|
|
703
|
-
this.docStates.set(docId, state);
|
|
704
|
-
return state;
|
|
477
|
+
async clearDocUpdates(db, docId) {
|
|
478
|
+
await this.runInTransaction(db, this.docUpdateStore, "readwrite", (store) => this.wrapRequest(store.delete(docId), "delete"));
|
|
705
479
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
if (!state) return;
|
|
709
|
-
for (const entry of state.listeners) entry.unsubscribe();
|
|
710
|
-
state.channel.removeEventListener("message", state.onMessage);
|
|
711
|
-
state.channel.close();
|
|
712
|
-
this.docStates.delete(docId);
|
|
480
|
+
async writeSnapshot(db, docId, snapshot) {
|
|
481
|
+
await this.putBinary(db, this.docStore, docId, snapshot.slice());
|
|
713
482
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
//#region src/storage/indexeddb.ts
|
|
718
|
-
const DEFAULT_DB_NAME = "loro-repo";
|
|
719
|
-
const DEFAULT_DB_VERSION = 1;
|
|
720
|
-
const DEFAULT_DOC_STORE = "docs";
|
|
721
|
-
const DEFAULT_META_STORE = "meta";
|
|
722
|
-
const DEFAULT_ASSET_STORE = "assets";
|
|
723
|
-
const DEFAULT_DOC_UPDATE_STORE = "doc-updates";
|
|
724
|
-
const DEFAULT_META_KEY = "snapshot";
|
|
725
|
-
const textDecoder$1 = new TextDecoder();
|
|
726
|
-
function describeUnknown(cause) {
|
|
727
|
-
if (typeof cause === "string") return cause;
|
|
728
|
-
if (typeof cause === "number" || typeof cause === "boolean") return String(cause);
|
|
729
|
-
if (typeof cause === "bigint") return cause.toString();
|
|
730
|
-
if (typeof cause === "symbol") return cause.description ?? cause.toString();
|
|
731
|
-
if (typeof cause === "function") return `[function ${cause.name ?? "anonymous"}]`;
|
|
732
|
-
if (cause && typeof cause === "object") try {
|
|
733
|
-
return JSON.stringify(cause);
|
|
734
|
-
} catch {
|
|
735
|
-
return "[object]";
|
|
736
|
-
}
|
|
737
|
-
return String(cause);
|
|
738
|
-
}
|
|
739
|
-
var IndexedDBStorageAdaptor = class {
|
|
740
|
-
idb;
|
|
741
|
-
dbName;
|
|
742
|
-
version;
|
|
743
|
-
docStore;
|
|
744
|
-
docUpdateStore;
|
|
745
|
-
metaStore;
|
|
746
|
-
assetStore;
|
|
747
|
-
metaKey;
|
|
748
|
-
dbPromise;
|
|
749
|
-
closed = false;
|
|
750
|
-
constructor(options = {}) {
|
|
751
|
-
const idbFactory = globalThis.indexedDB;
|
|
752
|
-
if (!idbFactory) throw new Error("IndexedDB is not available in this environment");
|
|
753
|
-
this.idb = idbFactory;
|
|
754
|
-
this.dbName = options.dbName ?? DEFAULT_DB_NAME;
|
|
755
|
-
this.version = options.version ?? DEFAULT_DB_VERSION;
|
|
756
|
-
this.docStore = options.docStoreName ?? DEFAULT_DOC_STORE;
|
|
757
|
-
this.docUpdateStore = options.docUpdateStoreName ?? DEFAULT_DOC_UPDATE_STORE;
|
|
758
|
-
this.metaStore = options.metaStoreName ?? DEFAULT_META_STORE;
|
|
759
|
-
this.assetStore = options.assetStoreName ?? DEFAULT_ASSET_STORE;
|
|
760
|
-
this.metaKey = options.metaKey ?? DEFAULT_META_KEY;
|
|
761
|
-
}
|
|
762
|
-
async save(payload) {
|
|
763
|
-
const db = await this.ensureDb();
|
|
764
|
-
switch (payload.type) {
|
|
765
|
-
case "doc-snapshot": {
|
|
766
|
-
const snapshot = payload.snapshot.slice();
|
|
767
|
-
await this.storeMergedSnapshot(db, payload.docId, snapshot);
|
|
768
|
-
break;
|
|
769
|
-
}
|
|
770
|
-
case "doc-update": {
|
|
771
|
-
const update = payload.update.slice();
|
|
772
|
-
await this.appendDocUpdate(db, payload.docId, update);
|
|
773
|
-
break;
|
|
774
|
-
}
|
|
775
|
-
case "asset": {
|
|
776
|
-
const bytes = payload.data.slice();
|
|
777
|
-
await this.putBinary(db, this.assetStore, payload.assetId, bytes);
|
|
778
|
-
break;
|
|
779
|
-
}
|
|
780
|
-
case "meta": {
|
|
781
|
-
const bytes = payload.update.slice();
|
|
782
|
-
await this.putBinary(db, this.metaStore, this.metaKey, bytes);
|
|
783
|
-
break;
|
|
784
|
-
}
|
|
785
|
-
default: throw new Error("Unsupported storage payload type");
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
async deleteAsset(assetId) {
|
|
789
|
-
const db = await this.ensureDb();
|
|
790
|
-
await this.deleteKey(db, this.assetStore, assetId);
|
|
791
|
-
}
|
|
792
|
-
async loadDoc(docId) {
|
|
793
|
-
const db = await this.ensureDb();
|
|
794
|
-
const snapshot = await this.getBinaryFromDb(db, this.docStore, docId);
|
|
795
|
-
const pendingUpdates = await this.getDocUpdates(db, docId);
|
|
796
|
-
if (!snapshot && pendingUpdates.length === 0) return;
|
|
797
|
-
let doc;
|
|
798
|
-
try {
|
|
799
|
-
doc = snapshot ? loro_crdt.LoroDoc.fromSnapshot(snapshot) : new loro_crdt.LoroDoc();
|
|
800
|
-
} catch (error) {
|
|
801
|
-
throw this.createError(`Failed to hydrate document snapshot for "${docId}"`, error);
|
|
802
|
-
}
|
|
803
|
-
let appliedUpdates = false;
|
|
804
|
-
for (const update of pendingUpdates) try {
|
|
805
|
-
doc.import(update);
|
|
806
|
-
appliedUpdates = true;
|
|
807
|
-
} catch (error) {
|
|
808
|
-
throw this.createError(`Failed to apply queued document update for "${docId}"`, error);
|
|
809
|
-
}
|
|
810
|
-
if (appliedUpdates) {
|
|
811
|
-
let consolidated;
|
|
812
|
-
try {
|
|
813
|
-
consolidated = doc.export({ mode: "snapshot" });
|
|
814
|
-
} catch (error) {
|
|
815
|
-
throw this.createError(`Failed to export consolidated snapshot for "${docId}"`, error);
|
|
816
|
-
}
|
|
817
|
-
await this.writeSnapshot(db, docId, consolidated);
|
|
818
|
-
await this.clearDocUpdates(db, docId);
|
|
819
|
-
}
|
|
820
|
-
return doc;
|
|
821
|
-
}
|
|
822
|
-
async loadMeta() {
|
|
823
|
-
const bytes = await this.getBinary(this.metaStore, this.metaKey);
|
|
824
|
-
if (!bytes) return void 0;
|
|
825
|
-
try {
|
|
826
|
-
const json = textDecoder$1.decode(bytes);
|
|
827
|
-
const bundle = JSON.parse(json);
|
|
828
|
-
const flock = new __loro_dev_flock.Flock();
|
|
829
|
-
flock.importJson(bundle);
|
|
830
|
-
return flock;
|
|
831
|
-
} catch (error) {
|
|
832
|
-
throw this.createError("Failed to hydrate metadata snapshot", error);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
async loadAsset(assetId) {
|
|
836
|
-
return await this.getBinary(this.assetStore, assetId) ?? void 0;
|
|
837
|
-
}
|
|
838
|
-
async close() {
|
|
839
|
-
this.closed = true;
|
|
840
|
-
const db = await this.dbPromise;
|
|
841
|
-
if (db) db.close();
|
|
842
|
-
this.dbPromise = void 0;
|
|
843
|
-
}
|
|
844
|
-
async ensureDb() {
|
|
845
|
-
if (this.closed) throw new Error("IndexedDBStorageAdaptor has been closed");
|
|
846
|
-
if (!this.dbPromise) this.dbPromise = new Promise((resolve, reject) => {
|
|
847
|
-
const request = this.idb.open(this.dbName, this.version);
|
|
848
|
-
request.addEventListener("upgradeneeded", () => {
|
|
849
|
-
const db = request.result;
|
|
850
|
-
this.ensureStore(db, this.docStore);
|
|
851
|
-
this.ensureStore(db, this.docUpdateStore);
|
|
852
|
-
this.ensureStore(db, this.metaStore);
|
|
853
|
-
this.ensureStore(db, this.assetStore);
|
|
854
|
-
});
|
|
855
|
-
request.addEventListener("success", () => resolve(request.result), { once: true });
|
|
856
|
-
request.addEventListener("error", () => {
|
|
857
|
-
reject(this.createError(`Failed to open IndexedDB database "${this.dbName}"`, request.error));
|
|
858
|
-
}, { once: true });
|
|
859
|
-
});
|
|
860
|
-
return this.dbPromise;
|
|
861
|
-
}
|
|
862
|
-
ensureStore(db, storeName) {
|
|
863
|
-
const names = db.objectStoreNames;
|
|
864
|
-
if (this.storeExists(names, storeName)) return;
|
|
865
|
-
db.createObjectStore(storeName);
|
|
866
|
-
}
|
|
867
|
-
storeExists(names, storeName) {
|
|
868
|
-
if (typeof names.contains === "function") return names.contains(storeName);
|
|
869
|
-
const length = names.length ?? 0;
|
|
870
|
-
for (let index = 0; index < length; index += 1) if (names.item?.(index) === storeName) return true;
|
|
871
|
-
return false;
|
|
872
|
-
}
|
|
873
|
-
async storeMergedSnapshot(db, docId, incoming) {
|
|
874
|
-
await this.runInTransaction(db, this.docStore, "readwrite", async (store) => {
|
|
875
|
-
const existingRaw = await this.wrapRequest(store.get(docId), "read");
|
|
876
|
-
const existing = await this.normalizeBinary(existingRaw);
|
|
877
|
-
const merged = this.mergeSnapshots(docId, existing, incoming);
|
|
878
|
-
await this.wrapRequest(store.put(merged, docId), "write");
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
mergeSnapshots(docId, existing, incoming) {
|
|
882
|
-
try {
|
|
883
|
-
const doc = existing ? loro_crdt.LoroDoc.fromSnapshot(existing) : new loro_crdt.LoroDoc();
|
|
884
|
-
doc.import(incoming);
|
|
885
|
-
return doc.export({ mode: "snapshot" });
|
|
886
|
-
} catch (error) {
|
|
887
|
-
throw this.createError(`Failed to merge snapshot for "${docId}"`, error);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
async appendDocUpdate(db, docId, update) {
|
|
891
|
-
await this.runInTransaction(db, this.docUpdateStore, "readwrite", async (store) => {
|
|
892
|
-
const raw = await this.wrapRequest(store.get(docId), "read");
|
|
893
|
-
const queue = await this.normalizeUpdateQueue(raw);
|
|
894
|
-
queue.push(update.slice());
|
|
895
|
-
await this.wrapRequest(store.put({ updates: queue }, docId), "write");
|
|
896
|
-
});
|
|
897
|
-
}
|
|
898
|
-
async getDocUpdates(db, docId) {
|
|
899
|
-
const raw = await this.runInTransaction(db, this.docUpdateStore, "readonly", (store) => this.wrapRequest(store.get(docId), "read"));
|
|
900
|
-
return this.normalizeUpdateQueue(raw);
|
|
901
|
-
}
|
|
902
|
-
async clearDocUpdates(db, docId) {
|
|
903
|
-
await this.runInTransaction(db, this.docUpdateStore, "readwrite", (store) => this.wrapRequest(store.delete(docId), "delete"));
|
|
904
|
-
}
|
|
905
|
-
async writeSnapshot(db, docId, snapshot) {
|
|
906
|
-
await this.putBinary(db, this.docStore, docId, snapshot.slice());
|
|
907
|
-
}
|
|
908
|
-
async getBinaryFromDb(db, storeName, key) {
|
|
909
|
-
const value = await this.runInTransaction(db, storeName, "readonly", (store) => this.wrapRequest(store.get(key), "read"));
|
|
910
|
-
return this.normalizeBinary(value);
|
|
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);
|
|
911
486
|
}
|
|
912
487
|
async normalizeUpdateQueue(value) {
|
|
913
488
|
if (value == null) return [];
|
|
@@ -1117,241 +692,476 @@ async function writeFileAtomic(targetPath, data) {
|
|
|
1117
692
|
}
|
|
1118
693
|
|
|
1119
694
|
//#endregion
|
|
1120
|
-
//#region src/internal/
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
|
|
1153
|
-
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
1154
|
-
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
1155
|
-
const docId = (() => {
|
|
1156
|
-
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
1157
|
-
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
1158
|
-
})();
|
|
1159
|
-
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
1160
|
-
if (filter.docIds && !docId) return false;
|
|
1161
|
-
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
1162
|
-
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
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;
|
|
1163
727
|
}
|
|
1164
|
-
|
|
1165
|
-
}
|
|
728
|
+
console.info(prefix, ...args);
|
|
729
|
+
};
|
|
1166
730
|
};
|
|
1167
731
|
|
|
1168
732
|
//#endregion
|
|
1169
|
-
//#region src/
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
buffer.set(chunk, offset);
|
|
1186
|
-
offset += chunk.byteLength;
|
|
1187
|
-
}
|
|
1188
|
-
return buffer;
|
|
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
|
+
});
|
|
1189
749
|
}
|
|
1190
|
-
|
|
1191
|
-
if (
|
|
1192
|
-
if (
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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;
|
|
1196
758
|
}
|
|
1197
|
-
function
|
|
1198
|
-
|
|
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;
|
|
1199
765
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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;
|
|
1205
777
|
}
|
|
1206
|
-
|
|
1207
|
-
const
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
+
}
|
|
1213
790
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
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");
|
|
1224
804
|
}
|
|
1225
|
-
|
|
805
|
+
debug("close completed");
|
|
1226
806
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
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 };
|
|
1233
825
|
}
|
|
1234
|
-
return obj;
|
|
1235
826
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
continue;
|
|
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();
|
|
1274
864
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
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 };
|
|
1277
891
|
}
|
|
1278
892
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
+
};
|
|
1116
|
+
|
|
1117
|
+
//#endregion
|
|
1118
|
+
//#region src/internal/event-bus.ts
|
|
1119
|
+
var RepoEventBus = class {
|
|
1120
|
+
watchers = /* @__PURE__ */ new Set();
|
|
1121
|
+
eventByStack = [];
|
|
1122
|
+
watch(listener, filter = {}) {
|
|
1123
|
+
const entry = {
|
|
1124
|
+
listener,
|
|
1125
|
+
filter
|
|
1126
|
+
};
|
|
1127
|
+
this.watchers.add(entry);
|
|
1128
|
+
return { unsubscribe: () => {
|
|
1129
|
+
this.watchers.delete(entry);
|
|
1130
|
+
} };
|
|
1131
|
+
}
|
|
1132
|
+
emit(event) {
|
|
1133
|
+
for (const entry of this.watchers) if (this.shouldNotify(entry.filter, event)) entry.listener(event);
|
|
1134
|
+
}
|
|
1135
|
+
clear() {
|
|
1136
|
+
this.watchers.clear();
|
|
1137
|
+
this.eventByStack.length = 0;
|
|
1138
|
+
}
|
|
1139
|
+
pushEventBy(by) {
|
|
1140
|
+
this.eventByStack.push(by);
|
|
1141
|
+
}
|
|
1142
|
+
popEventBy() {
|
|
1143
|
+
this.eventByStack.pop();
|
|
1144
|
+
}
|
|
1145
|
+
resolveEventBy(defaultBy) {
|
|
1146
|
+
const index = this.eventByStack.length - 1;
|
|
1147
|
+
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
1148
|
+
}
|
|
1149
|
+
shouldNotify(filter, event) {
|
|
1150
|
+
if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
|
|
1151
|
+
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
1152
|
+
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
1153
|
+
const docId = (() => {
|
|
1154
|
+
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
1155
|
+
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
1156
|
+
})();
|
|
1157
|
+
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
1158
|
+
if (filter.docIds && !docId) return false;
|
|
1159
|
+
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
1160
|
+
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
1161
|
+
}
|
|
1162
|
+
return true;
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1355
1165
|
|
|
1356
1166
|
//#endregion
|
|
1357
1167
|
//#region src/internal/logging.ts
|
|
@@ -1370,23 +1180,18 @@ var DocManager = class {
|
|
|
1370
1180
|
getMetaFlock;
|
|
1371
1181
|
eventBus;
|
|
1372
1182
|
persistMeta;
|
|
1373
|
-
state;
|
|
1374
1183
|
docs = /* @__PURE__ */ new Map();
|
|
1375
1184
|
docSubscriptions = /* @__PURE__ */ new Map();
|
|
1376
1185
|
docFrontierUpdates = /* @__PURE__ */ new Map();
|
|
1377
1186
|
docPersistedVersions = /* @__PURE__ */ new Map();
|
|
1378
|
-
get docFrontierKeys() {
|
|
1379
|
-
return this.state.docFrontierKeys;
|
|
1380
|
-
}
|
|
1381
1187
|
constructor(options) {
|
|
1382
1188
|
this.storage = options.storage;
|
|
1383
1189
|
this.docFrontierDebounceMs = options.docFrontierDebounceMs;
|
|
1384
1190
|
this.getMetaFlock = options.getMetaFlock;
|
|
1385
1191
|
this.eventBus = options.eventBus;
|
|
1386
1192
|
this.persistMeta = options.persistMeta;
|
|
1387
|
-
this.state = options.state;
|
|
1388
1193
|
}
|
|
1389
|
-
async
|
|
1194
|
+
async openPersistedDoc(docId) {
|
|
1390
1195
|
return await this.ensureDoc(docId);
|
|
1391
1196
|
}
|
|
1392
1197
|
async openDetachedDoc(docId) {
|
|
@@ -1433,38 +1238,27 @@ var DocManager = class {
|
|
|
1433
1238
|
}
|
|
1434
1239
|
async updateDocFrontiers(docId, doc, defaultBy) {
|
|
1435
1240
|
const frontiers = doc.oplogFrontiers();
|
|
1436
|
-
const
|
|
1437
|
-
const
|
|
1241
|
+
const vv = doc.version();
|
|
1242
|
+
const existingFrontiers = this.readFrontiersFromFlock(docId);
|
|
1438
1243
|
let mutated = false;
|
|
1439
1244
|
const metaFlock = this.metaFlock;
|
|
1440
|
-
const
|
|
1441
|
-
for (const entry of existingKeys) {
|
|
1442
|
-
if (entry === key) continue;
|
|
1443
|
-
let oldFrontiers;
|
|
1444
|
-
try {
|
|
1445
|
-
oldFrontiers = JSON.parse(entry);
|
|
1446
|
-
} catch {
|
|
1447
|
-
continue;
|
|
1448
|
-
}
|
|
1449
|
-
if (includesFrontiers(vv, oldFrontiers)) {
|
|
1450
|
-
metaFlock.delete([
|
|
1451
|
-
"f",
|
|
1452
|
-
docId,
|
|
1453
|
-
entry
|
|
1454
|
-
]);
|
|
1455
|
-
mutated = true;
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
if (!existingKeys.has(key)) {
|
|
1245
|
+
for (const f of frontiers) if (existingFrontiers.get(f.peer) !== f.counter) {
|
|
1459
1246
|
metaFlock.put([
|
|
1460
1247
|
"f",
|
|
1461
1248
|
docId,
|
|
1462
|
-
|
|
1463
|
-
],
|
|
1249
|
+
f.peer
|
|
1250
|
+
], f.counter);
|
|
1464
1251
|
mutated = true;
|
|
1465
1252
|
}
|
|
1466
1253
|
if (mutated) {
|
|
1467
|
-
|
|
1254
|
+
for (const [peer, counter] of existingFrontiers) {
|
|
1255
|
+
const docCounterEnd = vv.get(peer);
|
|
1256
|
+
if (docCounterEnd != null && docCounterEnd > counter) metaFlock.delete([
|
|
1257
|
+
"f",
|
|
1258
|
+
docId,
|
|
1259
|
+
peer
|
|
1260
|
+
]);
|
|
1261
|
+
}
|
|
1468
1262
|
await this.persistMeta();
|
|
1469
1263
|
}
|
|
1470
1264
|
const by = this.eventBus.resolveEventBy(defaultBy);
|
|
@@ -1516,37 +1310,22 @@ var DocManager = class {
|
|
|
1516
1310
|
this.docFrontierUpdates.clear();
|
|
1517
1311
|
this.docs.clear();
|
|
1518
1312
|
this.docPersistedVersions.clear();
|
|
1519
|
-
this.docFrontierKeys.clear();
|
|
1520
1313
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
const frontierRows = this.metaFlock.scan({ prefix: ["f"] });
|
|
1524
|
-
for (const row of frontierRows) {
|
|
1525
|
-
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1526
|
-
const docId = row.key[1];
|
|
1527
|
-
const frontierKey = row.key[2];
|
|
1528
|
-
if (typeof docId !== "string" || typeof frontierKey !== "string") continue;
|
|
1529
|
-
const set = nextFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1530
|
-
set.add(frontierKey);
|
|
1531
|
-
nextFrontierKeys.set(docId, set);
|
|
1532
|
-
}
|
|
1533
|
-
this.docFrontierKeys.clear();
|
|
1534
|
-
for (const [docId, keys] of nextFrontierKeys) this.docFrontierKeys.set(docId, keys);
|
|
1314
|
+
get metaFlock() {
|
|
1315
|
+
return this.getMetaFlock();
|
|
1535
1316
|
}
|
|
1536
|
-
|
|
1317
|
+
readFrontiersFromFlock(docId) {
|
|
1537
1318
|
const rows = this.metaFlock.scan({ prefix: ["f", docId] });
|
|
1538
|
-
const
|
|
1319
|
+
const frontiers = /* @__PURE__ */ new Map();
|
|
1539
1320
|
for (const row of rows) {
|
|
1540
1321
|
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1541
|
-
|
|
1542
|
-
const
|
|
1543
|
-
if (typeof
|
|
1322
|
+
const peer = row.key[2];
|
|
1323
|
+
const counter = row.value;
|
|
1324
|
+
if (typeof peer !== "string") continue;
|
|
1325
|
+
if (typeof counter !== "number" || !Number.isFinite(counter)) continue;
|
|
1326
|
+
frontiers.set(peer, counter);
|
|
1544
1327
|
}
|
|
1545
|
-
|
|
1546
|
-
else this.docFrontierKeys.delete(docId);
|
|
1547
|
-
}
|
|
1548
|
-
get metaFlock() {
|
|
1549
|
-
return this.getMetaFlock();
|
|
1328
|
+
return frontiers;
|
|
1550
1329
|
}
|
|
1551
1330
|
registerDoc(docId, doc) {
|
|
1552
1331
|
this.docs.set(docId, doc);
|
|
@@ -1659,6 +1438,176 @@ var DocManager = class {
|
|
|
1659
1438
|
}
|
|
1660
1439
|
};
|
|
1661
1440
|
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/utils.ts
|
|
1443
|
+
async function streamToUint8Array(stream) {
|
|
1444
|
+
const reader = stream.getReader();
|
|
1445
|
+
const chunks = [];
|
|
1446
|
+
let total = 0;
|
|
1447
|
+
while (true) {
|
|
1448
|
+
const { done, value } = await reader.read();
|
|
1449
|
+
if (done) break;
|
|
1450
|
+
if (value) {
|
|
1451
|
+
chunks.push(value);
|
|
1452
|
+
total += value.byteLength;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const buffer = new Uint8Array(total);
|
|
1456
|
+
let offset = 0;
|
|
1457
|
+
for (const chunk of chunks) {
|
|
1458
|
+
buffer.set(chunk, offset);
|
|
1459
|
+
offset += chunk.byteLength;
|
|
1460
|
+
}
|
|
1461
|
+
return buffer;
|
|
1462
|
+
}
|
|
1463
|
+
async function assetContentToUint8Array(content) {
|
|
1464
|
+
if (content instanceof Uint8Array) return content;
|
|
1465
|
+
if (ArrayBuffer.isView(content)) return new Uint8Array(content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
|
|
1466
|
+
if (typeof Blob !== "undefined" && content instanceof Blob) return new Uint8Array(await content.arrayBuffer());
|
|
1467
|
+
if (typeof ReadableStream !== "undefined" && content instanceof ReadableStream) return streamToUint8Array(content);
|
|
1468
|
+
throw new TypeError("Unsupported asset content type");
|
|
1469
|
+
}
|
|
1470
|
+
function bytesToHex(bytes) {
|
|
1471
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1472
|
+
}
|
|
1473
|
+
async function computeSha256(bytes) {
|
|
1474
|
+
const globalCrypto = globalThis.crypto;
|
|
1475
|
+
if (globalCrypto?.subtle && typeof globalCrypto.subtle.digest === "function") {
|
|
1476
|
+
const digest = await globalCrypto.subtle.digest("SHA-256", bytes);
|
|
1477
|
+
return bytesToHex(new Uint8Array(digest));
|
|
1478
|
+
}
|
|
1479
|
+
try {
|
|
1480
|
+
const { createHash } = await import("node:crypto");
|
|
1481
|
+
const hash = createHash("sha256");
|
|
1482
|
+
hash.update(bytes);
|
|
1483
|
+
return hash.digest("hex");
|
|
1484
|
+
} catch {
|
|
1485
|
+
throw new Error("SHA-256 digest is not available in this environment");
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function cloneJsonValue(value) {
|
|
1489
|
+
if (value === null) return null;
|
|
1490
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
1491
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
1492
|
+
if (Array.isArray(value)) {
|
|
1493
|
+
const arr = [];
|
|
1494
|
+
for (const entry of value) {
|
|
1495
|
+
const cloned = cloneJsonValue(entry);
|
|
1496
|
+
if (cloned !== void 0) arr.push(cloned);
|
|
1497
|
+
}
|
|
1498
|
+
return arr;
|
|
1499
|
+
}
|
|
1500
|
+
if (value && typeof value === "object") {
|
|
1501
|
+
const input = value;
|
|
1502
|
+
const obj = {};
|
|
1503
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
1504
|
+
const cloned = cloneJsonValue(entry);
|
|
1505
|
+
if (cloned !== void 0) obj[key] = cloned;
|
|
1506
|
+
}
|
|
1507
|
+
return obj;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function cloneJsonObject(value) {
|
|
1511
|
+
const cloned = cloneJsonValue(value);
|
|
1512
|
+
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1513
|
+
return {};
|
|
1514
|
+
}
|
|
1515
|
+
function asJsonObject(value) {
|
|
1516
|
+
const cloned = cloneJsonValue(value);
|
|
1517
|
+
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1518
|
+
}
|
|
1519
|
+
function isJsonObjectValue(value) {
|
|
1520
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1521
|
+
}
|
|
1522
|
+
function stableStringify(value) {
|
|
1523
|
+
if (value === null) return "null";
|
|
1524
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1525
|
+
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
1526
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1527
|
+
if (!isJsonObjectValue(value)) return "null";
|
|
1528
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
1529
|
+
}
|
|
1530
|
+
function jsonEquals(a, b) {
|
|
1531
|
+
if (a === void 0 && b === void 0) return true;
|
|
1532
|
+
if (a === void 0 || b === void 0) return false;
|
|
1533
|
+
return stableStringify(a) === stableStringify(b);
|
|
1534
|
+
}
|
|
1535
|
+
function diffJsonObjects(previous, next) {
|
|
1536
|
+
const patch = {};
|
|
1537
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1538
|
+
if (previous) for (const key of Object.keys(previous)) keys.add(key);
|
|
1539
|
+
for (const key of Object.keys(next)) keys.add(key);
|
|
1540
|
+
for (const key of keys) {
|
|
1541
|
+
const prevValue = previous ? previous[key] : void 0;
|
|
1542
|
+
const nextValue = next[key];
|
|
1543
|
+
if (!jsonEquals(prevValue, nextValue)) {
|
|
1544
|
+
if (nextValue === void 0 && previous && key in previous) {
|
|
1545
|
+
patch[key] = null;
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
const cloned = cloneJsonValue(nextValue);
|
|
1549
|
+
if (cloned !== void 0) patch[key] = cloned;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return patch;
|
|
1553
|
+
}
|
|
1554
|
+
function assetMetaToJson(meta) {
|
|
1555
|
+
const json = {
|
|
1556
|
+
assetId: meta.assetId,
|
|
1557
|
+
size: meta.size,
|
|
1558
|
+
createdAt: meta.createdAt
|
|
1559
|
+
};
|
|
1560
|
+
if (meta.mime !== void 0) json.mime = meta.mime;
|
|
1561
|
+
if (meta.policy !== void 0) json.policy = meta.policy;
|
|
1562
|
+
if (meta.tag !== void 0) json.tag = meta.tag;
|
|
1563
|
+
return json;
|
|
1564
|
+
}
|
|
1565
|
+
function assetMetaFromJson(value) {
|
|
1566
|
+
const obj = asJsonObject(value);
|
|
1567
|
+
if (!obj) return void 0;
|
|
1568
|
+
const assetId = typeof obj.assetId === "string" ? obj.assetId : void 0;
|
|
1569
|
+
if (!assetId) return void 0;
|
|
1570
|
+
const size = typeof obj.size === "number" ? obj.size : void 0;
|
|
1571
|
+
const createdAt = typeof obj.createdAt === "number" ? obj.createdAt : void 0;
|
|
1572
|
+
if (size === void 0 || createdAt === void 0) return void 0;
|
|
1573
|
+
return {
|
|
1574
|
+
assetId,
|
|
1575
|
+
size,
|
|
1576
|
+
createdAt,
|
|
1577
|
+
...typeof obj.mime === "string" ? { mime: obj.mime } : {},
|
|
1578
|
+
...typeof obj.policy === "string" ? { policy: obj.policy } : {},
|
|
1579
|
+
...typeof obj.tag === "string" ? { tag: obj.tag } : {}
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
function assetMetadataEqual(a, b) {
|
|
1583
|
+
if (!a && !b) return true;
|
|
1584
|
+
if (!a || !b) return false;
|
|
1585
|
+
return stableStringify(assetMetaToJson(a)) === stableStringify(assetMetaToJson(b));
|
|
1586
|
+
}
|
|
1587
|
+
function cloneRepoAssetMetadata(meta) {
|
|
1588
|
+
return {
|
|
1589
|
+
assetId: meta.assetId,
|
|
1590
|
+
size: meta.size,
|
|
1591
|
+
createdAt: meta.createdAt,
|
|
1592
|
+
...meta.mime !== void 0 ? { mime: meta.mime } : {},
|
|
1593
|
+
...meta.policy !== void 0 ? { policy: meta.policy } : {},
|
|
1594
|
+
...meta.tag !== void 0 ? { tag: meta.tag } : {}
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function toReadableStream(bytes) {
|
|
1598
|
+
return new ReadableStream({ start(controller) {
|
|
1599
|
+
controller.enqueue(bytes);
|
|
1600
|
+
controller.close();
|
|
1601
|
+
} });
|
|
1602
|
+
}
|
|
1603
|
+
function matchesQuery(docId, _metadata, query) {
|
|
1604
|
+
if (!query) return true;
|
|
1605
|
+
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
1606
|
+
if (query.start && docId < query.start) return false;
|
|
1607
|
+
if (query.end && docId > query.end) return false;
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1662
1611
|
//#endregion
|
|
1663
1612
|
//#region src/internal/metadata-manager.ts
|
|
1664
1613
|
var MetadataManager = class {
|
|
@@ -2456,13 +2405,11 @@ var FlockHydrator = class {
|
|
|
2456
2405
|
const nextMetadata = this.readAllDocMetadata();
|
|
2457
2406
|
this.metadataManager.replaceAll(nextMetadata, by);
|
|
2458
2407
|
this.assetManager.hydrateFromFlock(by);
|
|
2459
|
-
this.docManager.hydrateFrontierKeys();
|
|
2460
2408
|
}
|
|
2461
2409
|
applyEvents(events, by) {
|
|
2462
2410
|
if (!events.length) return;
|
|
2463
2411
|
const docMetadataIds = /* @__PURE__ */ new Set();
|
|
2464
2412
|
const docAssetIds = /* @__PURE__ */ new Set();
|
|
2465
|
-
const docFrontiersIds = /* @__PURE__ */ new Set();
|
|
2466
2413
|
const assetIds = /* @__PURE__ */ new Set();
|
|
2467
2414
|
for (const event of events) {
|
|
2468
2415
|
const key = event.key;
|
|
@@ -2479,15 +2426,11 @@ var FlockHydrator = class {
|
|
|
2479
2426
|
const assetId = key[2];
|
|
2480
2427
|
if (typeof docId === "string") docAssetIds.add(docId);
|
|
2481
2428
|
if (typeof assetId === "string") assetIds.add(assetId);
|
|
2482
|
-
} else if (root === "f") {
|
|
2483
|
-
const docId = key[1];
|
|
2484
|
-
if (typeof docId === "string") docFrontiersIds.add(docId);
|
|
2485
2429
|
}
|
|
2486
2430
|
}
|
|
2487
2431
|
for (const assetId of assetIds) this.assetManager.refreshAssetMetadataEntry(assetId, by);
|
|
2488
2432
|
for (const docId of docMetadataIds) this.metadataManager.refreshFromFlock(docId, by);
|
|
2489
2433
|
for (const docId of docAssetIds) this.assetManager.refreshDocAssetsEntry(docId, by);
|
|
2490
|
-
for (const docId of docFrontiersIds) this.docManager.refreshDocFrontierKeys(docId);
|
|
2491
2434
|
}
|
|
2492
2435
|
readAllDocMetadata() {
|
|
2493
2436
|
const nextMetadata = /* @__PURE__ */ new Map();
|
|
@@ -2698,8 +2641,7 @@ function createRepoState() {
|
|
|
2698
2641
|
docAssets: /* @__PURE__ */ new Map(),
|
|
2699
2642
|
assets: /* @__PURE__ */ new Map(),
|
|
2700
2643
|
orphanedAssets: /* @__PURE__ */ new Map(),
|
|
2701
|
-
assetToDocRefs: /* @__PURE__ */ new Map()
|
|
2702
|
-
docFrontierKeys: /* @__PURE__ */ new Map()
|
|
2644
|
+
assetToDocRefs: /* @__PURE__ */ new Map()
|
|
2703
2645
|
};
|
|
2704
2646
|
}
|
|
2705
2647
|
|
|
@@ -2735,8 +2677,7 @@ var LoroRepo = class LoroRepo {
|
|
|
2735
2677
|
docFrontierDebounceMs,
|
|
2736
2678
|
getMetaFlock: () => this.metaFlock,
|
|
2737
2679
|
eventBus: this.eventBus,
|
|
2738
|
-
persistMeta: () => this.persistMeta()
|
|
2739
|
-
state: this.state
|
|
2680
|
+
persistMeta: () => this.persistMeta()
|
|
2740
2681
|
});
|
|
2741
2682
|
this.metadataManager = new MetadataManager({
|
|
2742
2683
|
getMetaFlock: () => this.metaFlock,
|
|
@@ -2825,7 +2766,7 @@ var LoroRepo = class LoroRepo {
|
|
|
2825
2766
|
*/
|
|
2826
2767
|
async openPersistedDoc(docId) {
|
|
2827
2768
|
return {
|
|
2828
|
-
doc: await this.docManager.
|
|
2769
|
+
doc: await this.docManager.openPersistedDoc(docId),
|
|
2829
2770
|
syncOnce: () => {
|
|
2830
2771
|
return this.sync({
|
|
2831
2772
|
scope: "doc",
|