loro-repo 0.3.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/README.md +18 -17
- package/dist/index.cjs +1101 -1191
- package/dist/index.cjs.map +1 -1
- package/dist/{index-DsCaL9JX.d.cts → index.d.cts} +117 -136
- package/dist/{index-tq65q3qY.d.ts → index.d.ts} +118 -137
- package/dist/index.js +1084 -1178
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,876 +1,452 @@
|
|
|
1
1
|
import { Flock } from "@loro-dev/flock";
|
|
2
|
-
import { FlockAdaptor, LoroAdaptor } from "loro-adaptors";
|
|
3
|
-
import { CrdtType, bytesToHex } from "loro-protocol";
|
|
4
|
-
import { LoroWebsocketClient } from "loro-websocket";
|
|
5
2
|
import { LoroDoc } from "loro-crdt";
|
|
6
3
|
import { promises } from "node:fs";
|
|
7
4
|
import * as path from "node:path";
|
|
8
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { LoroAdaptor } from "loro-adaptors/loro";
|
|
7
|
+
import { bytesToHex } from "loro-protocol";
|
|
8
|
+
import { LoroWebsocketClient } from "loro-websocket";
|
|
9
|
+
import { FlockAdaptor } from "loro-adaptors/flock";
|
|
9
10
|
|
|
10
|
-
//#region src/
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (typeof globalThis !== "object" || globalThis === null) return;
|
|
19
|
-
return globalThis.process?.env;
|
|
20
|
-
};
|
|
21
|
-
const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
|
|
22
|
-
const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
|
|
23
|
-
const wildcardTokens = new Set([
|
|
24
|
-
"*",
|
|
25
|
-
"1",
|
|
26
|
-
"true",
|
|
27
|
-
"all"
|
|
28
|
-
]);
|
|
29
|
-
const namespaceSet = new Set(normalizedNamespaces);
|
|
30
|
-
const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
|
|
31
|
-
const isDebugEnabled = (namespace) => {
|
|
32
|
-
if (!namespaceSet.size) return false;
|
|
33
|
-
if (!namespace) return hasWildcard;
|
|
34
|
-
const normalized = namespace.toLowerCase();
|
|
35
|
-
if (hasWildcard) return true;
|
|
36
|
-
if (namespaceSet.has(normalized)) return true;
|
|
37
|
-
const [root] = normalized.split(":");
|
|
38
|
-
return namespaceSet.has(root);
|
|
39
|
-
};
|
|
40
|
-
const createDebugLogger = (namespace) => {
|
|
41
|
-
const normalized = namespace.toLowerCase();
|
|
42
|
-
return (...args) => {
|
|
43
|
-
if (!isDebugEnabled(normalized)) return;
|
|
44
|
-
const prefix = `[loro-repo:${namespace}]`;
|
|
45
|
-
if (args.length === 0) {
|
|
46
|
-
console.info(prefix);
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
console.info(prefix, ...args);
|
|
11
|
+
//#region src/transport/broadcast-channel.ts
|
|
12
|
+
function deferred() {
|
|
13
|
+
let resolve;
|
|
14
|
+
return {
|
|
15
|
+
promise: new Promise((res) => {
|
|
16
|
+
resolve = res;
|
|
17
|
+
}),
|
|
18
|
+
resolve
|
|
50
19
|
};
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
//#endregion
|
|
54
|
-
//#region src/transport/websocket.ts
|
|
55
|
-
const debug = createDebugLogger("transport:websocket");
|
|
56
|
-
function withTimeout(promise, timeoutMs) {
|
|
57
|
-
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
58
|
-
return new Promise((resolve, reject) => {
|
|
59
|
-
const timer = setTimeout(() => {
|
|
60
|
-
reject(/* @__PURE__ */ new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
61
|
-
}, timeoutMs);
|
|
62
|
-
promise.then((value) => {
|
|
63
|
-
clearTimeout(timer);
|
|
64
|
-
resolve(value);
|
|
65
|
-
}).catch((error) => {
|
|
66
|
-
clearTimeout(timer);
|
|
67
|
-
reject(error);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
20
|
}
|
|
71
|
-
function
|
|
72
|
-
if (typeof
|
|
73
|
-
|
|
74
|
-
|
|
21
|
+
function randomInstanceId() {
|
|
22
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
23
|
+
return Math.random().toString(36).slice(2);
|
|
24
|
+
}
|
|
25
|
+
function ensureBroadcastChannel() {
|
|
26
|
+
if (typeof BroadcastChannel === "undefined") throw new Error("BroadcastChannel API is not available in this environment");
|
|
27
|
+
return BroadcastChannel;
|
|
28
|
+
}
|
|
29
|
+
function encodeDocChannelId(docId) {
|
|
30
|
+
try {
|
|
31
|
+
return encodeURIComponent(docId);
|
|
75
32
|
} catch {
|
|
76
|
-
return
|
|
33
|
+
return docId.replace(/[^a-z0-9_-]/gi, "_");
|
|
77
34
|
}
|
|
78
|
-
return fallback;
|
|
79
35
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
if (!a || !b) return false;
|
|
83
|
-
if (a.length !== b.length) return false;
|
|
84
|
-
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
85
|
-
return true;
|
|
36
|
+
function postChannelMessage(channel, message) {
|
|
37
|
+
channel.postMessage(message);
|
|
86
38
|
}
|
|
87
39
|
/**
|
|
88
|
-
*
|
|
40
|
+
* TransportAdapter that relies on the BroadcastChannel API to fan out metadata
|
|
41
|
+
* and document updates between browser tabs within the same origin.
|
|
89
42
|
*/
|
|
90
|
-
var
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
43
|
+
var BroadcastChannelTransportAdapter = class {
|
|
44
|
+
instanceId = randomInstanceId();
|
|
45
|
+
namespace;
|
|
46
|
+
metaChannelName;
|
|
47
|
+
connected = false;
|
|
48
|
+
metaState;
|
|
49
|
+
docStates = /* @__PURE__ */ new Map();
|
|
50
|
+
constructor(options = {}) {
|
|
51
|
+
ensureBroadcastChannel();
|
|
52
|
+
this.namespace = options.namespace ?? "loro-repo";
|
|
53
|
+
this.metaChannelName = options.metaChannelName ?? `${this.namespace}-meta`;
|
|
97
54
|
}
|
|
98
|
-
async connect(
|
|
99
|
-
|
|
100
|
-
debug("connect requested", { status: client.getStatus() });
|
|
101
|
-
try {
|
|
102
|
-
await client.connect();
|
|
103
|
-
debug("client.connect resolved");
|
|
104
|
-
await client.waitConnected();
|
|
105
|
-
debug("client.waitConnected resolved", { status: client.getStatus() });
|
|
106
|
-
} catch (error) {
|
|
107
|
-
debug("connect failed", error);
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
55
|
+
async connect() {
|
|
56
|
+
this.connected = true;
|
|
110
57
|
}
|
|
111
58
|
async close() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.docSessions.clear();
|
|
118
|
-
await this.teardownMetadataSession().catch(() => {});
|
|
119
|
-
if (this.client) {
|
|
120
|
-
const client = this.client;
|
|
121
|
-
this.client = void 0;
|
|
122
|
-
client.destroy();
|
|
123
|
-
debug("websocket client destroyed");
|
|
59
|
+
this.connected = false;
|
|
60
|
+
if (this.metaState) {
|
|
61
|
+
for (const entry of this.metaState.listeners) entry.unsubscribe();
|
|
62
|
+
this.metaState.channel.close();
|
|
63
|
+
this.metaState = void 0;
|
|
124
64
|
}
|
|
125
|
-
|
|
65
|
+
for (const [docId] of this.docStates) this.teardownDocChannel(docId);
|
|
66
|
+
this.docStates.clear();
|
|
126
67
|
}
|
|
127
68
|
isConnected() {
|
|
128
|
-
return this.
|
|
69
|
+
return this.connected;
|
|
129
70
|
}
|
|
130
|
-
async syncMeta(flock,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
await withTimeout((await this.ensureMetadataSession(flock, {
|
|
138
|
-
roomId: this.options.metadataRoomId,
|
|
139
|
-
auth: this.options.metadataAuth
|
|
140
|
-
})).firstSynced, options?.timeout);
|
|
141
|
-
debug("syncMeta completed", { roomId: this.options.metadataRoomId });
|
|
142
|
-
return { ok: true };
|
|
143
|
-
} catch (error) {
|
|
144
|
-
debug("syncMeta failed", error);
|
|
145
|
-
return { ok: false };
|
|
146
|
-
}
|
|
71
|
+
async syncMeta(flock, _options) {
|
|
72
|
+
const subscription = this.joinMetaRoom(flock);
|
|
73
|
+
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
74
|
+
await subscription.firstSyncedWithRemote;
|
|
75
|
+
subscription.unsubscribe();
|
|
76
|
+
return { ok: true };
|
|
147
77
|
}
|
|
148
|
-
joinMetaRoom(flock,
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
78
|
+
joinMetaRoom(flock, _params) {
|
|
79
|
+
const state = this.ensureMetaChannel();
|
|
80
|
+
const { promise, resolve } = deferred();
|
|
81
|
+
const listener = {
|
|
82
|
+
flock,
|
|
83
|
+
muted: false,
|
|
84
|
+
unsubscribe: flock.subscribe(() => {
|
|
85
|
+
if (listener.muted) return;
|
|
86
|
+
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
87
|
+
postChannelMessage(state.channel, {
|
|
88
|
+
kind: "meta-export",
|
|
89
|
+
from: this.instanceId,
|
|
90
|
+
bundle
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}),
|
|
94
|
+
resolveFirst: resolve,
|
|
95
|
+
firstSynced: promise
|
|
96
|
+
};
|
|
97
|
+
state.listeners.add(listener);
|
|
98
|
+
postChannelMessage(state.channel, {
|
|
99
|
+
kind: "meta-request",
|
|
100
|
+
from: this.instanceId
|
|
156
101
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
102
|
+
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
103
|
+
postChannelMessage(state.channel, {
|
|
104
|
+
kind: "meta-export",
|
|
105
|
+
from: this.instanceId,
|
|
106
|
+
bundle
|
|
107
|
+
});
|
|
160
108
|
});
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const subscription = {
|
|
109
|
+
queueMicrotask(() => resolve());
|
|
110
|
+
return {
|
|
164
111
|
unsubscribe: () => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
debug("tearing down metadata session due to refCount=0", { roomId: session.roomId });
|
|
173
|
-
this.teardownMetadataSession(session).catch(() => {});
|
|
174
|
-
}
|
|
175
|
-
});
|
|
112
|
+
listener.unsubscribe();
|
|
113
|
+
state.listeners.delete(listener);
|
|
114
|
+
if (!state.listeners.size) {
|
|
115
|
+
state.channel.removeEventListener("message", state.onMessage);
|
|
116
|
+
state.channel.close();
|
|
117
|
+
this.metaState = void 0;
|
|
118
|
+
}
|
|
176
119
|
},
|
|
177
|
-
firstSyncedWithRemote: firstSynced,
|
|
120
|
+
firstSyncedWithRemote: listener.firstSynced,
|
|
178
121
|
get connected() {
|
|
179
|
-
return
|
|
122
|
+
return true;
|
|
180
123
|
}
|
|
181
124
|
};
|
|
182
|
-
ensure.then((session) => {
|
|
183
|
-
session.refCount += 1;
|
|
184
|
-
debug("metadata session refCount incremented", {
|
|
185
|
-
roomId: session.roomId,
|
|
186
|
-
refCount: session.refCount
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
return subscription;
|
|
190
|
-
}
|
|
191
|
-
async syncDoc(docId, doc, options) {
|
|
192
|
-
debug("syncDoc requested", { docId });
|
|
193
|
-
try {
|
|
194
|
-
const session = await this.ensureDocSession(docId, doc, {});
|
|
195
|
-
await withTimeout(session.firstSynced, options?.timeout);
|
|
196
|
-
debug("syncDoc completed", {
|
|
197
|
-
docId,
|
|
198
|
-
roomId: session.roomId
|
|
199
|
-
});
|
|
200
|
-
return { ok: true };
|
|
201
|
-
} catch (error) {
|
|
202
|
-
debug("syncDoc failed", {
|
|
203
|
-
docId,
|
|
204
|
-
error
|
|
205
|
-
});
|
|
206
|
-
return { ok: false };
|
|
207
|
-
}
|
|
208
125
|
}
|
|
209
|
-
|
|
210
|
-
|
|
126
|
+
async syncDoc(docId, doc, _options) {
|
|
127
|
+
const subscription = this.joinDocRoom(docId, doc);
|
|
128
|
+
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
129
|
+
await subscription.firstSyncedWithRemote;
|
|
130
|
+
subscription.unsubscribe();
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
joinDocRoom(docId, doc, _params) {
|
|
134
|
+
const state = this.ensureDocChannel(docId);
|
|
135
|
+
const { promise, resolve } = deferred();
|
|
136
|
+
const listener = {
|
|
137
|
+
doc,
|
|
138
|
+
muted: false,
|
|
139
|
+
unsubscribe: doc.subscribe(() => {
|
|
140
|
+
if (listener.muted) return;
|
|
141
|
+
const payload = doc.export({ mode: "update" });
|
|
142
|
+
postChannelMessage(state.channel, {
|
|
143
|
+
kind: "doc-update",
|
|
144
|
+
docId,
|
|
145
|
+
from: this.instanceId,
|
|
146
|
+
mode: "update",
|
|
147
|
+
payload
|
|
148
|
+
});
|
|
149
|
+
}),
|
|
150
|
+
resolveFirst: resolve,
|
|
151
|
+
firstSynced: promise
|
|
152
|
+
};
|
|
153
|
+
state.listeners.add(listener);
|
|
154
|
+
postChannelMessage(state.channel, {
|
|
155
|
+
kind: "doc-request",
|
|
211
156
|
docId,
|
|
212
|
-
|
|
213
|
-
hasAuthOverride: Boolean(params?.auth && params.auth.length)
|
|
157
|
+
from: this.instanceId
|
|
214
158
|
});
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
159
|
+
postChannelMessage(state.channel, {
|
|
160
|
+
kind: "doc-update",
|
|
161
|
+
docId,
|
|
162
|
+
from: this.instanceId,
|
|
163
|
+
mode: "snapshot",
|
|
164
|
+
payload: doc.export({ mode: "snapshot" })
|
|
165
|
+
});
|
|
166
|
+
queueMicrotask(() => resolve());
|
|
167
|
+
return {
|
|
219
168
|
unsubscribe: () => {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
docId,
|
|
224
|
-
roomId: session.roomId,
|
|
225
|
-
refCount: session.refCount
|
|
226
|
-
});
|
|
227
|
-
if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
|
|
228
|
-
});
|
|
169
|
+
listener.unsubscribe();
|
|
170
|
+
state.listeners.delete(listener);
|
|
171
|
+
if (!state.listeners.size) this.teardownDocChannel(docId);
|
|
229
172
|
},
|
|
230
|
-
firstSyncedWithRemote: firstSynced,
|
|
173
|
+
firstSyncedWithRemote: listener.firstSynced,
|
|
231
174
|
get connected() {
|
|
232
|
-
return
|
|
175
|
+
return true;
|
|
233
176
|
}
|
|
234
177
|
};
|
|
235
|
-
ensure.then((session) => {
|
|
236
|
-
session.refCount += 1;
|
|
237
|
-
debug("doc session refCount incremented", {
|
|
238
|
-
docId,
|
|
239
|
-
roomId: session.roomId,
|
|
240
|
-
refCount: session.refCount
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
return subscription;
|
|
244
178
|
}
|
|
245
|
-
|
|
246
|
-
if (this.
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
179
|
+
ensureMetaChannel() {
|
|
180
|
+
if (this.metaState) return this.metaState;
|
|
181
|
+
const channel = new (ensureBroadcastChannel())(this.metaChannelName);
|
|
182
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
183
|
+
const onMessage = (event) => {
|
|
184
|
+
const message = event.data;
|
|
185
|
+
if (!message || message.from === this.instanceId) return;
|
|
186
|
+
if (message.kind === "meta-export") for (const entry of listeners) {
|
|
187
|
+
entry.muted = true;
|
|
188
|
+
entry.flock.importJson(message.bundle);
|
|
189
|
+
entry.muted = false;
|
|
190
|
+
entry.resolveFirst();
|
|
191
|
+
}
|
|
192
|
+
else if (message.kind === "meta-request") {
|
|
193
|
+
const first = listeners.values().next().value;
|
|
194
|
+
if (!first) return;
|
|
195
|
+
Promise.resolve(first.flock.exportJson()).then((bundle) => {
|
|
196
|
+
postChannelMessage(channel, {
|
|
197
|
+
kind: "meta-export",
|
|
198
|
+
from: this.instanceId,
|
|
199
|
+
bundle
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
channel.addEventListener("message", onMessage);
|
|
205
|
+
this.metaState = {
|
|
206
|
+
channel,
|
|
207
|
+
listeners,
|
|
208
|
+
onMessage
|
|
209
|
+
};
|
|
210
|
+
return this.metaState;
|
|
261
211
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
roomId: params.roomId,
|
|
290
|
-
crdtAdaptor: adaptor,
|
|
291
|
-
auth: params.auth
|
|
292
|
-
});
|
|
293
|
-
const firstSynced = room.waitForReachingServerVersion();
|
|
294
|
-
firstSynced.then(() => {
|
|
295
|
-
debug("metadata session firstSynced resolved", { roomId: params.roomId });
|
|
296
|
-
}, (error) => {
|
|
297
|
-
debug("metadata session firstSynced rejected", {
|
|
298
|
-
roomId: params.roomId,
|
|
299
|
-
error
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
const session = {
|
|
303
|
-
adaptor,
|
|
304
|
-
room,
|
|
305
|
-
firstSynced,
|
|
306
|
-
flock,
|
|
307
|
-
roomId: params.roomId,
|
|
308
|
-
auth: params.auth,
|
|
309
|
-
refCount: 0
|
|
212
|
+
ensureDocChannel(docId) {
|
|
213
|
+
const existing = this.docStates.get(docId);
|
|
214
|
+
if (existing) return existing;
|
|
215
|
+
const channel = new (ensureBroadcastChannel())(`${this.namespace}-doc-${encodeDocChannelId(docId)}`);
|
|
216
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
217
|
+
const onMessage = (event) => {
|
|
218
|
+
const message = event.data;
|
|
219
|
+
if (!message || message.from === this.instanceId) return;
|
|
220
|
+
if (message.kind === "doc-update") for (const entry of listeners) {
|
|
221
|
+
entry.muted = true;
|
|
222
|
+
entry.doc.import(message.payload);
|
|
223
|
+
entry.muted = false;
|
|
224
|
+
entry.resolveFirst();
|
|
225
|
+
}
|
|
226
|
+
else if (message.kind === "doc-request") {
|
|
227
|
+
const first = listeners.values().next().value;
|
|
228
|
+
if (!first) return;
|
|
229
|
+
const payload = message.docId === docId ? first.doc.export({ mode: "snapshot" }) : void 0;
|
|
230
|
+
if (!payload) return;
|
|
231
|
+
postChannelMessage(channel, {
|
|
232
|
+
kind: "doc-update",
|
|
233
|
+
docId,
|
|
234
|
+
from: this.instanceId,
|
|
235
|
+
mode: "snapshot",
|
|
236
|
+
payload
|
|
237
|
+
});
|
|
238
|
+
}
|
|
310
239
|
};
|
|
311
|
-
|
|
312
|
-
|
|
240
|
+
channel.addEventListener("message", onMessage);
|
|
241
|
+
const state = {
|
|
242
|
+
channel,
|
|
243
|
+
listeners,
|
|
244
|
+
onMessage
|
|
245
|
+
};
|
|
246
|
+
this.docStates.set(docId, state);
|
|
247
|
+
return state;
|
|
313
248
|
}
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
if (!
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
await room.leave();
|
|
322
|
-
debug("metadata room left", { roomId: target.roomId });
|
|
323
|
-
} catch (error) {
|
|
324
|
-
debug("metadata room leave failed; destroying", {
|
|
325
|
-
roomId: target.roomId,
|
|
326
|
-
error
|
|
327
|
-
});
|
|
328
|
-
await room.destroy().catch(() => {});
|
|
329
|
-
}
|
|
330
|
-
adaptor.destroy();
|
|
331
|
-
debug("metadata session destroyed", { roomId: target.roomId });
|
|
332
|
-
}
|
|
333
|
-
async ensureDocSession(docId, doc, params) {
|
|
334
|
-
debug("ensureDocSession invoked", { docId });
|
|
335
|
-
const client = this.ensureClient();
|
|
336
|
-
await client.waitConnected();
|
|
337
|
-
debug("websocket client ready for doc session", {
|
|
338
|
-
docId,
|
|
339
|
-
status: client.getStatus()
|
|
340
|
-
});
|
|
341
|
-
const existing = this.docSessions.get(docId);
|
|
342
|
-
const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
|
|
343
|
-
const roomId = normalizeRoomId(params.roomId, derivedRoomId);
|
|
344
|
-
const auth = params.auth ?? this.options.docAuth?.(docId);
|
|
345
|
-
debug("doc session params resolved", {
|
|
346
|
-
docId,
|
|
347
|
-
roomId,
|
|
348
|
-
hasAuth: Boolean(auth && auth.length)
|
|
349
|
-
});
|
|
350
|
-
if (existing && existing.doc === doc && existing.roomId === roomId) {
|
|
351
|
-
debug("reusing doc session", {
|
|
352
|
-
docId,
|
|
353
|
-
roomId,
|
|
354
|
-
refCount: existing.refCount
|
|
355
|
-
});
|
|
356
|
-
return existing;
|
|
357
|
-
}
|
|
358
|
-
if (existing) {
|
|
359
|
-
debug("doc session mismatch; leaving existing session", {
|
|
360
|
-
docId,
|
|
361
|
-
previousRoomId: existing.roomId,
|
|
362
|
-
nextRoomId: roomId
|
|
363
|
-
});
|
|
364
|
-
await this.leaveDocSession(docId).catch(() => {});
|
|
365
|
-
}
|
|
366
|
-
const adaptor = new LoroAdaptor(doc);
|
|
367
|
-
debug("joining doc room", {
|
|
368
|
-
docId,
|
|
369
|
-
roomId,
|
|
370
|
-
hasAuth: Boolean(auth && auth.length)
|
|
371
|
-
});
|
|
372
|
-
const room = await client.join({
|
|
373
|
-
roomId,
|
|
374
|
-
crdtAdaptor: adaptor,
|
|
375
|
-
auth
|
|
376
|
-
});
|
|
377
|
-
const firstSynced = room.waitForReachingServerVersion();
|
|
378
|
-
firstSynced.then(() => {
|
|
379
|
-
debug("doc session firstSynced resolved", {
|
|
380
|
-
docId,
|
|
381
|
-
roomId
|
|
382
|
-
});
|
|
383
|
-
}, (error) => {
|
|
384
|
-
debug("doc session firstSynced rejected", {
|
|
385
|
-
docId,
|
|
386
|
-
roomId,
|
|
387
|
-
error
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
const session = {
|
|
391
|
-
adaptor,
|
|
392
|
-
room,
|
|
393
|
-
firstSynced,
|
|
394
|
-
doc,
|
|
395
|
-
roomId,
|
|
396
|
-
refCount: 0
|
|
397
|
-
};
|
|
398
|
-
this.docSessions.set(docId, session);
|
|
399
|
-
return session;
|
|
400
|
-
}
|
|
401
|
-
async leaveDocSession(docId) {
|
|
402
|
-
const session = this.docSessions.get(docId);
|
|
403
|
-
if (!session) {
|
|
404
|
-
debug("leaveDocSession invoked but no session found", { docId });
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
this.docSessions.delete(docId);
|
|
408
|
-
debug("leaving doc session", {
|
|
409
|
-
docId,
|
|
410
|
-
roomId: session.roomId
|
|
411
|
-
});
|
|
412
|
-
try {
|
|
413
|
-
await session.room.leave();
|
|
414
|
-
debug("doc room left", {
|
|
415
|
-
docId,
|
|
416
|
-
roomId: session.roomId
|
|
417
|
-
});
|
|
418
|
-
} catch (error) {
|
|
419
|
-
debug("doc room leave failed; destroying", {
|
|
420
|
-
docId,
|
|
421
|
-
roomId: session.roomId,
|
|
422
|
-
error
|
|
423
|
-
});
|
|
424
|
-
await session.room.destroy().catch(() => {});
|
|
425
|
-
}
|
|
426
|
-
session.adaptor.destroy();
|
|
427
|
-
debug("doc session destroyed", {
|
|
428
|
-
docId,
|
|
429
|
-
roomId: session.roomId
|
|
430
|
-
});
|
|
249
|
+
teardownDocChannel(docId) {
|
|
250
|
+
const state = this.docStates.get(docId);
|
|
251
|
+
if (!state) return;
|
|
252
|
+
for (const entry of state.listeners) entry.unsubscribe();
|
|
253
|
+
state.channel.removeEventListener("message", state.onMessage);
|
|
254
|
+
state.channel.close();
|
|
255
|
+
this.docStates.delete(docId);
|
|
431
256
|
}
|
|
432
257
|
};
|
|
433
258
|
|
|
434
259
|
//#endregion
|
|
435
|
-
//#region src/
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (typeof
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
if (typeof
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
function encodeDocChannelId(docId) {
|
|
454
|
-
try {
|
|
455
|
-
return encodeURIComponent(docId);
|
|
260
|
+
//#region src/storage/indexeddb.ts
|
|
261
|
+
const DEFAULT_DB_NAME = "loro-repo";
|
|
262
|
+
const DEFAULT_DB_VERSION = 1;
|
|
263
|
+
const DEFAULT_DOC_STORE = "docs";
|
|
264
|
+
const DEFAULT_META_STORE = "meta";
|
|
265
|
+
const DEFAULT_ASSET_STORE = "assets";
|
|
266
|
+
const DEFAULT_DOC_UPDATE_STORE = "doc-updates";
|
|
267
|
+
const DEFAULT_META_KEY = "snapshot";
|
|
268
|
+
const textDecoder$1 = new TextDecoder();
|
|
269
|
+
function describeUnknown(cause) {
|
|
270
|
+
if (typeof cause === "string") return cause;
|
|
271
|
+
if (typeof cause === "number" || typeof cause === "boolean") return String(cause);
|
|
272
|
+
if (typeof cause === "bigint") return cause.toString();
|
|
273
|
+
if (typeof cause === "symbol") return cause.description ?? cause.toString();
|
|
274
|
+
if (typeof cause === "function") return `[function ${cause.name ?? "anonymous"}]`;
|
|
275
|
+
if (cause && typeof cause === "object") try {
|
|
276
|
+
return JSON.stringify(cause);
|
|
456
277
|
} catch {
|
|
457
|
-
return
|
|
278
|
+
return "[object]";
|
|
458
279
|
}
|
|
280
|
+
return String(cause);
|
|
459
281
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
connected = false;
|
|
472
|
-
metaState;
|
|
473
|
-
docStates = /* @__PURE__ */ new Map();
|
|
282
|
+
var IndexedDBStorageAdaptor = class {
|
|
283
|
+
idb;
|
|
284
|
+
dbName;
|
|
285
|
+
version;
|
|
286
|
+
docStore;
|
|
287
|
+
docUpdateStore;
|
|
288
|
+
metaStore;
|
|
289
|
+
assetStore;
|
|
290
|
+
metaKey;
|
|
291
|
+
dbPromise;
|
|
292
|
+
closed = false;
|
|
474
293
|
constructor(options = {}) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
this.
|
|
294
|
+
const idbFactory = globalThis.indexedDB;
|
|
295
|
+
if (!idbFactory) throw new Error("IndexedDB is not available in this environment");
|
|
296
|
+
this.idb = idbFactory;
|
|
297
|
+
this.dbName = options.dbName ?? DEFAULT_DB_NAME;
|
|
298
|
+
this.version = options.version ?? DEFAULT_DB_VERSION;
|
|
299
|
+
this.docStore = options.docStoreName ?? DEFAULT_DOC_STORE;
|
|
300
|
+
this.docUpdateStore = options.docUpdateStoreName ?? DEFAULT_DOC_UPDATE_STORE;
|
|
301
|
+
this.metaStore = options.metaStoreName ?? DEFAULT_META_STORE;
|
|
302
|
+
this.assetStore = options.assetStoreName ?? DEFAULT_ASSET_STORE;
|
|
303
|
+
this.metaKey = options.metaKey ?? DEFAULT_META_KEY;
|
|
478
304
|
}
|
|
479
|
-
async
|
|
480
|
-
|
|
305
|
+
async save(payload) {
|
|
306
|
+
const db = await this.ensureDb();
|
|
307
|
+
switch (payload.type) {
|
|
308
|
+
case "doc-snapshot": {
|
|
309
|
+
const snapshot = payload.snapshot.slice();
|
|
310
|
+
await this.storeMergedSnapshot(db, payload.docId, snapshot);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case "doc-update": {
|
|
314
|
+
const update = payload.update.slice();
|
|
315
|
+
await this.appendDocUpdate(db, payload.docId, update);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
case "asset": {
|
|
319
|
+
const bytes = payload.data.slice();
|
|
320
|
+
await this.putBinary(db, this.assetStore, payload.assetId, bytes);
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
case "meta": {
|
|
324
|
+
const bytes = payload.update.slice();
|
|
325
|
+
await this.putBinary(db, this.metaStore, this.metaKey, bytes);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
default: throw new Error("Unsupported storage payload type");
|
|
329
|
+
}
|
|
481
330
|
}
|
|
482
|
-
async
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
331
|
+
async deleteAsset(assetId) {
|
|
332
|
+
const db = await this.ensureDb();
|
|
333
|
+
await this.deleteKey(db, this.assetStore, assetId);
|
|
334
|
+
}
|
|
335
|
+
async loadDoc(docId) {
|
|
336
|
+
const db = await this.ensureDb();
|
|
337
|
+
const snapshot = await this.getBinaryFromDb(db, this.docStore, docId);
|
|
338
|
+
const pendingUpdates = await this.getDocUpdates(db, docId);
|
|
339
|
+
if (!snapshot && pendingUpdates.length === 0) return;
|
|
340
|
+
let doc;
|
|
341
|
+
try {
|
|
342
|
+
doc = snapshot ? LoroDoc.fromSnapshot(snapshot) : new LoroDoc();
|
|
343
|
+
} catch (error) {
|
|
344
|
+
throw this.createError(`Failed to hydrate document snapshot for "${docId}"`, error);
|
|
488
345
|
}
|
|
489
|
-
|
|
490
|
-
|
|
346
|
+
let appliedUpdates = false;
|
|
347
|
+
for (const update of pendingUpdates) try {
|
|
348
|
+
doc.import(update);
|
|
349
|
+
appliedUpdates = true;
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw this.createError(`Failed to apply queued document update for "${docId}"`, error);
|
|
352
|
+
}
|
|
353
|
+
if (appliedUpdates) {
|
|
354
|
+
let consolidated;
|
|
355
|
+
try {
|
|
356
|
+
consolidated = doc.export({ mode: "snapshot" });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
throw this.createError(`Failed to export consolidated snapshot for "${docId}"`, error);
|
|
359
|
+
}
|
|
360
|
+
await this.writeSnapshot(db, docId, consolidated);
|
|
361
|
+
await this.clearDocUpdates(db, docId);
|
|
362
|
+
}
|
|
363
|
+
return doc;
|
|
491
364
|
}
|
|
492
|
-
|
|
493
|
-
|
|
365
|
+
async loadMeta() {
|
|
366
|
+
const bytes = await this.getBinary(this.metaStore, this.metaKey);
|
|
367
|
+
if (!bytes) return void 0;
|
|
368
|
+
try {
|
|
369
|
+
const json = textDecoder$1.decode(bytes);
|
|
370
|
+
const bundle = JSON.parse(json);
|
|
371
|
+
const flock = new Flock();
|
|
372
|
+
flock.importJson(bundle);
|
|
373
|
+
return flock;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw this.createError("Failed to hydrate metadata snapshot", error);
|
|
376
|
+
}
|
|
494
377
|
}
|
|
495
|
-
async
|
|
496
|
-
|
|
497
|
-
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
498
|
-
await subscription.firstSyncedWithRemote;
|
|
499
|
-
subscription.unsubscribe();
|
|
500
|
-
return { ok: true };
|
|
378
|
+
async loadAsset(assetId) {
|
|
379
|
+
return await this.getBinary(this.assetStore, assetId) ?? void 0;
|
|
501
380
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
resolveFirst: resolve,
|
|
519
|
-
firstSynced: promise
|
|
520
|
-
};
|
|
521
|
-
state.listeners.add(listener);
|
|
522
|
-
postChannelMessage(state.channel, {
|
|
523
|
-
kind: "meta-request",
|
|
524
|
-
from: this.instanceId
|
|
525
|
-
});
|
|
526
|
-
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
527
|
-
postChannelMessage(state.channel, {
|
|
528
|
-
kind: "meta-export",
|
|
529
|
-
from: this.instanceId,
|
|
530
|
-
bundle
|
|
381
|
+
async close() {
|
|
382
|
+
this.closed = true;
|
|
383
|
+
const db = await this.dbPromise;
|
|
384
|
+
if (db) db.close();
|
|
385
|
+
this.dbPromise = void 0;
|
|
386
|
+
}
|
|
387
|
+
async ensureDb() {
|
|
388
|
+
if (this.closed) throw new Error("IndexedDBStorageAdaptor has been closed");
|
|
389
|
+
if (!this.dbPromise) this.dbPromise = new Promise((resolve, reject) => {
|
|
390
|
+
const request = this.idb.open(this.dbName, this.version);
|
|
391
|
+
request.addEventListener("upgradeneeded", () => {
|
|
392
|
+
const db = request.result;
|
|
393
|
+
this.ensureStore(db, this.docStore);
|
|
394
|
+
this.ensureStore(db, this.docUpdateStore);
|
|
395
|
+
this.ensureStore(db, this.metaStore);
|
|
396
|
+
this.ensureStore(db, this.assetStore);
|
|
531
397
|
});
|
|
398
|
+
request.addEventListener("success", () => resolve(request.result), { once: true });
|
|
399
|
+
request.addEventListener("error", () => {
|
|
400
|
+
reject(this.createError(`Failed to open IndexedDB database "${this.dbName}"`, request.error));
|
|
401
|
+
}, { once: true });
|
|
532
402
|
});
|
|
533
|
-
|
|
534
|
-
return {
|
|
535
|
-
unsubscribe: () => {
|
|
536
|
-
listener.unsubscribe();
|
|
537
|
-
state.listeners.delete(listener);
|
|
538
|
-
if (!state.listeners.size) {
|
|
539
|
-
state.channel.removeEventListener("message", state.onMessage);
|
|
540
|
-
state.channel.close();
|
|
541
|
-
this.metaState = void 0;
|
|
542
|
-
}
|
|
543
|
-
},
|
|
544
|
-
firstSyncedWithRemote: listener.firstSynced,
|
|
545
|
-
get connected() {
|
|
546
|
-
return true;
|
|
547
|
-
}
|
|
548
|
-
};
|
|
403
|
+
return this.dbPromise;
|
|
549
404
|
}
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
subscription.unsubscribe();
|
|
555
|
-
return { ok: true };
|
|
405
|
+
ensureStore(db, storeName) {
|
|
406
|
+
const names = db.objectStoreNames;
|
|
407
|
+
if (this.storeExists(names, storeName)) return;
|
|
408
|
+
db.createObjectStore(storeName);
|
|
556
409
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
from: this.instanceId,
|
|
570
|
-
mode: "update",
|
|
571
|
-
payload
|
|
572
|
-
});
|
|
573
|
-
}),
|
|
574
|
-
resolveFirst: resolve,
|
|
575
|
-
firstSynced: promise
|
|
576
|
-
};
|
|
577
|
-
state.listeners.add(listener);
|
|
578
|
-
postChannelMessage(state.channel, {
|
|
579
|
-
kind: "doc-request",
|
|
580
|
-
docId,
|
|
581
|
-
from: this.instanceId
|
|
582
|
-
});
|
|
583
|
-
postChannelMessage(state.channel, {
|
|
584
|
-
kind: "doc-update",
|
|
585
|
-
docId,
|
|
586
|
-
from: this.instanceId,
|
|
587
|
-
mode: "snapshot",
|
|
588
|
-
payload: doc.export({ mode: "snapshot" })
|
|
410
|
+
storeExists(names, storeName) {
|
|
411
|
+
if (typeof names.contains === "function") return names.contains(storeName);
|
|
412
|
+
const length = names.length ?? 0;
|
|
413
|
+
for (let index = 0; index < length; index += 1) if (names.item?.(index) === storeName) return true;
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
async storeMergedSnapshot(db, docId, incoming) {
|
|
417
|
+
await this.runInTransaction(db, this.docStore, "readwrite", async (store) => {
|
|
418
|
+
const existingRaw = await this.wrapRequest(store.get(docId), "read");
|
|
419
|
+
const existing = await this.normalizeBinary(existingRaw);
|
|
420
|
+
const merged = this.mergeSnapshots(docId, existing, incoming);
|
|
421
|
+
await this.wrapRequest(store.put(merged, docId), "write");
|
|
589
422
|
});
|
|
590
|
-
queueMicrotask(() => resolve());
|
|
591
|
-
return {
|
|
592
|
-
unsubscribe: () => {
|
|
593
|
-
listener.unsubscribe();
|
|
594
|
-
state.listeners.delete(listener);
|
|
595
|
-
if (!state.listeners.size) this.teardownDocChannel(docId);
|
|
596
|
-
},
|
|
597
|
-
firstSyncedWithRemote: listener.firstSynced,
|
|
598
|
-
get connected() {
|
|
599
|
-
return true;
|
|
600
|
-
}
|
|
601
|
-
};
|
|
602
423
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
entry.muted = true;
|
|
612
|
-
entry.flock.importJson(message.bundle);
|
|
613
|
-
entry.muted = false;
|
|
614
|
-
entry.resolveFirst();
|
|
615
|
-
}
|
|
616
|
-
else if (message.kind === "meta-request") {
|
|
617
|
-
const first = listeners.values().next().value;
|
|
618
|
-
if (!first) return;
|
|
619
|
-
Promise.resolve(first.flock.exportJson()).then((bundle) => {
|
|
620
|
-
postChannelMessage(channel, {
|
|
621
|
-
kind: "meta-export",
|
|
622
|
-
from: this.instanceId,
|
|
623
|
-
bundle
|
|
624
|
-
});
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
channel.addEventListener("message", onMessage);
|
|
629
|
-
this.metaState = {
|
|
630
|
-
channel,
|
|
631
|
-
listeners,
|
|
632
|
-
onMessage
|
|
633
|
-
};
|
|
634
|
-
return this.metaState;
|
|
424
|
+
mergeSnapshots(docId, existing, incoming) {
|
|
425
|
+
try {
|
|
426
|
+
const doc = existing ? LoroDoc.fromSnapshot(existing) : new LoroDoc();
|
|
427
|
+
doc.import(incoming);
|
|
428
|
+
return doc.export({ mode: "snapshot" });
|
|
429
|
+
} catch (error) {
|
|
430
|
+
throw this.createError(`Failed to merge snapshot for "${docId}"`, error);
|
|
431
|
+
}
|
|
635
432
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
if (!message || message.from === this.instanceId) return;
|
|
644
|
-
if (message.kind === "doc-update") for (const entry of listeners) {
|
|
645
|
-
entry.muted = true;
|
|
646
|
-
entry.doc.import(message.payload);
|
|
647
|
-
entry.muted = false;
|
|
648
|
-
entry.resolveFirst();
|
|
649
|
-
}
|
|
650
|
-
else if (message.kind === "doc-request") {
|
|
651
|
-
const first = listeners.values().next().value;
|
|
652
|
-
if (!first) return;
|
|
653
|
-
const payload = message.docId === docId ? first.doc.export({ mode: "snapshot" }) : void 0;
|
|
654
|
-
if (!payload) return;
|
|
655
|
-
postChannelMessage(channel, {
|
|
656
|
-
kind: "doc-update",
|
|
657
|
-
docId,
|
|
658
|
-
from: this.instanceId,
|
|
659
|
-
mode: "snapshot",
|
|
660
|
-
payload
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
};
|
|
664
|
-
channel.addEventListener("message", onMessage);
|
|
665
|
-
const state = {
|
|
666
|
-
channel,
|
|
667
|
-
listeners,
|
|
668
|
-
onMessage
|
|
669
|
-
};
|
|
670
|
-
this.docStates.set(docId, state);
|
|
671
|
-
return state;
|
|
433
|
+
async appendDocUpdate(db, docId, update) {
|
|
434
|
+
await this.runInTransaction(db, this.docUpdateStore, "readwrite", async (store) => {
|
|
435
|
+
const raw = await this.wrapRequest(store.get(docId), "read");
|
|
436
|
+
const queue = await this.normalizeUpdateQueue(raw);
|
|
437
|
+
queue.push(update.slice());
|
|
438
|
+
await this.wrapRequest(store.put({ updates: queue }, docId), "write");
|
|
439
|
+
});
|
|
672
440
|
}
|
|
673
|
-
|
|
674
|
-
const
|
|
675
|
-
|
|
676
|
-
for (const entry of state.listeners) entry.unsubscribe();
|
|
677
|
-
state.channel.removeEventListener("message", state.onMessage);
|
|
678
|
-
state.channel.close();
|
|
679
|
-
this.docStates.delete(docId);
|
|
441
|
+
async getDocUpdates(db, docId) {
|
|
442
|
+
const raw = await this.runInTransaction(db, this.docUpdateStore, "readonly", (store) => this.wrapRequest(store.get(docId), "read"));
|
|
443
|
+
return this.normalizeUpdateQueue(raw);
|
|
680
444
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
//#endregion
|
|
684
|
-
//#region src/storage/indexeddb.ts
|
|
685
|
-
const DEFAULT_DB_NAME = "loro-repo";
|
|
686
|
-
const DEFAULT_DB_VERSION = 1;
|
|
687
|
-
const DEFAULT_DOC_STORE = "docs";
|
|
688
|
-
const DEFAULT_META_STORE = "meta";
|
|
689
|
-
const DEFAULT_ASSET_STORE = "assets";
|
|
690
|
-
const DEFAULT_DOC_UPDATE_STORE = "doc-updates";
|
|
691
|
-
const DEFAULT_META_KEY = "snapshot";
|
|
692
|
-
const textDecoder$1 = new TextDecoder();
|
|
693
|
-
function describeUnknown(cause) {
|
|
694
|
-
if (typeof cause === "string") return cause;
|
|
695
|
-
if (typeof cause === "number" || typeof cause === "boolean") return String(cause);
|
|
696
|
-
if (typeof cause === "bigint") return cause.toString();
|
|
697
|
-
if (typeof cause === "symbol") return cause.description ?? cause.toString();
|
|
698
|
-
if (typeof cause === "function") return `[function ${cause.name ?? "anonymous"}]`;
|
|
699
|
-
if (cause && typeof cause === "object") try {
|
|
700
|
-
return JSON.stringify(cause);
|
|
701
|
-
} catch {
|
|
702
|
-
return "[object]";
|
|
445
|
+
async clearDocUpdates(db, docId) {
|
|
446
|
+
await this.runInTransaction(db, this.docUpdateStore, "readwrite", (store) => this.wrapRequest(store.delete(docId), "delete"));
|
|
703
447
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
var IndexedDBStorageAdaptor = class {
|
|
707
|
-
idb;
|
|
708
|
-
dbName;
|
|
709
|
-
version;
|
|
710
|
-
docStore;
|
|
711
|
-
docUpdateStore;
|
|
712
|
-
metaStore;
|
|
713
|
-
assetStore;
|
|
714
|
-
metaKey;
|
|
715
|
-
dbPromise;
|
|
716
|
-
closed = false;
|
|
717
|
-
constructor(options = {}) {
|
|
718
|
-
const idbFactory = globalThis.indexedDB;
|
|
719
|
-
if (!idbFactory) throw new Error("IndexedDB is not available in this environment");
|
|
720
|
-
this.idb = idbFactory;
|
|
721
|
-
this.dbName = options.dbName ?? DEFAULT_DB_NAME;
|
|
722
|
-
this.version = options.version ?? DEFAULT_DB_VERSION;
|
|
723
|
-
this.docStore = options.docStoreName ?? DEFAULT_DOC_STORE;
|
|
724
|
-
this.docUpdateStore = options.docUpdateStoreName ?? DEFAULT_DOC_UPDATE_STORE;
|
|
725
|
-
this.metaStore = options.metaStoreName ?? DEFAULT_META_STORE;
|
|
726
|
-
this.assetStore = options.assetStoreName ?? DEFAULT_ASSET_STORE;
|
|
727
|
-
this.metaKey = options.metaKey ?? DEFAULT_META_KEY;
|
|
728
|
-
}
|
|
729
|
-
async save(payload) {
|
|
730
|
-
const db = await this.ensureDb();
|
|
731
|
-
switch (payload.type) {
|
|
732
|
-
case "doc-snapshot": {
|
|
733
|
-
const snapshot = payload.snapshot.slice();
|
|
734
|
-
await this.storeMergedSnapshot(db, payload.docId, snapshot);
|
|
735
|
-
break;
|
|
736
|
-
}
|
|
737
|
-
case "doc-update": {
|
|
738
|
-
const update = payload.update.slice();
|
|
739
|
-
await this.appendDocUpdate(db, payload.docId, update);
|
|
740
|
-
break;
|
|
741
|
-
}
|
|
742
|
-
case "asset": {
|
|
743
|
-
const bytes = payload.data.slice();
|
|
744
|
-
await this.putBinary(db, this.assetStore, payload.assetId, bytes);
|
|
745
|
-
break;
|
|
746
|
-
}
|
|
747
|
-
case "meta": {
|
|
748
|
-
const bytes = payload.update.slice();
|
|
749
|
-
await this.putBinary(db, this.metaStore, this.metaKey, bytes);
|
|
750
|
-
break;
|
|
751
|
-
}
|
|
752
|
-
default: throw new Error("Unsupported storage payload type");
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
async deleteAsset(assetId) {
|
|
756
|
-
const db = await this.ensureDb();
|
|
757
|
-
await this.deleteKey(db, this.assetStore, assetId);
|
|
758
|
-
}
|
|
759
|
-
async loadDoc(docId) {
|
|
760
|
-
const db = await this.ensureDb();
|
|
761
|
-
const snapshot = await this.getBinaryFromDb(db, this.docStore, docId);
|
|
762
|
-
const pendingUpdates = await this.getDocUpdates(db, docId);
|
|
763
|
-
if (!snapshot && pendingUpdates.length === 0) return;
|
|
764
|
-
let doc;
|
|
765
|
-
try {
|
|
766
|
-
doc = snapshot ? LoroDoc.fromSnapshot(snapshot) : new LoroDoc();
|
|
767
|
-
} catch (error) {
|
|
768
|
-
throw this.createError(`Failed to hydrate document snapshot for "${docId}"`, error);
|
|
769
|
-
}
|
|
770
|
-
let appliedUpdates = false;
|
|
771
|
-
for (const update of pendingUpdates) try {
|
|
772
|
-
doc.import(update);
|
|
773
|
-
appliedUpdates = true;
|
|
774
|
-
} catch (error) {
|
|
775
|
-
throw this.createError(`Failed to apply queued document update for "${docId}"`, error);
|
|
776
|
-
}
|
|
777
|
-
if (appliedUpdates) {
|
|
778
|
-
let consolidated;
|
|
779
|
-
try {
|
|
780
|
-
consolidated = doc.export({ mode: "snapshot" });
|
|
781
|
-
} catch (error) {
|
|
782
|
-
throw this.createError(`Failed to export consolidated snapshot for "${docId}"`, error);
|
|
783
|
-
}
|
|
784
|
-
await this.writeSnapshot(db, docId, consolidated);
|
|
785
|
-
await this.clearDocUpdates(db, docId);
|
|
786
|
-
}
|
|
787
|
-
return doc;
|
|
788
|
-
}
|
|
789
|
-
async loadMeta() {
|
|
790
|
-
const bytes = await this.getBinary(this.metaStore, this.metaKey);
|
|
791
|
-
if (!bytes) return void 0;
|
|
792
|
-
try {
|
|
793
|
-
const json = textDecoder$1.decode(bytes);
|
|
794
|
-
const bundle = JSON.parse(json);
|
|
795
|
-
const flock = new Flock();
|
|
796
|
-
flock.importJson(bundle);
|
|
797
|
-
return flock;
|
|
798
|
-
} catch (error) {
|
|
799
|
-
throw this.createError("Failed to hydrate metadata snapshot", error);
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
async loadAsset(assetId) {
|
|
803
|
-
return await this.getBinary(this.assetStore, assetId) ?? void 0;
|
|
804
|
-
}
|
|
805
|
-
async close() {
|
|
806
|
-
this.closed = true;
|
|
807
|
-
const db = await this.dbPromise;
|
|
808
|
-
if (db) db.close();
|
|
809
|
-
this.dbPromise = void 0;
|
|
810
|
-
}
|
|
811
|
-
async ensureDb() {
|
|
812
|
-
if (this.closed) throw new Error("IndexedDBStorageAdaptor has been closed");
|
|
813
|
-
if (!this.dbPromise) this.dbPromise = new Promise((resolve, reject) => {
|
|
814
|
-
const request = this.idb.open(this.dbName, this.version);
|
|
815
|
-
request.addEventListener("upgradeneeded", () => {
|
|
816
|
-
const db = request.result;
|
|
817
|
-
this.ensureStore(db, this.docStore);
|
|
818
|
-
this.ensureStore(db, this.docUpdateStore);
|
|
819
|
-
this.ensureStore(db, this.metaStore);
|
|
820
|
-
this.ensureStore(db, this.assetStore);
|
|
821
|
-
});
|
|
822
|
-
request.addEventListener("success", () => resolve(request.result), { once: true });
|
|
823
|
-
request.addEventListener("error", () => {
|
|
824
|
-
reject(this.createError(`Failed to open IndexedDB database "${this.dbName}"`, request.error));
|
|
825
|
-
}, { once: true });
|
|
826
|
-
});
|
|
827
|
-
return this.dbPromise;
|
|
828
|
-
}
|
|
829
|
-
ensureStore(db, storeName) {
|
|
830
|
-
const names = db.objectStoreNames;
|
|
831
|
-
if (this.storeExists(names, storeName)) return;
|
|
832
|
-
db.createObjectStore(storeName);
|
|
833
|
-
}
|
|
834
|
-
storeExists(names, storeName) {
|
|
835
|
-
if (typeof names.contains === "function") return names.contains(storeName);
|
|
836
|
-
const length = names.length ?? 0;
|
|
837
|
-
for (let index = 0; index < length; index += 1) if (names.item?.(index) === storeName) return true;
|
|
838
|
-
return false;
|
|
839
|
-
}
|
|
840
|
-
async storeMergedSnapshot(db, docId, incoming) {
|
|
841
|
-
await this.runInTransaction(db, this.docStore, "readwrite", async (store) => {
|
|
842
|
-
const existingRaw = await this.wrapRequest(store.get(docId), "read");
|
|
843
|
-
const existing = await this.normalizeBinary(existingRaw);
|
|
844
|
-
const merged = this.mergeSnapshots(docId, existing, incoming);
|
|
845
|
-
await this.wrapRequest(store.put(merged, docId), "write");
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
mergeSnapshots(docId, existing, incoming) {
|
|
849
|
-
try {
|
|
850
|
-
const doc = existing ? LoroDoc.fromSnapshot(existing) : new LoroDoc();
|
|
851
|
-
doc.import(incoming);
|
|
852
|
-
return doc.export({ mode: "snapshot" });
|
|
853
|
-
} catch (error) {
|
|
854
|
-
throw this.createError(`Failed to merge snapshot for "${docId}"`, error);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
async appendDocUpdate(db, docId, update) {
|
|
858
|
-
await this.runInTransaction(db, this.docUpdateStore, "readwrite", async (store) => {
|
|
859
|
-
const raw = await this.wrapRequest(store.get(docId), "read");
|
|
860
|
-
const queue = await this.normalizeUpdateQueue(raw);
|
|
861
|
-
queue.push(update.slice());
|
|
862
|
-
await this.wrapRequest(store.put({ updates: queue }, docId), "write");
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
async getDocUpdates(db, docId) {
|
|
866
|
-
const raw = await this.runInTransaction(db, this.docUpdateStore, "readonly", (store) => this.wrapRequest(store.get(docId), "read"));
|
|
867
|
-
return this.normalizeUpdateQueue(raw);
|
|
868
|
-
}
|
|
869
|
-
async clearDocUpdates(db, docId) {
|
|
870
|
-
await this.runInTransaction(db, this.docUpdateStore, "readwrite", (store) => this.wrapRequest(store.delete(docId), "delete"));
|
|
871
|
-
}
|
|
872
|
-
async writeSnapshot(db, docId, snapshot) {
|
|
873
|
-
await this.putBinary(db, this.docStore, docId, snapshot.slice());
|
|
448
|
+
async writeSnapshot(db, docId, snapshot) {
|
|
449
|
+
await this.putBinary(db, this.docStore, docId, snapshot.slice());
|
|
874
450
|
}
|
|
875
451
|
async getBinaryFromDb(db, storeName, key) {
|
|
876
452
|
const value = await this.runInTransaction(db, storeName, "readonly", (store) => this.wrapRequest(store.get(key), "read"));
|
|
@@ -1066,22 +642,445 @@ async function removeIfExists(filePath) {
|
|
|
1066
642
|
if (error.code === "ENOENT") return;
|
|
1067
643
|
throw error;
|
|
1068
644
|
}
|
|
1069
|
-
}
|
|
1070
|
-
async function listFiles(dir) {
|
|
1071
|
-
try {
|
|
1072
|
-
return (await promises.readdir(dir)).sort();
|
|
1073
|
-
} catch (error) {
|
|
1074
|
-
if (error.code === "ENOENT") return [];
|
|
1075
|
-
throw error;
|
|
645
|
+
}
|
|
646
|
+
async function listFiles(dir) {
|
|
647
|
+
try {
|
|
648
|
+
return (await promises.readdir(dir)).sort();
|
|
649
|
+
} catch (error) {
|
|
650
|
+
if (error.code === "ENOENT") return [];
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function writeFileAtomic(targetPath, data) {
|
|
655
|
+
const dir = path.dirname(targetPath);
|
|
656
|
+
await ensureDir(dir);
|
|
657
|
+
const tempPath = path.join(dir, `.tmp-${randomUUID()}`);
|
|
658
|
+
await promises.writeFile(tempPath, data);
|
|
659
|
+
await promises.rename(tempPath, targetPath);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/internal/debug.ts
|
|
664
|
+
const getEnv = () => {
|
|
665
|
+
if (typeof globalThis !== "object" || globalThis === null) return;
|
|
666
|
+
return globalThis.process?.env;
|
|
667
|
+
};
|
|
668
|
+
const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
|
|
669
|
+
const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
|
|
670
|
+
const wildcardTokens = new Set([
|
|
671
|
+
"*",
|
|
672
|
+
"1",
|
|
673
|
+
"true",
|
|
674
|
+
"all"
|
|
675
|
+
]);
|
|
676
|
+
const namespaceSet = new Set(normalizedNamespaces);
|
|
677
|
+
const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
|
|
678
|
+
const isDebugEnabled = (namespace) => {
|
|
679
|
+
if (!namespaceSet.size) return false;
|
|
680
|
+
if (!namespace) return hasWildcard;
|
|
681
|
+
const normalized = namespace.toLowerCase();
|
|
682
|
+
if (hasWildcard) return true;
|
|
683
|
+
if (namespaceSet.has(normalized)) return true;
|
|
684
|
+
const [root] = normalized.split(":");
|
|
685
|
+
return namespaceSet.has(root);
|
|
686
|
+
};
|
|
687
|
+
const createDebugLogger = (namespace) => {
|
|
688
|
+
const normalized = namespace.toLowerCase();
|
|
689
|
+
return (...args) => {
|
|
690
|
+
if (!isDebugEnabled(normalized)) return;
|
|
691
|
+
const prefix = `[loro-repo:${namespace}]`;
|
|
692
|
+
if (args.length === 0) {
|
|
693
|
+
console.info(prefix);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
console.info(prefix, ...args);
|
|
697
|
+
};
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
//#endregion
|
|
701
|
+
//#region src/transport/websocket.ts
|
|
702
|
+
const debug = createDebugLogger("transport:websocket");
|
|
703
|
+
function withTimeout(promise, timeoutMs) {
|
|
704
|
+
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
705
|
+
return new Promise((resolve, reject) => {
|
|
706
|
+
const timer = setTimeout(() => {
|
|
707
|
+
reject(/* @__PURE__ */ new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
708
|
+
}, timeoutMs);
|
|
709
|
+
promise.then((value) => {
|
|
710
|
+
clearTimeout(timer);
|
|
711
|
+
resolve(value);
|
|
712
|
+
}).catch((error) => {
|
|
713
|
+
clearTimeout(timer);
|
|
714
|
+
reject(error);
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
function normalizeRoomId(roomId, fallback) {
|
|
719
|
+
if (typeof roomId === "string" && roomId.length > 0) return roomId;
|
|
720
|
+
if (roomId instanceof Uint8Array && roomId.length > 0) try {
|
|
721
|
+
return bytesToHex(roomId);
|
|
722
|
+
} catch {
|
|
723
|
+
return fallback;
|
|
724
|
+
}
|
|
725
|
+
return fallback;
|
|
726
|
+
}
|
|
727
|
+
function bytesEqual(a, b) {
|
|
728
|
+
if (a === b) return true;
|
|
729
|
+
if (!a || !b) return false;
|
|
730
|
+
if (a.length !== b.length) return false;
|
|
731
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* loro-websocket backed {@link TransportAdapter} implementation for LoroRepo.
|
|
736
|
+
* It uses loro-protocol as the underlying protocol for the transport.
|
|
737
|
+
*/
|
|
738
|
+
var WebSocketTransportAdapter = class {
|
|
739
|
+
options;
|
|
740
|
+
client;
|
|
741
|
+
metadataSession;
|
|
742
|
+
docSessions = /* @__PURE__ */ new Map();
|
|
743
|
+
constructor(options) {
|
|
744
|
+
this.options = options;
|
|
745
|
+
}
|
|
746
|
+
async connect(_options) {
|
|
747
|
+
const client = this.ensureClient();
|
|
748
|
+
debug("connect requested", { status: client.getStatus() });
|
|
749
|
+
try {
|
|
750
|
+
await client.connect();
|
|
751
|
+
debug("client.connect resolved");
|
|
752
|
+
await client.waitConnected();
|
|
753
|
+
debug("client.waitConnected resolved", { status: client.getStatus() });
|
|
754
|
+
} catch (error) {
|
|
755
|
+
debug("connect failed", error);
|
|
756
|
+
throw error;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async close() {
|
|
760
|
+
debug("close requested", {
|
|
761
|
+
docSessions: this.docSessions.size,
|
|
762
|
+
metadataSession: Boolean(this.metadataSession)
|
|
763
|
+
});
|
|
764
|
+
for (const [docId] of this.docSessions) await this.leaveDocSession(docId).catch(() => {});
|
|
765
|
+
this.docSessions.clear();
|
|
766
|
+
await this.teardownMetadataSession().catch(() => {});
|
|
767
|
+
if (this.client) {
|
|
768
|
+
const client = this.client;
|
|
769
|
+
this.client = void 0;
|
|
770
|
+
client.destroy();
|
|
771
|
+
debug("websocket client destroyed");
|
|
772
|
+
}
|
|
773
|
+
debug("close completed");
|
|
774
|
+
}
|
|
775
|
+
isConnected() {
|
|
776
|
+
return this.client?.getStatus() === "connected";
|
|
777
|
+
}
|
|
778
|
+
async syncMeta(flock, options) {
|
|
779
|
+
debug("syncMeta requested", { roomId: this.options.metadataRoomId });
|
|
780
|
+
try {
|
|
781
|
+
let auth;
|
|
782
|
+
if (this.options.metadataAuth) if (typeof this.options.metadataAuth === "function") auth = await this.options.metadataAuth();
|
|
783
|
+
else auth = this.options.metadataAuth;
|
|
784
|
+
await withTimeout((await this.ensureMetadataSession(flock, {
|
|
785
|
+
roomId: this.options.metadataRoomId ?? "repo:meta",
|
|
786
|
+
auth
|
|
787
|
+
})).firstSynced, options?.timeout);
|
|
788
|
+
debug("syncMeta completed", { roomId: this.options.metadataRoomId });
|
|
789
|
+
return { ok: true };
|
|
790
|
+
} catch (error) {
|
|
791
|
+
debug("syncMeta failed", error);
|
|
792
|
+
return { ok: false };
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
joinMetaRoom(flock, params) {
|
|
796
|
+
const fallback = this.options.metadataRoomId ?? "";
|
|
797
|
+
const roomId = normalizeRoomId(params?.roomId, fallback);
|
|
798
|
+
if (!roomId) throw new Error("Metadata room id not configured");
|
|
799
|
+
const session = (async () => {
|
|
800
|
+
let auth;
|
|
801
|
+
const authWay = params?.auth ?? this.options.metadataAuth;
|
|
802
|
+
if (typeof authWay === "function") auth = await authWay();
|
|
803
|
+
else auth = authWay;
|
|
804
|
+
debug("joinMetaRoom requested", {
|
|
805
|
+
roomId,
|
|
806
|
+
hasAuth: Boolean(auth && auth.length)
|
|
807
|
+
});
|
|
808
|
+
return this.ensureMetadataSession(flock, {
|
|
809
|
+
roomId,
|
|
810
|
+
auth
|
|
811
|
+
});
|
|
812
|
+
})();
|
|
813
|
+
const firstSynced = session.then((session$1) => session$1.firstSynced);
|
|
814
|
+
const getConnected = () => this.isConnected();
|
|
815
|
+
const subscription = {
|
|
816
|
+
unsubscribe: () => {
|
|
817
|
+
session.then((session$1) => {
|
|
818
|
+
session$1.refCount = Math.max(0, session$1.refCount - 1);
|
|
819
|
+
debug("metadata session refCount decremented", {
|
|
820
|
+
roomId: session$1.roomId,
|
|
821
|
+
refCount: session$1.refCount
|
|
822
|
+
});
|
|
823
|
+
if (session$1.refCount === 0) {
|
|
824
|
+
debug("tearing down metadata session due to refCount=0", { roomId: session$1.roomId });
|
|
825
|
+
this.teardownMetadataSession(session$1).catch(() => {});
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
},
|
|
829
|
+
firstSyncedWithRemote: firstSynced,
|
|
830
|
+
get connected() {
|
|
831
|
+
return getConnected();
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
session.then((session$1) => {
|
|
835
|
+
session$1.refCount += 1;
|
|
836
|
+
debug("metadata session refCount incremented", {
|
|
837
|
+
roomId: session$1.roomId,
|
|
838
|
+
refCount: session$1.refCount
|
|
839
|
+
});
|
|
840
|
+
});
|
|
841
|
+
return subscription;
|
|
842
|
+
}
|
|
843
|
+
async syncDoc(docId, doc, options) {
|
|
844
|
+
debug("syncDoc requested", { docId });
|
|
845
|
+
try {
|
|
846
|
+
const session = await this.ensureDocSession(docId, doc, {});
|
|
847
|
+
await withTimeout(session.firstSynced, options?.timeout);
|
|
848
|
+
debug("syncDoc completed", {
|
|
849
|
+
docId,
|
|
850
|
+
roomId: session.roomId
|
|
851
|
+
});
|
|
852
|
+
return { ok: true };
|
|
853
|
+
} catch (error) {
|
|
854
|
+
debug("syncDoc failed", {
|
|
855
|
+
docId,
|
|
856
|
+
error
|
|
857
|
+
});
|
|
858
|
+
return { ok: false };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
joinDocRoom(docId, doc, params) {
|
|
862
|
+
debug("joinDocRoom requested", {
|
|
863
|
+
docId,
|
|
864
|
+
roomParamType: params?.roomId ? typeof params.roomId === "string" ? "string" : "uint8array" : void 0,
|
|
865
|
+
hasAuthOverride: Boolean(params?.auth && params.auth.length)
|
|
866
|
+
});
|
|
867
|
+
const ensure = this.ensureDocSession(docId, doc, params ?? {});
|
|
868
|
+
const firstSynced = ensure.then((session) => session.firstSynced);
|
|
869
|
+
const getConnected = () => this.isConnected();
|
|
870
|
+
const subscription = {
|
|
871
|
+
unsubscribe: () => {
|
|
872
|
+
ensure.then((session) => {
|
|
873
|
+
session.refCount = Math.max(0, session.refCount - 1);
|
|
874
|
+
debug("doc session refCount decremented", {
|
|
875
|
+
docId,
|
|
876
|
+
roomId: session.roomId,
|
|
877
|
+
refCount: session.refCount
|
|
878
|
+
});
|
|
879
|
+
if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
|
|
880
|
+
});
|
|
881
|
+
},
|
|
882
|
+
firstSyncedWithRemote: firstSynced,
|
|
883
|
+
get connected() {
|
|
884
|
+
return getConnected();
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
ensure.then((session) => {
|
|
888
|
+
session.refCount += 1;
|
|
889
|
+
debug("doc session refCount incremented", {
|
|
890
|
+
docId,
|
|
891
|
+
roomId: session.roomId,
|
|
892
|
+
refCount: session.refCount
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
return subscription;
|
|
896
|
+
}
|
|
897
|
+
ensureClient() {
|
|
898
|
+
if (this.client) {
|
|
899
|
+
debug("reusing websocket client", { status: this.client.getStatus() });
|
|
900
|
+
return this.client;
|
|
901
|
+
}
|
|
902
|
+
const { url, client: clientOptions } = this.options;
|
|
903
|
+
debug("creating websocket client", {
|
|
904
|
+
url,
|
|
905
|
+
clientOptionsKeys: clientOptions ? Object.keys(clientOptions) : []
|
|
906
|
+
});
|
|
907
|
+
const client = new LoroWebsocketClient({
|
|
908
|
+
url,
|
|
909
|
+
...clientOptions
|
|
910
|
+
});
|
|
911
|
+
this.client = client;
|
|
912
|
+
return client;
|
|
913
|
+
}
|
|
914
|
+
async ensureMetadataSession(flock, params) {
|
|
915
|
+
debug("ensureMetadataSession invoked", {
|
|
916
|
+
roomId: params.roomId,
|
|
917
|
+
hasAuth: Boolean(params.auth && params.auth.length)
|
|
918
|
+
});
|
|
919
|
+
const client = this.ensureClient();
|
|
920
|
+
await client.waitConnected();
|
|
921
|
+
debug("websocket client ready for metadata session", { status: client.getStatus() });
|
|
922
|
+
if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) {
|
|
923
|
+
debug("reusing metadata session", {
|
|
924
|
+
roomId: this.metadataSession.roomId,
|
|
925
|
+
refCount: this.metadataSession.refCount
|
|
926
|
+
});
|
|
927
|
+
return this.metadataSession;
|
|
928
|
+
}
|
|
929
|
+
if (this.metadataSession) {
|
|
930
|
+
debug("tearing down previous metadata session", { roomId: this.metadataSession.roomId });
|
|
931
|
+
await this.teardownMetadataSession(this.metadataSession).catch(() => {});
|
|
932
|
+
}
|
|
933
|
+
const adaptor = new FlockAdaptor(flock, this.options.metadataAdaptorConfig);
|
|
934
|
+
debug("joining metadata room", {
|
|
935
|
+
roomId: params.roomId,
|
|
936
|
+
hasAuth: Boolean(params.auth && params.auth.length)
|
|
937
|
+
});
|
|
938
|
+
const room = await client.join({
|
|
939
|
+
roomId: params.roomId,
|
|
940
|
+
crdtAdaptor: adaptor,
|
|
941
|
+
auth: params.auth
|
|
942
|
+
});
|
|
943
|
+
const firstSynced = room.waitForReachingServerVersion();
|
|
944
|
+
firstSynced.then(() => {
|
|
945
|
+
debug("metadata session firstSynced resolved", { roomId: params.roomId });
|
|
946
|
+
}, (error) => {
|
|
947
|
+
debug("metadata session firstSynced rejected", {
|
|
948
|
+
roomId: params.roomId,
|
|
949
|
+
error
|
|
950
|
+
});
|
|
951
|
+
});
|
|
952
|
+
const session = {
|
|
953
|
+
adaptor,
|
|
954
|
+
room,
|
|
955
|
+
firstSynced,
|
|
956
|
+
flock,
|
|
957
|
+
roomId: params.roomId,
|
|
958
|
+
auth: params.auth,
|
|
959
|
+
refCount: 0
|
|
960
|
+
};
|
|
961
|
+
this.metadataSession = session;
|
|
962
|
+
return session;
|
|
963
|
+
}
|
|
964
|
+
async teardownMetadataSession(session) {
|
|
965
|
+
const target = session ?? this.metadataSession;
|
|
966
|
+
if (!target) return;
|
|
967
|
+
debug("teardownMetadataSession invoked", { roomId: target.roomId });
|
|
968
|
+
if (this.metadataSession === target) this.metadataSession = void 0;
|
|
969
|
+
const { adaptor, room } = target;
|
|
970
|
+
try {
|
|
971
|
+
await room.leave();
|
|
972
|
+
debug("metadata room left", { roomId: target.roomId });
|
|
973
|
+
} catch (error) {
|
|
974
|
+
debug("metadata room leave failed; destroying", {
|
|
975
|
+
roomId: target.roomId,
|
|
976
|
+
error
|
|
977
|
+
});
|
|
978
|
+
await room.destroy().catch(() => {});
|
|
979
|
+
}
|
|
980
|
+
adaptor.destroy();
|
|
981
|
+
debug("metadata session destroyed", { roomId: target.roomId });
|
|
982
|
+
}
|
|
983
|
+
async ensureDocSession(docId, doc, params) {
|
|
984
|
+
debug("ensureDocSession invoked", { docId });
|
|
985
|
+
const client = this.ensureClient();
|
|
986
|
+
await client.waitConnected();
|
|
987
|
+
debug("websocket client ready for doc session", {
|
|
988
|
+
docId,
|
|
989
|
+
status: client.getStatus()
|
|
990
|
+
});
|
|
991
|
+
const existing = this.docSessions.get(docId);
|
|
992
|
+
const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
|
|
993
|
+
const roomId = normalizeRoomId(params.roomId, derivedRoomId);
|
|
994
|
+
let auth;
|
|
995
|
+
auth = await (params.auth ?? this.options.docAuth?.(docId));
|
|
996
|
+
debug("doc session params resolved", {
|
|
997
|
+
docId,
|
|
998
|
+
roomId,
|
|
999
|
+
hasAuth: Boolean(auth && auth.length)
|
|
1000
|
+
});
|
|
1001
|
+
if (existing && existing.doc === doc && existing.roomId === roomId) {
|
|
1002
|
+
debug("reusing doc session", {
|
|
1003
|
+
docId,
|
|
1004
|
+
roomId,
|
|
1005
|
+
refCount: existing.refCount
|
|
1006
|
+
});
|
|
1007
|
+
return existing;
|
|
1008
|
+
}
|
|
1009
|
+
if (existing) {
|
|
1010
|
+
debug("doc session mismatch; leaving existing session", {
|
|
1011
|
+
docId,
|
|
1012
|
+
previousRoomId: existing.roomId,
|
|
1013
|
+
nextRoomId: roomId
|
|
1014
|
+
});
|
|
1015
|
+
await this.leaveDocSession(docId).catch(() => {});
|
|
1016
|
+
}
|
|
1017
|
+
const adaptor = new LoroAdaptor(doc);
|
|
1018
|
+
debug("joining doc room", {
|
|
1019
|
+
docId,
|
|
1020
|
+
roomId,
|
|
1021
|
+
hasAuth: Boolean(auth && auth.length)
|
|
1022
|
+
});
|
|
1023
|
+
const room = await client.join({
|
|
1024
|
+
roomId,
|
|
1025
|
+
crdtAdaptor: adaptor,
|
|
1026
|
+
auth
|
|
1027
|
+
});
|
|
1028
|
+
const firstSynced = room.waitForReachingServerVersion();
|
|
1029
|
+
firstSynced.then(() => {
|
|
1030
|
+
debug("doc session firstSynced resolved", {
|
|
1031
|
+
docId,
|
|
1032
|
+
roomId
|
|
1033
|
+
});
|
|
1034
|
+
}, (error) => {
|
|
1035
|
+
debug("doc session firstSynced rejected", {
|
|
1036
|
+
docId,
|
|
1037
|
+
roomId,
|
|
1038
|
+
error
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
const session = {
|
|
1042
|
+
adaptor,
|
|
1043
|
+
room,
|
|
1044
|
+
firstSynced,
|
|
1045
|
+
doc,
|
|
1046
|
+
roomId,
|
|
1047
|
+
refCount: 0
|
|
1048
|
+
};
|
|
1049
|
+
this.docSessions.set(docId, session);
|
|
1050
|
+
return session;
|
|
1051
|
+
}
|
|
1052
|
+
async leaveDocSession(docId) {
|
|
1053
|
+
const session = this.docSessions.get(docId);
|
|
1054
|
+
if (!session) {
|
|
1055
|
+
debug("leaveDocSession invoked but no session found", { docId });
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
this.docSessions.delete(docId);
|
|
1059
|
+
debug("leaving doc session", {
|
|
1060
|
+
docId,
|
|
1061
|
+
roomId: session.roomId
|
|
1062
|
+
});
|
|
1063
|
+
try {
|
|
1064
|
+
await session.room.leave();
|
|
1065
|
+
debug("doc room left", {
|
|
1066
|
+
docId,
|
|
1067
|
+
roomId: session.roomId
|
|
1068
|
+
});
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
debug("doc room leave failed; destroying", {
|
|
1071
|
+
docId,
|
|
1072
|
+
roomId: session.roomId,
|
|
1073
|
+
error
|
|
1074
|
+
});
|
|
1075
|
+
await session.room.destroy().catch(() => {});
|
|
1076
|
+
}
|
|
1077
|
+
session.adaptor.destroy();
|
|
1078
|
+
debug("doc session destroyed", {
|
|
1079
|
+
docId,
|
|
1080
|
+
roomId: session.roomId
|
|
1081
|
+
});
|
|
1076
1082
|
}
|
|
1077
|
-
}
|
|
1078
|
-
async function writeFileAtomic(targetPath, data) {
|
|
1079
|
-
const dir = path.dirname(targetPath);
|
|
1080
|
-
await ensureDir(dir);
|
|
1081
|
-
const tempPath = path.join(dir, `.tmp-${randomUUID()}`);
|
|
1082
|
-
await promises.writeFile(tempPath, data);
|
|
1083
|
-
await promises.rename(tempPath, targetPath);
|
|
1084
|
-
}
|
|
1083
|
+
};
|
|
1085
1084
|
|
|
1086
1085
|
//#endregion
|
|
1087
1086
|
//#region src/internal/event-bus.ts
|
|
@@ -1102,223 +1101,35 @@ var RepoEventBus = class {
|
|
|
1102
1101
|
for (const entry of this.watchers) if (this.shouldNotify(entry.filter, event)) entry.listener(event);
|
|
1103
1102
|
}
|
|
1104
1103
|
clear() {
|
|
1105
|
-
this.watchers.clear();
|
|
1106
|
-
this.eventByStack.length = 0;
|
|
1107
|
-
}
|
|
1108
|
-
pushEventBy(by) {
|
|
1109
|
-
this.eventByStack.push(by);
|
|
1110
|
-
}
|
|
1111
|
-
popEventBy() {
|
|
1112
|
-
this.eventByStack.pop();
|
|
1113
|
-
}
|
|
1114
|
-
resolveEventBy(defaultBy) {
|
|
1115
|
-
const index = this.eventByStack.length - 1;
|
|
1116
|
-
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
1117
|
-
}
|
|
1118
|
-
shouldNotify(filter, event) {
|
|
1119
|
-
if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
|
|
1120
|
-
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
1121
|
-
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
1122
|
-
const docId = (() => {
|
|
1123
|
-
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
1124
|
-
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
1125
|
-
})();
|
|
1126
|
-
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
1127
|
-
if (filter.docIds && !docId) return false;
|
|
1128
|
-
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
1129
|
-
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
1130
|
-
}
|
|
1131
|
-
return true;
|
|
1132
|
-
}
|
|
1133
|
-
};
|
|
1134
|
-
|
|
1135
|
-
//#endregion
|
|
1136
|
-
//#region src/utils.ts
|
|
1137
|
-
async function streamToUint8Array(stream) {
|
|
1138
|
-
const reader = stream.getReader();
|
|
1139
|
-
const chunks = [];
|
|
1140
|
-
let total = 0;
|
|
1141
|
-
while (true) {
|
|
1142
|
-
const { done, value } = await reader.read();
|
|
1143
|
-
if (done) break;
|
|
1144
|
-
if (value) {
|
|
1145
|
-
chunks.push(value);
|
|
1146
|
-
total += value.byteLength;
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
const buffer = new Uint8Array(total);
|
|
1150
|
-
let offset = 0;
|
|
1151
|
-
for (const chunk of chunks) {
|
|
1152
|
-
buffer.set(chunk, offset);
|
|
1153
|
-
offset += chunk.byteLength;
|
|
1154
|
-
}
|
|
1155
|
-
return buffer;
|
|
1156
|
-
}
|
|
1157
|
-
async function assetContentToUint8Array(content) {
|
|
1158
|
-
if (content instanceof Uint8Array) return content;
|
|
1159
|
-
if (ArrayBuffer.isView(content)) return new Uint8Array(content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
|
|
1160
|
-
if (typeof Blob !== "undefined" && content instanceof Blob) return new Uint8Array(await content.arrayBuffer());
|
|
1161
|
-
if (typeof ReadableStream !== "undefined" && content instanceof ReadableStream) return streamToUint8Array(content);
|
|
1162
|
-
throw new TypeError("Unsupported asset content type");
|
|
1163
|
-
}
|
|
1164
|
-
function bytesToHex$1(bytes) {
|
|
1165
|
-
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1166
|
-
}
|
|
1167
|
-
async function computeSha256(bytes) {
|
|
1168
|
-
const globalCrypto = globalThis.crypto;
|
|
1169
|
-
if (globalCrypto?.subtle && typeof globalCrypto.subtle.digest === "function") {
|
|
1170
|
-
const digest = await globalCrypto.subtle.digest("SHA-256", bytes);
|
|
1171
|
-
return bytesToHex$1(new Uint8Array(digest));
|
|
1172
|
-
}
|
|
1173
|
-
try {
|
|
1174
|
-
const { createHash } = await import("node:crypto");
|
|
1175
|
-
const hash = createHash("sha256");
|
|
1176
|
-
hash.update(bytes);
|
|
1177
|
-
return hash.digest("hex");
|
|
1178
|
-
} catch {
|
|
1179
|
-
throw new Error("SHA-256 digest is not available in this environment");
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
function cloneJsonValue(value) {
|
|
1183
|
-
if (value === null) return null;
|
|
1184
|
-
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
1185
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
1186
|
-
if (Array.isArray(value)) {
|
|
1187
|
-
const arr = [];
|
|
1188
|
-
for (const entry of value) {
|
|
1189
|
-
const cloned = cloneJsonValue(entry);
|
|
1190
|
-
if (cloned !== void 0) arr.push(cloned);
|
|
1191
|
-
}
|
|
1192
|
-
return arr;
|
|
1193
|
-
}
|
|
1194
|
-
if (value && typeof value === "object") {
|
|
1195
|
-
const input = value;
|
|
1196
|
-
const obj = {};
|
|
1197
|
-
for (const [key, entry] of Object.entries(input)) {
|
|
1198
|
-
const cloned = cloneJsonValue(entry);
|
|
1199
|
-
if (cloned !== void 0) obj[key] = cloned;
|
|
1200
|
-
}
|
|
1201
|
-
return obj;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
function cloneJsonObject(value) {
|
|
1205
|
-
const cloned = cloneJsonValue(value);
|
|
1206
|
-
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1207
|
-
return {};
|
|
1208
|
-
}
|
|
1209
|
-
function asJsonObject(value) {
|
|
1210
|
-
const cloned = cloneJsonValue(value);
|
|
1211
|
-
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1212
|
-
}
|
|
1213
|
-
function isJsonObjectValue(value) {
|
|
1214
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1215
|
-
}
|
|
1216
|
-
function stableStringify(value) {
|
|
1217
|
-
if (value === null) return "null";
|
|
1218
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
1219
|
-
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
1220
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1221
|
-
if (!isJsonObjectValue(value)) return "null";
|
|
1222
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
1223
|
-
}
|
|
1224
|
-
function jsonEquals(a, b) {
|
|
1225
|
-
if (a === void 0 && b === void 0) return true;
|
|
1226
|
-
if (a === void 0 || b === void 0) return false;
|
|
1227
|
-
return stableStringify(a) === stableStringify(b);
|
|
1228
|
-
}
|
|
1229
|
-
function diffJsonObjects(previous, next) {
|
|
1230
|
-
const patch = {};
|
|
1231
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1232
|
-
if (previous) for (const key of Object.keys(previous)) keys.add(key);
|
|
1233
|
-
for (const key of Object.keys(next)) keys.add(key);
|
|
1234
|
-
for (const key of keys) {
|
|
1235
|
-
const prevValue = previous ? previous[key] : void 0;
|
|
1236
|
-
const nextValue = next[key];
|
|
1237
|
-
if (!jsonEquals(prevValue, nextValue)) {
|
|
1238
|
-
if (nextValue === void 0 && previous && key in previous) {
|
|
1239
|
-
patch[key] = null;
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
const cloned = cloneJsonValue(nextValue);
|
|
1243
|
-
if (cloned !== void 0) patch[key] = cloned;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return patch;
|
|
1247
|
-
}
|
|
1248
|
-
function assetMetaToJson(meta) {
|
|
1249
|
-
const json = {
|
|
1250
|
-
assetId: meta.assetId,
|
|
1251
|
-
size: meta.size,
|
|
1252
|
-
createdAt: meta.createdAt
|
|
1253
|
-
};
|
|
1254
|
-
if (meta.mime !== void 0) json.mime = meta.mime;
|
|
1255
|
-
if (meta.policy !== void 0) json.policy = meta.policy;
|
|
1256
|
-
if (meta.tag !== void 0) json.tag = meta.tag;
|
|
1257
|
-
return json;
|
|
1258
|
-
}
|
|
1259
|
-
function assetMetaFromJson(value) {
|
|
1260
|
-
const obj = asJsonObject(value);
|
|
1261
|
-
if (!obj) return void 0;
|
|
1262
|
-
const assetId = typeof obj.assetId === "string" ? obj.assetId : void 0;
|
|
1263
|
-
if (!assetId) return void 0;
|
|
1264
|
-
const size = typeof obj.size === "number" ? obj.size : void 0;
|
|
1265
|
-
const createdAt = typeof obj.createdAt === "number" ? obj.createdAt : void 0;
|
|
1266
|
-
if (size === void 0 || createdAt === void 0) return void 0;
|
|
1267
|
-
return {
|
|
1268
|
-
assetId,
|
|
1269
|
-
size,
|
|
1270
|
-
createdAt,
|
|
1271
|
-
...typeof obj.mime === "string" ? { mime: obj.mime } : {},
|
|
1272
|
-
...typeof obj.policy === "string" ? { policy: obj.policy } : {},
|
|
1273
|
-
...typeof obj.tag === "string" ? { tag: obj.tag } : {}
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
function assetMetadataEqual(a, b) {
|
|
1277
|
-
if (!a && !b) return true;
|
|
1278
|
-
if (!a || !b) return false;
|
|
1279
|
-
return stableStringify(assetMetaToJson(a)) === stableStringify(assetMetaToJson(b));
|
|
1280
|
-
}
|
|
1281
|
-
function cloneRepoAssetMetadata(meta) {
|
|
1282
|
-
return {
|
|
1283
|
-
assetId: meta.assetId,
|
|
1284
|
-
size: meta.size,
|
|
1285
|
-
createdAt: meta.createdAt,
|
|
1286
|
-
...meta.mime !== void 0 ? { mime: meta.mime } : {},
|
|
1287
|
-
...meta.policy !== void 0 ? { policy: meta.policy } : {},
|
|
1288
|
-
...meta.tag !== void 0 ? { tag: meta.tag } : {}
|
|
1289
|
-
};
|
|
1290
|
-
}
|
|
1291
|
-
function toReadableStream(bytes) {
|
|
1292
|
-
return new ReadableStream({ start(controller) {
|
|
1293
|
-
controller.enqueue(bytes);
|
|
1294
|
-
controller.close();
|
|
1295
|
-
} });
|
|
1296
|
-
}
|
|
1297
|
-
function canonicalizeFrontiers(frontiers) {
|
|
1298
|
-
const json = [...frontiers].sort((a, b) => {
|
|
1299
|
-
if (a.peer < b.peer) return -1;
|
|
1300
|
-
if (a.peer > b.peer) return 1;
|
|
1301
|
-
return a.counter - b.counter;
|
|
1302
|
-
}).map((f) => ({
|
|
1303
|
-
peer: f.peer,
|
|
1304
|
-
counter: f.counter
|
|
1305
|
-
}));
|
|
1306
|
-
return {
|
|
1307
|
-
json,
|
|
1308
|
-
key: stableStringify(json)
|
|
1309
|
-
};
|
|
1310
|
-
}
|
|
1311
|
-
function includesFrontiers(vv, frontiers) {
|
|
1312
|
-
for (const { peer, counter } of frontiers) if ((vv.get(peer) ?? 0) <= counter) return false;
|
|
1313
|
-
return true;
|
|
1314
|
-
}
|
|
1315
|
-
function matchesQuery(docId, _metadata, query) {
|
|
1316
|
-
if (!query) return true;
|
|
1317
|
-
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
1318
|
-
if (query.start && docId < query.start) return false;
|
|
1319
|
-
if (query.end && docId > query.end) return false;
|
|
1320
|
-
return true;
|
|
1321
|
-
}
|
|
1104
|
+
this.watchers.clear();
|
|
1105
|
+
this.eventByStack.length = 0;
|
|
1106
|
+
}
|
|
1107
|
+
pushEventBy(by) {
|
|
1108
|
+
this.eventByStack.push(by);
|
|
1109
|
+
}
|
|
1110
|
+
popEventBy() {
|
|
1111
|
+
this.eventByStack.pop();
|
|
1112
|
+
}
|
|
1113
|
+
resolveEventBy(defaultBy) {
|
|
1114
|
+
const index = this.eventByStack.length - 1;
|
|
1115
|
+
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
1116
|
+
}
|
|
1117
|
+
shouldNotify(filter, event) {
|
|
1118
|
+
if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
|
|
1119
|
+
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
1120
|
+
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
1121
|
+
const docId = (() => {
|
|
1122
|
+
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
1123
|
+
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
1124
|
+
})();
|
|
1125
|
+
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
1126
|
+
if (filter.docIds && !docId) return false;
|
|
1127
|
+
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
1128
|
+
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
1129
|
+
}
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1322
1133
|
|
|
1323
1134
|
//#endregion
|
|
1324
1135
|
//#region src/internal/logging.ts
|
|
@@ -1337,23 +1148,18 @@ var DocManager = class {
|
|
|
1337
1148
|
getMetaFlock;
|
|
1338
1149
|
eventBus;
|
|
1339
1150
|
persistMeta;
|
|
1340
|
-
state;
|
|
1341
1151
|
docs = /* @__PURE__ */ new Map();
|
|
1342
1152
|
docSubscriptions = /* @__PURE__ */ new Map();
|
|
1343
1153
|
docFrontierUpdates = /* @__PURE__ */ new Map();
|
|
1344
1154
|
docPersistedVersions = /* @__PURE__ */ new Map();
|
|
1345
|
-
get docFrontierKeys() {
|
|
1346
|
-
return this.state.docFrontierKeys;
|
|
1347
|
-
}
|
|
1348
1155
|
constructor(options) {
|
|
1349
1156
|
this.storage = options.storage;
|
|
1350
1157
|
this.docFrontierDebounceMs = options.docFrontierDebounceMs;
|
|
1351
1158
|
this.getMetaFlock = options.getMetaFlock;
|
|
1352
1159
|
this.eventBus = options.eventBus;
|
|
1353
1160
|
this.persistMeta = options.persistMeta;
|
|
1354
|
-
this.state = options.state;
|
|
1355
1161
|
}
|
|
1356
|
-
async
|
|
1162
|
+
async openPersistedDoc(docId) {
|
|
1357
1163
|
return await this.ensureDoc(docId);
|
|
1358
1164
|
}
|
|
1359
1165
|
async openDetachedDoc(docId) {
|
|
@@ -1400,38 +1206,27 @@ var DocManager = class {
|
|
|
1400
1206
|
}
|
|
1401
1207
|
async updateDocFrontiers(docId, doc, defaultBy) {
|
|
1402
1208
|
const frontiers = doc.oplogFrontiers();
|
|
1403
|
-
const
|
|
1404
|
-
const
|
|
1209
|
+
const vv = doc.version();
|
|
1210
|
+
const existingFrontiers = this.readFrontiersFromFlock(docId);
|
|
1405
1211
|
let mutated = false;
|
|
1406
1212
|
const metaFlock = this.metaFlock;
|
|
1407
|
-
const
|
|
1408
|
-
for (const entry of existingKeys) {
|
|
1409
|
-
if (entry === key) continue;
|
|
1410
|
-
let oldFrontiers;
|
|
1411
|
-
try {
|
|
1412
|
-
oldFrontiers = JSON.parse(entry);
|
|
1413
|
-
} catch {
|
|
1414
|
-
continue;
|
|
1415
|
-
}
|
|
1416
|
-
if (includesFrontiers(vv, oldFrontiers)) {
|
|
1417
|
-
metaFlock.delete([
|
|
1418
|
-
"f",
|
|
1419
|
-
docId,
|
|
1420
|
-
entry
|
|
1421
|
-
]);
|
|
1422
|
-
mutated = true;
|
|
1423
|
-
}
|
|
1424
|
-
}
|
|
1425
|
-
if (!existingKeys.has(key)) {
|
|
1213
|
+
for (const f of frontiers) if (existingFrontiers.get(f.peer) !== f.counter) {
|
|
1426
1214
|
metaFlock.put([
|
|
1427
1215
|
"f",
|
|
1428
1216
|
docId,
|
|
1429
|
-
|
|
1430
|
-
],
|
|
1217
|
+
f.peer
|
|
1218
|
+
], f.counter);
|
|
1431
1219
|
mutated = true;
|
|
1432
1220
|
}
|
|
1433
1221
|
if (mutated) {
|
|
1434
|
-
|
|
1222
|
+
for (const [peer, counter] of existingFrontiers) {
|
|
1223
|
+
const docCounterEnd = vv.get(peer);
|
|
1224
|
+
if (docCounterEnd != null && docCounterEnd > counter) metaFlock.delete([
|
|
1225
|
+
"f",
|
|
1226
|
+
docId,
|
|
1227
|
+
peer
|
|
1228
|
+
]);
|
|
1229
|
+
}
|
|
1435
1230
|
await this.persistMeta();
|
|
1436
1231
|
}
|
|
1437
1232
|
const by = this.eventBus.resolveEventBy(defaultBy);
|
|
@@ -1483,37 +1278,22 @@ var DocManager = class {
|
|
|
1483
1278
|
this.docFrontierUpdates.clear();
|
|
1484
1279
|
this.docs.clear();
|
|
1485
1280
|
this.docPersistedVersions.clear();
|
|
1486
|
-
this.docFrontierKeys.clear();
|
|
1487
1281
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
const frontierRows = this.metaFlock.scan({ prefix: ["f"] });
|
|
1491
|
-
for (const row of frontierRows) {
|
|
1492
|
-
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1493
|
-
const docId = row.key[1];
|
|
1494
|
-
const frontierKey = row.key[2];
|
|
1495
|
-
if (typeof docId !== "string" || typeof frontierKey !== "string") continue;
|
|
1496
|
-
const set = nextFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1497
|
-
set.add(frontierKey);
|
|
1498
|
-
nextFrontierKeys.set(docId, set);
|
|
1499
|
-
}
|
|
1500
|
-
this.docFrontierKeys.clear();
|
|
1501
|
-
for (const [docId, keys] of nextFrontierKeys) this.docFrontierKeys.set(docId, keys);
|
|
1282
|
+
get metaFlock() {
|
|
1283
|
+
return this.getMetaFlock();
|
|
1502
1284
|
}
|
|
1503
|
-
|
|
1285
|
+
readFrontiersFromFlock(docId) {
|
|
1504
1286
|
const rows = this.metaFlock.scan({ prefix: ["f", docId] });
|
|
1505
|
-
const
|
|
1287
|
+
const frontiers = /* @__PURE__ */ new Map();
|
|
1506
1288
|
for (const row of rows) {
|
|
1507
1289
|
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1508
|
-
|
|
1509
|
-
const
|
|
1510
|
-
if (typeof
|
|
1290
|
+
const peer = row.key[2];
|
|
1291
|
+
const counter = row.value;
|
|
1292
|
+
if (typeof peer !== "string") continue;
|
|
1293
|
+
if (typeof counter !== "number" || !Number.isFinite(counter)) continue;
|
|
1294
|
+
frontiers.set(peer, counter);
|
|
1511
1295
|
}
|
|
1512
|
-
|
|
1513
|
-
else this.docFrontierKeys.delete(docId);
|
|
1514
|
-
}
|
|
1515
|
-
get metaFlock() {
|
|
1516
|
-
return this.getMetaFlock();
|
|
1296
|
+
return frontiers;
|
|
1517
1297
|
}
|
|
1518
1298
|
registerDoc(docId, doc) {
|
|
1519
1299
|
this.docs.set(docId, doc);
|
|
@@ -1626,6 +1406,176 @@ var DocManager = class {
|
|
|
1626
1406
|
}
|
|
1627
1407
|
};
|
|
1628
1408
|
|
|
1409
|
+
//#endregion
|
|
1410
|
+
//#region src/utils.ts
|
|
1411
|
+
async function streamToUint8Array(stream) {
|
|
1412
|
+
const reader = stream.getReader();
|
|
1413
|
+
const chunks = [];
|
|
1414
|
+
let total = 0;
|
|
1415
|
+
while (true) {
|
|
1416
|
+
const { done, value } = await reader.read();
|
|
1417
|
+
if (done) break;
|
|
1418
|
+
if (value) {
|
|
1419
|
+
chunks.push(value);
|
|
1420
|
+
total += value.byteLength;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
const buffer = new Uint8Array(total);
|
|
1424
|
+
let offset = 0;
|
|
1425
|
+
for (const chunk of chunks) {
|
|
1426
|
+
buffer.set(chunk, offset);
|
|
1427
|
+
offset += chunk.byteLength;
|
|
1428
|
+
}
|
|
1429
|
+
return buffer;
|
|
1430
|
+
}
|
|
1431
|
+
async function assetContentToUint8Array(content) {
|
|
1432
|
+
if (content instanceof Uint8Array) return content;
|
|
1433
|
+
if (ArrayBuffer.isView(content)) return new Uint8Array(content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
|
|
1434
|
+
if (typeof Blob !== "undefined" && content instanceof Blob) return new Uint8Array(await content.arrayBuffer());
|
|
1435
|
+
if (typeof ReadableStream !== "undefined" && content instanceof ReadableStream) return streamToUint8Array(content);
|
|
1436
|
+
throw new TypeError("Unsupported asset content type");
|
|
1437
|
+
}
|
|
1438
|
+
function bytesToHex$1(bytes) {
|
|
1439
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1440
|
+
}
|
|
1441
|
+
async function computeSha256(bytes) {
|
|
1442
|
+
const globalCrypto = globalThis.crypto;
|
|
1443
|
+
if (globalCrypto?.subtle && typeof globalCrypto.subtle.digest === "function") {
|
|
1444
|
+
const digest = await globalCrypto.subtle.digest("SHA-256", bytes);
|
|
1445
|
+
return bytesToHex$1(new Uint8Array(digest));
|
|
1446
|
+
}
|
|
1447
|
+
try {
|
|
1448
|
+
const { createHash } = await import("node:crypto");
|
|
1449
|
+
const hash = createHash("sha256");
|
|
1450
|
+
hash.update(bytes);
|
|
1451
|
+
return hash.digest("hex");
|
|
1452
|
+
} catch {
|
|
1453
|
+
throw new Error("SHA-256 digest is not available in this environment");
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
function cloneJsonValue(value) {
|
|
1457
|
+
if (value === null) return null;
|
|
1458
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
1459
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
1460
|
+
if (Array.isArray(value)) {
|
|
1461
|
+
const arr = [];
|
|
1462
|
+
for (const entry of value) {
|
|
1463
|
+
const cloned = cloneJsonValue(entry);
|
|
1464
|
+
if (cloned !== void 0) arr.push(cloned);
|
|
1465
|
+
}
|
|
1466
|
+
return arr;
|
|
1467
|
+
}
|
|
1468
|
+
if (value && typeof value === "object") {
|
|
1469
|
+
const input = value;
|
|
1470
|
+
const obj = {};
|
|
1471
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
1472
|
+
const cloned = cloneJsonValue(entry);
|
|
1473
|
+
if (cloned !== void 0) obj[key] = cloned;
|
|
1474
|
+
}
|
|
1475
|
+
return obj;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function cloneJsonObject(value) {
|
|
1479
|
+
const cloned = cloneJsonValue(value);
|
|
1480
|
+
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1481
|
+
return {};
|
|
1482
|
+
}
|
|
1483
|
+
function asJsonObject(value) {
|
|
1484
|
+
const cloned = cloneJsonValue(value);
|
|
1485
|
+
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1486
|
+
}
|
|
1487
|
+
function isJsonObjectValue(value) {
|
|
1488
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1489
|
+
}
|
|
1490
|
+
function stableStringify(value) {
|
|
1491
|
+
if (value === null) return "null";
|
|
1492
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1493
|
+
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
1494
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1495
|
+
if (!isJsonObjectValue(value)) return "null";
|
|
1496
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
1497
|
+
}
|
|
1498
|
+
function jsonEquals(a, b) {
|
|
1499
|
+
if (a === void 0 && b === void 0) return true;
|
|
1500
|
+
if (a === void 0 || b === void 0) return false;
|
|
1501
|
+
return stableStringify(a) === stableStringify(b);
|
|
1502
|
+
}
|
|
1503
|
+
function diffJsonObjects(previous, next) {
|
|
1504
|
+
const patch = {};
|
|
1505
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1506
|
+
if (previous) for (const key of Object.keys(previous)) keys.add(key);
|
|
1507
|
+
for (const key of Object.keys(next)) keys.add(key);
|
|
1508
|
+
for (const key of keys) {
|
|
1509
|
+
const prevValue = previous ? previous[key] : void 0;
|
|
1510
|
+
const nextValue = next[key];
|
|
1511
|
+
if (!jsonEquals(prevValue, nextValue)) {
|
|
1512
|
+
if (nextValue === void 0 && previous && key in previous) {
|
|
1513
|
+
patch[key] = null;
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
const cloned = cloneJsonValue(nextValue);
|
|
1517
|
+
if (cloned !== void 0) patch[key] = cloned;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return patch;
|
|
1521
|
+
}
|
|
1522
|
+
function assetMetaToJson(meta) {
|
|
1523
|
+
const json = {
|
|
1524
|
+
assetId: meta.assetId,
|
|
1525
|
+
size: meta.size,
|
|
1526
|
+
createdAt: meta.createdAt
|
|
1527
|
+
};
|
|
1528
|
+
if (meta.mime !== void 0) json.mime = meta.mime;
|
|
1529
|
+
if (meta.policy !== void 0) json.policy = meta.policy;
|
|
1530
|
+
if (meta.tag !== void 0) json.tag = meta.tag;
|
|
1531
|
+
return json;
|
|
1532
|
+
}
|
|
1533
|
+
function assetMetaFromJson(value) {
|
|
1534
|
+
const obj = asJsonObject(value);
|
|
1535
|
+
if (!obj) return void 0;
|
|
1536
|
+
const assetId = typeof obj.assetId === "string" ? obj.assetId : void 0;
|
|
1537
|
+
if (!assetId) return void 0;
|
|
1538
|
+
const size = typeof obj.size === "number" ? obj.size : void 0;
|
|
1539
|
+
const createdAt = typeof obj.createdAt === "number" ? obj.createdAt : void 0;
|
|
1540
|
+
if (size === void 0 || createdAt === void 0) return void 0;
|
|
1541
|
+
return {
|
|
1542
|
+
assetId,
|
|
1543
|
+
size,
|
|
1544
|
+
createdAt,
|
|
1545
|
+
...typeof obj.mime === "string" ? { mime: obj.mime } : {},
|
|
1546
|
+
...typeof obj.policy === "string" ? { policy: obj.policy } : {},
|
|
1547
|
+
...typeof obj.tag === "string" ? { tag: obj.tag } : {}
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
function assetMetadataEqual(a, b) {
|
|
1551
|
+
if (!a && !b) return true;
|
|
1552
|
+
if (!a || !b) return false;
|
|
1553
|
+
return stableStringify(assetMetaToJson(a)) === stableStringify(assetMetaToJson(b));
|
|
1554
|
+
}
|
|
1555
|
+
function cloneRepoAssetMetadata(meta) {
|
|
1556
|
+
return {
|
|
1557
|
+
assetId: meta.assetId,
|
|
1558
|
+
size: meta.size,
|
|
1559
|
+
createdAt: meta.createdAt,
|
|
1560
|
+
...meta.mime !== void 0 ? { mime: meta.mime } : {},
|
|
1561
|
+
...meta.policy !== void 0 ? { policy: meta.policy } : {},
|
|
1562
|
+
...meta.tag !== void 0 ? { tag: meta.tag } : {}
|
|
1563
|
+
};
|
|
1564
|
+
}
|
|
1565
|
+
function toReadableStream(bytes) {
|
|
1566
|
+
return new ReadableStream({ start(controller) {
|
|
1567
|
+
controller.enqueue(bytes);
|
|
1568
|
+
controller.close();
|
|
1569
|
+
} });
|
|
1570
|
+
}
|
|
1571
|
+
function matchesQuery(docId, _metadata, query) {
|
|
1572
|
+
if (!query) return true;
|
|
1573
|
+
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
1574
|
+
if (query.start && docId < query.start) return false;
|
|
1575
|
+
if (query.end && docId > query.end) return false;
|
|
1576
|
+
return true;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1629
1579
|
//#endregion
|
|
1630
1580
|
//#region src/internal/metadata-manager.ts
|
|
1631
1581
|
var MetadataManager = class {
|
|
@@ -1867,13 +1817,11 @@ var AssetManager = class {
|
|
|
1867
1817
|
if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
|
|
1868
1818
|
const existing = this.assets.get(assetId);
|
|
1869
1819
|
if (existing) {
|
|
1870
|
-
if (
|
|
1871
|
-
|
|
1872
|
-
existing.data = clone;
|
|
1873
|
-
if (this.storage) await this.storage.save({
|
|
1820
|
+
if (this.storage) {
|
|
1821
|
+
if (!await this.storage.loadAsset(assetId)) await this.storage.save({
|
|
1874
1822
|
type: "asset",
|
|
1875
1823
|
assetId,
|
|
1876
|
-
data:
|
|
1824
|
+
data: bytes.slice()
|
|
1877
1825
|
});
|
|
1878
1826
|
}
|
|
1879
1827
|
let metadataMutated = false;
|
|
@@ -1900,7 +1848,7 @@ var AssetManager = class {
|
|
|
1900
1848
|
await this.persistMeta();
|
|
1901
1849
|
this.eventBus.emit({
|
|
1902
1850
|
kind: "asset-metadata",
|
|
1903
|
-
asset: this.createAssetDownload(assetId, metadata$1,
|
|
1851
|
+
asset: this.createAssetDownload(assetId, metadata$1, bytes),
|
|
1904
1852
|
by: "local"
|
|
1905
1853
|
});
|
|
1906
1854
|
}
|
|
@@ -1930,7 +1878,8 @@ var AssetManager = class {
|
|
|
1930
1878
|
assetId,
|
|
1931
1879
|
data: storedBytes.slice()
|
|
1932
1880
|
});
|
|
1933
|
-
this.rememberAsset(metadata
|
|
1881
|
+
this.rememberAsset(metadata);
|
|
1882
|
+
this.markAssetAsOrphan(assetId, metadata);
|
|
1934
1883
|
this.updateDocAssetMetadata(assetId, metadata);
|
|
1935
1884
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
1936
1885
|
await this.persistMeta();
|
|
@@ -1951,13 +1900,11 @@ var AssetManager = class {
|
|
|
1951
1900
|
const existing = this.assets.get(assetId);
|
|
1952
1901
|
if (existing) {
|
|
1953
1902
|
metadata = existing.metadata;
|
|
1954
|
-
if (
|
|
1955
|
-
|
|
1956
|
-
existing.data = clone;
|
|
1957
|
-
if (this.storage) await this.storage.save({
|
|
1903
|
+
if (this.storage) {
|
|
1904
|
+
if (!await this.storage.loadAsset(assetId)) await this.storage.save({
|
|
1958
1905
|
type: "asset",
|
|
1959
1906
|
assetId,
|
|
1960
|
-
data:
|
|
1907
|
+
data: bytes.slice()
|
|
1961
1908
|
});
|
|
1962
1909
|
}
|
|
1963
1910
|
let nextMetadata = metadata;
|
|
@@ -1997,11 +1944,11 @@ var AssetManager = class {
|
|
|
1997
1944
|
await this.persistMeta();
|
|
1998
1945
|
this.eventBus.emit({
|
|
1999
1946
|
kind: "asset-metadata",
|
|
2000
|
-
asset: this.createAssetDownload(assetId, metadata,
|
|
1947
|
+
asset: this.createAssetDownload(assetId, metadata, bytes),
|
|
2001
1948
|
by: "local"
|
|
2002
1949
|
});
|
|
2003
1950
|
} else metadata = existing.metadata;
|
|
2004
|
-
storedBytes =
|
|
1951
|
+
storedBytes = bytes;
|
|
2005
1952
|
this.rememberAsset(metadata);
|
|
2006
1953
|
} else {
|
|
2007
1954
|
metadata = {
|
|
@@ -2027,7 +1974,7 @@ var AssetManager = class {
|
|
|
2027
1974
|
assetId,
|
|
2028
1975
|
data: storedBytes.slice()
|
|
2029
1976
|
});
|
|
2030
|
-
this.rememberAsset(metadata
|
|
1977
|
+
this.rememberAsset(metadata);
|
|
2031
1978
|
this.updateDocAssetMetadata(assetId, metadata);
|
|
2032
1979
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
2033
1980
|
created = true;
|
|
@@ -2035,9 +1982,7 @@ var AssetManager = class {
|
|
|
2035
1982
|
const mapping = this.docAssets.get(docId) ?? /* @__PURE__ */ new Map();
|
|
2036
1983
|
mapping.set(assetId, metadata);
|
|
2037
1984
|
this.docAssets.set(docId, mapping);
|
|
2038
|
-
|
|
2039
|
-
refs.add(docId);
|
|
2040
|
-
this.assetToDocRefs.set(assetId, refs);
|
|
1985
|
+
this.addDocReference(assetId, docId);
|
|
2041
1986
|
this.metaFlock.put([
|
|
2042
1987
|
"ld",
|
|
2043
1988
|
docId,
|
|
@@ -2067,20 +2012,7 @@ var AssetManager = class {
|
|
|
2067
2012
|
docId,
|
|
2068
2013
|
assetId
|
|
2069
2014
|
]);
|
|
2070
|
-
|
|
2071
|
-
if (refs) {
|
|
2072
|
-
refs.delete(docId);
|
|
2073
|
-
if (refs.size === 0) {
|
|
2074
|
-
this.assetToDocRefs.delete(assetId);
|
|
2075
|
-
const record = this.assets.get(assetId);
|
|
2076
|
-
if (record) this.orphanedAssets.set(assetId, {
|
|
2077
|
-
metadata: record.metadata,
|
|
2078
|
-
deletedAt: Date.now()
|
|
2079
|
-
});
|
|
2080
|
-
this.metaFlock.delete(["a", assetId]);
|
|
2081
|
-
this.assets.delete(assetId);
|
|
2082
|
-
}
|
|
2083
|
-
}
|
|
2015
|
+
this.removeDocAssetReference(assetId, docId);
|
|
2084
2016
|
await this.persistMeta();
|
|
2085
2017
|
this.eventBus.emit({
|
|
2086
2018
|
kind: "asset-unlink",
|
|
@@ -2167,12 +2099,11 @@ var AssetManager = class {
|
|
|
2167
2099
|
this.handleAssetRemoval(assetId, by);
|
|
2168
2100
|
return;
|
|
2169
2101
|
}
|
|
2170
|
-
|
|
2171
|
-
this.rememberAsset(metadata, existingData);
|
|
2102
|
+
this.rememberAsset(metadata);
|
|
2172
2103
|
this.updateDocAssetMetadata(assetId, cloneRepoAssetMetadata(metadata));
|
|
2173
2104
|
if (!previous || !assetMetadataEqual(previous.metadata, metadata)) this.eventBus.emit({
|
|
2174
2105
|
kind: "asset-metadata",
|
|
2175
|
-
asset: this.createAssetDownload(assetId, metadata
|
|
2106
|
+
asset: this.createAssetDownload(assetId, metadata),
|
|
2176
2107
|
by
|
|
2177
2108
|
});
|
|
2178
2109
|
}
|
|
@@ -2187,11 +2118,7 @@ var AssetManager = class {
|
|
|
2187
2118
|
if (typeof assetId !== "string") continue;
|
|
2188
2119
|
const metadata = assetMetaFromJson(row.value);
|
|
2189
2120
|
if (!metadata) continue;
|
|
2190
|
-
|
|
2191
|
-
nextAssets.set(assetId, {
|
|
2192
|
-
metadata,
|
|
2193
|
-
data: existing?.data
|
|
2194
|
-
});
|
|
2121
|
+
nextAssets.set(assetId, { metadata });
|
|
2195
2122
|
}
|
|
2196
2123
|
const nextDocAssets = /* @__PURE__ */ new Map();
|
|
2197
2124
|
const linkRows = this.metaFlock.scan({ prefix: ["ld"] });
|
|
@@ -2227,12 +2154,18 @@ var AssetManager = class {
|
|
|
2227
2154
|
this.assetToDocRefs.set(assetId, refs);
|
|
2228
2155
|
}
|
|
2229
2156
|
this.assets.clear();
|
|
2230
|
-
for (const record of nextAssets.values()) this.rememberAsset(record.metadata
|
|
2157
|
+
for (const record of nextAssets.values()) this.rememberAsset(record.metadata);
|
|
2158
|
+
for (const assetId of nextAssets.keys()) {
|
|
2159
|
+
const refs = this.assetToDocRefs.get(assetId);
|
|
2160
|
+
if (!refs || refs.size === 0) {
|
|
2161
|
+
if (!this.orphanedAssets.has(assetId)) this.markAssetAsOrphan(assetId, nextAssets.get(assetId).metadata);
|
|
2162
|
+
} else this.orphanedAssets.delete(assetId);
|
|
2163
|
+
}
|
|
2231
2164
|
for (const [assetId, record] of nextAssets) {
|
|
2232
2165
|
const previous = prevAssets.get(assetId)?.metadata;
|
|
2233
2166
|
if (!assetMetadataEqual(previous, record.metadata)) this.eventBus.emit({
|
|
2234
2167
|
kind: "asset-metadata",
|
|
2235
|
-
asset: this.createAssetDownload(assetId, record.metadata
|
|
2168
|
+
asset: this.createAssetDownload(assetId, record.metadata),
|
|
2236
2169
|
by
|
|
2237
2170
|
});
|
|
2238
2171
|
}
|
|
@@ -2328,36 +2261,24 @@ var AssetManager = class {
|
|
|
2328
2261
|
};
|
|
2329
2262
|
}
|
|
2330
2263
|
async materializeAsset(assetId) {
|
|
2331
|
-
|
|
2332
|
-
if (record?.data) return {
|
|
2333
|
-
metadata: record.metadata,
|
|
2334
|
-
bytes: record.data.slice()
|
|
2335
|
-
};
|
|
2264
|
+
const record = this.assets.get(assetId);
|
|
2336
2265
|
if (record && this.storage) {
|
|
2337
2266
|
const stored = await this.storage.loadAsset(assetId);
|
|
2338
|
-
if (stored) {
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
metadata: record.metadata,
|
|
2343
|
-
bytes: clone
|
|
2344
|
-
};
|
|
2345
|
-
}
|
|
2267
|
+
if (stored) return {
|
|
2268
|
+
metadata: record.metadata,
|
|
2269
|
+
bytes: stored
|
|
2270
|
+
};
|
|
2346
2271
|
}
|
|
2347
2272
|
if (!record && this.storage) {
|
|
2348
2273
|
const stored = await this.storage.loadAsset(assetId);
|
|
2349
2274
|
if (stored) {
|
|
2350
2275
|
const metadata$1 = this.getAssetMetadata(assetId);
|
|
2351
2276
|
if (!metadata$1) throw new Error(`Missing metadata for asset ${assetId}`);
|
|
2352
|
-
|
|
2353
|
-
this.assets.set(assetId, {
|
|
2354
|
-
metadata: metadata$1,
|
|
2355
|
-
data: clone.slice()
|
|
2356
|
-
});
|
|
2277
|
+
this.assets.set(assetId, { metadata: metadata$1 });
|
|
2357
2278
|
this.updateDocAssetMetadata(assetId, metadata$1);
|
|
2358
2279
|
return {
|
|
2359
2280
|
metadata: metadata$1,
|
|
2360
|
-
bytes:
|
|
2281
|
+
bytes: stored
|
|
2361
2282
|
};
|
|
2362
2283
|
}
|
|
2363
2284
|
}
|
|
@@ -2373,10 +2294,7 @@ var AssetManager = class {
|
|
|
2373
2294
|
...remote.policy ? { policy: remote.policy } : {},
|
|
2374
2295
|
...remote.tag ? { tag: remote.tag } : {}
|
|
2375
2296
|
};
|
|
2376
|
-
this.assets.set(assetId, {
|
|
2377
|
-
metadata,
|
|
2378
|
-
data: remoteBytes.slice()
|
|
2379
|
-
});
|
|
2297
|
+
this.assets.set(assetId, { metadata });
|
|
2380
2298
|
this.updateDocAssetMetadata(assetId, metadata);
|
|
2381
2299
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
2382
2300
|
await this.persistMeta();
|
|
@@ -2398,18 +2316,14 @@ var AssetManager = class {
|
|
|
2398
2316
|
if (assets) assets.set(assetId, metadata);
|
|
2399
2317
|
}
|
|
2400
2318
|
}
|
|
2401
|
-
rememberAsset(metadata
|
|
2402
|
-
|
|
2403
|
-
this.assets.set(metadata.assetId, {
|
|
2404
|
-
metadata,
|
|
2405
|
-
data
|
|
2406
|
-
});
|
|
2407
|
-
this.orphanedAssets.delete(metadata.assetId);
|
|
2319
|
+
rememberAsset(metadata) {
|
|
2320
|
+
this.assets.set(metadata.assetId, { metadata });
|
|
2408
2321
|
}
|
|
2409
2322
|
addDocReference(assetId, docId) {
|
|
2410
2323
|
const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
|
|
2411
2324
|
refs.add(docId);
|
|
2412
2325
|
this.assetToDocRefs.set(assetId, refs);
|
|
2326
|
+
this.orphanedAssets.delete(assetId);
|
|
2413
2327
|
}
|
|
2414
2328
|
removeDocAssetReference(assetId, docId) {
|
|
2415
2329
|
const refs = this.assetToDocRefs.get(assetId);
|
|
@@ -2459,13 +2373,11 @@ var FlockHydrator = class {
|
|
|
2459
2373
|
const nextMetadata = this.readAllDocMetadata();
|
|
2460
2374
|
this.metadataManager.replaceAll(nextMetadata, by);
|
|
2461
2375
|
this.assetManager.hydrateFromFlock(by);
|
|
2462
|
-
this.docManager.hydrateFrontierKeys();
|
|
2463
2376
|
}
|
|
2464
2377
|
applyEvents(events, by) {
|
|
2465
2378
|
if (!events.length) return;
|
|
2466
2379
|
const docMetadataIds = /* @__PURE__ */ new Set();
|
|
2467
2380
|
const docAssetIds = /* @__PURE__ */ new Set();
|
|
2468
|
-
const docFrontiersIds = /* @__PURE__ */ new Set();
|
|
2469
2381
|
const assetIds = /* @__PURE__ */ new Set();
|
|
2470
2382
|
for (const event of events) {
|
|
2471
2383
|
const key = event.key;
|
|
@@ -2482,15 +2394,11 @@ var FlockHydrator = class {
|
|
|
2482
2394
|
const assetId = key[2];
|
|
2483
2395
|
if (typeof docId === "string") docAssetIds.add(docId);
|
|
2484
2396
|
if (typeof assetId === "string") assetIds.add(assetId);
|
|
2485
|
-
} else if (root === "f") {
|
|
2486
|
-
const docId = key[1];
|
|
2487
|
-
if (typeof docId === "string") docFrontiersIds.add(docId);
|
|
2488
2397
|
}
|
|
2489
2398
|
}
|
|
2490
2399
|
for (const assetId of assetIds) this.assetManager.refreshAssetMetadataEntry(assetId, by);
|
|
2491
2400
|
for (const docId of docMetadataIds) this.metadataManager.refreshFromFlock(docId, by);
|
|
2492
2401
|
for (const docId of docAssetIds) this.assetManager.refreshDocAssetsEntry(docId, by);
|
|
2493
|
-
for (const docId of docFrontiersIds) this.docManager.refreshDocFrontierKeys(docId);
|
|
2494
2402
|
}
|
|
2495
2403
|
readAllDocMetadata() {
|
|
2496
2404
|
const nextMetadata = /* @__PURE__ */ new Map();
|
|
@@ -2701,8 +2609,7 @@ function createRepoState() {
|
|
|
2701
2609
|
docAssets: /* @__PURE__ */ new Map(),
|
|
2702
2610
|
assets: /* @__PURE__ */ new Map(),
|
|
2703
2611
|
orphanedAssets: /* @__PURE__ */ new Map(),
|
|
2704
|
-
assetToDocRefs: /* @__PURE__ */ new Map()
|
|
2705
|
-
docFrontierKeys: /* @__PURE__ */ new Map()
|
|
2612
|
+
assetToDocRefs: /* @__PURE__ */ new Map()
|
|
2706
2613
|
};
|
|
2707
2614
|
}
|
|
2708
2615
|
|
|
@@ -2738,8 +2645,7 @@ var LoroRepo = class LoroRepo {
|
|
|
2738
2645
|
docFrontierDebounceMs,
|
|
2739
2646
|
getMetaFlock: () => this.metaFlock,
|
|
2740
2647
|
eventBus: this.eventBus,
|
|
2741
|
-
persistMeta: () => this.persistMeta()
|
|
2742
|
-
state: this.state
|
|
2648
|
+
persistMeta: () => this.persistMeta()
|
|
2743
2649
|
});
|
|
2744
2650
|
this.metadataManager = new MetadataManager({
|
|
2745
2651
|
getMetaFlock: () => this.metaFlock,
|
|
@@ -2828,7 +2734,7 @@ var LoroRepo = class LoroRepo {
|
|
|
2828
2734
|
*/
|
|
2829
2735
|
async openPersistedDoc(docId) {
|
|
2830
2736
|
return {
|
|
2831
|
-
doc: await this.docManager.
|
|
2737
|
+
doc: await this.docManager.openPersistedDoc(docId),
|
|
2832
2738
|
syncOnce: () => {
|
|
2833
2739
|
return this.sync({
|
|
2834
2740
|
scope: "doc",
|