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.cjs
CHANGED
|
@@ -6,16 +6,12 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
8
|
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
}
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
19
15
|
}
|
|
20
16
|
return to;
|
|
21
17
|
};
|
|
@@ -26,883 +22,467 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
22
|
|
|
27
23
|
//#endregion
|
|
28
24
|
let __loro_dev_flock = require("@loro-dev/flock");
|
|
29
|
-
|
|
30
|
-
let loro_protocol = require("loro-protocol");
|
|
31
|
-
let loro_websocket = require("loro-websocket");
|
|
25
|
+
__loro_dev_flock = __toESM(__loro_dev_flock);
|
|
32
26
|
let loro_crdt = require("loro-crdt");
|
|
27
|
+
loro_crdt = __toESM(loro_crdt);
|
|
33
28
|
let node_fs = require("node:fs");
|
|
29
|
+
node_fs = __toESM(node_fs);
|
|
34
30
|
let node_path = require("node:path");
|
|
35
31
|
node_path = __toESM(node_path);
|
|
36
32
|
let node_crypto = require("node:crypto");
|
|
33
|
+
node_crypto = __toESM(node_crypto);
|
|
34
|
+
let loro_adaptors_loro = require("loro-adaptors/loro");
|
|
35
|
+
loro_adaptors_loro = __toESM(loro_adaptors_loro);
|
|
36
|
+
let loro_protocol = require("loro-protocol");
|
|
37
|
+
loro_protocol = __toESM(loro_protocol);
|
|
38
|
+
let loro_websocket = require("loro-websocket");
|
|
39
|
+
loro_websocket = __toESM(loro_websocket);
|
|
40
|
+
let loro_adaptors_flock = require("loro-adaptors/flock");
|
|
41
|
+
loro_adaptors_flock = __toESM(loro_adaptors_flock);
|
|
37
42
|
|
|
38
|
-
//#region src/
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (typeof globalThis !== "object" || globalThis === null) return;
|
|
47
|
-
return globalThis.process?.env;
|
|
48
|
-
};
|
|
49
|
-
const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
|
|
50
|
-
const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
|
|
51
|
-
const wildcardTokens = new Set([
|
|
52
|
-
"*",
|
|
53
|
-
"1",
|
|
54
|
-
"true",
|
|
55
|
-
"all"
|
|
56
|
-
]);
|
|
57
|
-
const namespaceSet = new Set(normalizedNamespaces);
|
|
58
|
-
const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
|
|
59
|
-
const isDebugEnabled = (namespace) => {
|
|
60
|
-
if (!namespaceSet.size) return false;
|
|
61
|
-
if (!namespace) return hasWildcard;
|
|
62
|
-
const normalized = namespace.toLowerCase();
|
|
63
|
-
if (hasWildcard) return true;
|
|
64
|
-
if (namespaceSet.has(normalized)) return true;
|
|
65
|
-
const [root] = normalized.split(":");
|
|
66
|
-
return namespaceSet.has(root);
|
|
67
|
-
};
|
|
68
|
-
const createDebugLogger = (namespace) => {
|
|
69
|
-
const normalized = namespace.toLowerCase();
|
|
70
|
-
return (...args) => {
|
|
71
|
-
if (!isDebugEnabled(normalized)) return;
|
|
72
|
-
const prefix = `[loro-repo:${namespace}]`;
|
|
73
|
-
if (args.length === 0) {
|
|
74
|
-
console.info(prefix);
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
console.info(prefix, ...args);
|
|
43
|
+
//#region src/transport/broadcast-channel.ts
|
|
44
|
+
function deferred() {
|
|
45
|
+
let resolve;
|
|
46
|
+
return {
|
|
47
|
+
promise: new Promise((res) => {
|
|
48
|
+
resolve = res;
|
|
49
|
+
}),
|
|
50
|
+
resolve
|
|
78
51
|
};
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
//#endregion
|
|
82
|
-
//#region src/transport/websocket.ts
|
|
83
|
-
const debug = createDebugLogger("transport:websocket");
|
|
84
|
-
function withTimeout(promise, timeoutMs) {
|
|
85
|
-
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
86
|
-
return new Promise((resolve, reject) => {
|
|
87
|
-
const timer = setTimeout(() => {
|
|
88
|
-
reject(/* @__PURE__ */ new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
89
|
-
}, timeoutMs);
|
|
90
|
-
promise.then((value) => {
|
|
91
|
-
clearTimeout(timer);
|
|
92
|
-
resolve(value);
|
|
93
|
-
}).catch((error) => {
|
|
94
|
-
clearTimeout(timer);
|
|
95
|
-
reject(error);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
52
|
}
|
|
99
|
-
function
|
|
100
|
-
if (typeof
|
|
101
|
-
|
|
102
|
-
|
|
53
|
+
function randomInstanceId() {
|
|
54
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
55
|
+
return Math.random().toString(36).slice(2);
|
|
56
|
+
}
|
|
57
|
+
function ensureBroadcastChannel() {
|
|
58
|
+
if (typeof BroadcastChannel === "undefined") throw new Error("BroadcastChannel API is not available in this environment");
|
|
59
|
+
return BroadcastChannel;
|
|
60
|
+
}
|
|
61
|
+
function encodeDocChannelId(docId) {
|
|
62
|
+
try {
|
|
63
|
+
return encodeURIComponent(docId);
|
|
103
64
|
} catch {
|
|
104
|
-
return
|
|
65
|
+
return docId.replace(/[^a-z0-9_-]/gi, "_");
|
|
105
66
|
}
|
|
106
|
-
return fallback;
|
|
107
67
|
}
|
|
108
|
-
function
|
|
109
|
-
|
|
110
|
-
if (!a || !b) return false;
|
|
111
|
-
if (a.length !== b.length) return false;
|
|
112
|
-
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
113
|
-
return true;
|
|
68
|
+
function postChannelMessage(channel, message) {
|
|
69
|
+
channel.postMessage(message);
|
|
114
70
|
}
|
|
115
71
|
/**
|
|
116
|
-
*
|
|
72
|
+
* TransportAdapter that relies on the BroadcastChannel API to fan out metadata
|
|
73
|
+
* and document updates between browser tabs within the same origin.
|
|
117
74
|
*/
|
|
118
|
-
var
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
75
|
+
var BroadcastChannelTransportAdapter = class {
|
|
76
|
+
instanceId = randomInstanceId();
|
|
77
|
+
namespace;
|
|
78
|
+
metaChannelName;
|
|
79
|
+
connected = false;
|
|
80
|
+
metaState;
|
|
81
|
+
docStates = /* @__PURE__ */ new Map();
|
|
82
|
+
constructor(options = {}) {
|
|
83
|
+
ensureBroadcastChannel();
|
|
84
|
+
this.namespace = options.namespace ?? "loro-repo";
|
|
85
|
+
this.metaChannelName = options.metaChannelName ?? `${this.namespace}-meta`;
|
|
125
86
|
}
|
|
126
|
-
async connect(
|
|
127
|
-
|
|
128
|
-
debug("connect requested", { status: client.getStatus() });
|
|
129
|
-
try {
|
|
130
|
-
await client.connect();
|
|
131
|
-
debug("client.connect resolved");
|
|
132
|
-
await client.waitConnected();
|
|
133
|
-
debug("client.waitConnected resolved", { status: client.getStatus() });
|
|
134
|
-
} catch (error) {
|
|
135
|
-
debug("connect failed", error);
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
87
|
+
async connect() {
|
|
88
|
+
this.connected = true;
|
|
138
89
|
}
|
|
139
90
|
async close() {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
this.docSessions.clear();
|
|
146
|
-
await this.teardownMetadataSession().catch(() => {});
|
|
147
|
-
if (this.client) {
|
|
148
|
-
const client = this.client;
|
|
149
|
-
this.client = void 0;
|
|
150
|
-
client.destroy();
|
|
151
|
-
debug("websocket client destroyed");
|
|
91
|
+
this.connected = false;
|
|
92
|
+
if (this.metaState) {
|
|
93
|
+
for (const entry of this.metaState.listeners) entry.unsubscribe();
|
|
94
|
+
this.metaState.channel.close();
|
|
95
|
+
this.metaState = void 0;
|
|
152
96
|
}
|
|
153
|
-
|
|
97
|
+
for (const [docId] of this.docStates) this.teardownDocChannel(docId);
|
|
98
|
+
this.docStates.clear();
|
|
154
99
|
}
|
|
155
100
|
isConnected() {
|
|
156
|
-
return this.
|
|
101
|
+
return this.connected;
|
|
157
102
|
}
|
|
158
|
-
async syncMeta(flock,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
await withTimeout((await this.ensureMetadataSession(flock, {
|
|
166
|
-
roomId: this.options.metadataRoomId,
|
|
167
|
-
auth: this.options.metadataAuth
|
|
168
|
-
})).firstSynced, options?.timeout);
|
|
169
|
-
debug("syncMeta completed", { roomId: this.options.metadataRoomId });
|
|
170
|
-
return { ok: true };
|
|
171
|
-
} catch (error) {
|
|
172
|
-
debug("syncMeta failed", error);
|
|
173
|
-
return { ok: false };
|
|
174
|
-
}
|
|
103
|
+
async syncMeta(flock, _options) {
|
|
104
|
+
const subscription = this.joinMetaRoom(flock);
|
|
105
|
+
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
106
|
+
await subscription.firstSyncedWithRemote;
|
|
107
|
+
subscription.unsubscribe();
|
|
108
|
+
return { ok: true };
|
|
175
109
|
}
|
|
176
|
-
joinMetaRoom(flock,
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
110
|
+
joinMetaRoom(flock, _params) {
|
|
111
|
+
const state = this.ensureMetaChannel();
|
|
112
|
+
const { promise, resolve } = deferred();
|
|
113
|
+
const listener = {
|
|
114
|
+
flock,
|
|
115
|
+
muted: false,
|
|
116
|
+
unsubscribe: flock.subscribe(() => {
|
|
117
|
+
if (listener.muted) return;
|
|
118
|
+
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
119
|
+
postChannelMessage(state.channel, {
|
|
120
|
+
kind: "meta-export",
|
|
121
|
+
from: this.instanceId,
|
|
122
|
+
bundle
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}),
|
|
126
|
+
resolveFirst: resolve,
|
|
127
|
+
firstSynced: promise
|
|
128
|
+
};
|
|
129
|
+
state.listeners.add(listener);
|
|
130
|
+
postChannelMessage(state.channel, {
|
|
131
|
+
kind: "meta-request",
|
|
132
|
+
from: this.instanceId
|
|
184
133
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
134
|
+
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
135
|
+
postChannelMessage(state.channel, {
|
|
136
|
+
kind: "meta-export",
|
|
137
|
+
from: this.instanceId,
|
|
138
|
+
bundle
|
|
139
|
+
});
|
|
188
140
|
});
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const subscription = {
|
|
141
|
+
queueMicrotask(() => resolve());
|
|
142
|
+
return {
|
|
192
143
|
unsubscribe: () => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
debug("tearing down metadata session due to refCount=0", { roomId: session.roomId });
|
|
201
|
-
this.teardownMetadataSession(session).catch(() => {});
|
|
202
|
-
}
|
|
203
|
-
});
|
|
144
|
+
listener.unsubscribe();
|
|
145
|
+
state.listeners.delete(listener);
|
|
146
|
+
if (!state.listeners.size) {
|
|
147
|
+
state.channel.removeEventListener("message", state.onMessage);
|
|
148
|
+
state.channel.close();
|
|
149
|
+
this.metaState = void 0;
|
|
150
|
+
}
|
|
204
151
|
},
|
|
205
|
-
firstSyncedWithRemote: firstSynced,
|
|
152
|
+
firstSyncedWithRemote: listener.firstSynced,
|
|
206
153
|
get connected() {
|
|
207
|
-
return
|
|
154
|
+
return true;
|
|
208
155
|
}
|
|
209
156
|
};
|
|
210
|
-
ensure.then((session) => {
|
|
211
|
-
session.refCount += 1;
|
|
212
|
-
debug("metadata session refCount incremented", {
|
|
213
|
-
roomId: session.roomId,
|
|
214
|
-
refCount: session.refCount
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
return subscription;
|
|
218
157
|
}
|
|
219
|
-
async syncDoc(docId, doc,
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
docId,
|
|
226
|
-
roomId: session.roomId
|
|
227
|
-
});
|
|
228
|
-
return { ok: true };
|
|
229
|
-
} catch (error) {
|
|
230
|
-
debug("syncDoc failed", {
|
|
231
|
-
docId,
|
|
232
|
-
error
|
|
233
|
-
});
|
|
234
|
-
return { ok: false };
|
|
235
|
-
}
|
|
158
|
+
async syncDoc(docId, doc, _options) {
|
|
159
|
+
const subscription = this.joinDocRoom(docId, doc);
|
|
160
|
+
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
161
|
+
await subscription.firstSyncedWithRemote;
|
|
162
|
+
subscription.unsubscribe();
|
|
163
|
+
return { ok: true };
|
|
236
164
|
}
|
|
237
|
-
joinDocRoom(docId, doc,
|
|
238
|
-
|
|
165
|
+
joinDocRoom(docId, doc, _params) {
|
|
166
|
+
const state = this.ensureDocChannel(docId);
|
|
167
|
+
const { promise, resolve } = deferred();
|
|
168
|
+
const listener = {
|
|
169
|
+
doc,
|
|
170
|
+
muted: false,
|
|
171
|
+
unsubscribe: doc.subscribe(() => {
|
|
172
|
+
if (listener.muted) return;
|
|
173
|
+
const payload = doc.export({ mode: "update" });
|
|
174
|
+
postChannelMessage(state.channel, {
|
|
175
|
+
kind: "doc-update",
|
|
176
|
+
docId,
|
|
177
|
+
from: this.instanceId,
|
|
178
|
+
mode: "update",
|
|
179
|
+
payload
|
|
180
|
+
});
|
|
181
|
+
}),
|
|
182
|
+
resolveFirst: resolve,
|
|
183
|
+
firstSynced: promise
|
|
184
|
+
};
|
|
185
|
+
state.listeners.add(listener);
|
|
186
|
+
postChannelMessage(state.channel, {
|
|
187
|
+
kind: "doc-request",
|
|
239
188
|
docId,
|
|
240
|
-
|
|
241
|
-
hasAuthOverride: Boolean(params?.auth && params.auth.length)
|
|
189
|
+
from: this.instanceId
|
|
242
190
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
191
|
+
postChannelMessage(state.channel, {
|
|
192
|
+
kind: "doc-update",
|
|
193
|
+
docId,
|
|
194
|
+
from: this.instanceId,
|
|
195
|
+
mode: "snapshot",
|
|
196
|
+
payload: doc.export({ mode: "snapshot" })
|
|
197
|
+
});
|
|
198
|
+
queueMicrotask(() => resolve());
|
|
199
|
+
return {
|
|
247
200
|
unsubscribe: () => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
docId,
|
|
252
|
-
roomId: session.roomId,
|
|
253
|
-
refCount: session.refCount
|
|
254
|
-
});
|
|
255
|
-
if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
|
|
256
|
-
});
|
|
201
|
+
listener.unsubscribe();
|
|
202
|
+
state.listeners.delete(listener);
|
|
203
|
+
if (!state.listeners.size) this.teardownDocChannel(docId);
|
|
257
204
|
},
|
|
258
|
-
firstSyncedWithRemote: firstSynced,
|
|
205
|
+
firstSyncedWithRemote: listener.firstSynced,
|
|
259
206
|
get connected() {
|
|
260
|
-
return
|
|
207
|
+
return true;
|
|
261
208
|
}
|
|
262
209
|
};
|
|
263
|
-
ensure.then((session) => {
|
|
264
|
-
session.refCount += 1;
|
|
265
|
-
debug("doc session refCount incremented", {
|
|
266
|
-
docId,
|
|
267
|
-
roomId: session.roomId,
|
|
268
|
-
refCount: session.refCount
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
return subscription;
|
|
272
210
|
}
|
|
273
|
-
|
|
274
|
-
if (this.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
211
|
+
ensureMetaChannel() {
|
|
212
|
+
if (this.metaState) return this.metaState;
|
|
213
|
+
const channel = new (ensureBroadcastChannel())(this.metaChannelName);
|
|
214
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
215
|
+
const onMessage = (event) => {
|
|
216
|
+
const message = event.data;
|
|
217
|
+
if (!message || message.from === this.instanceId) return;
|
|
218
|
+
if (message.kind === "meta-export") for (const entry of listeners) {
|
|
219
|
+
entry.muted = true;
|
|
220
|
+
entry.flock.importJson(message.bundle);
|
|
221
|
+
entry.muted = false;
|
|
222
|
+
entry.resolveFirst();
|
|
223
|
+
}
|
|
224
|
+
else if (message.kind === "meta-request") {
|
|
225
|
+
const first = listeners.values().next().value;
|
|
226
|
+
if (!first) return;
|
|
227
|
+
Promise.resolve(first.flock.exportJson()).then((bundle) => {
|
|
228
|
+
postChannelMessage(channel, {
|
|
229
|
+
kind: "meta-export",
|
|
230
|
+
from: this.instanceId,
|
|
231
|
+
bundle
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
channel.addEventListener("message", onMessage);
|
|
237
|
+
this.metaState = {
|
|
238
|
+
channel,
|
|
239
|
+
listeners,
|
|
240
|
+
onMessage
|
|
241
|
+
};
|
|
242
|
+
return this.metaState;
|
|
289
243
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
roomId: params.roomId,
|
|
318
|
-
crdtAdaptor: adaptor,
|
|
319
|
-
auth: params.auth
|
|
320
|
-
});
|
|
321
|
-
const firstSynced = room.waitForReachingServerVersion();
|
|
322
|
-
firstSynced.then(() => {
|
|
323
|
-
debug("metadata session firstSynced resolved", { roomId: params.roomId });
|
|
324
|
-
}, (error) => {
|
|
325
|
-
debug("metadata session firstSynced rejected", {
|
|
326
|
-
roomId: params.roomId,
|
|
327
|
-
error
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
const session = {
|
|
331
|
-
adaptor,
|
|
332
|
-
room,
|
|
333
|
-
firstSynced,
|
|
334
|
-
flock,
|
|
335
|
-
roomId: params.roomId,
|
|
336
|
-
auth: params.auth,
|
|
337
|
-
refCount: 0
|
|
244
|
+
ensureDocChannel(docId) {
|
|
245
|
+
const existing = this.docStates.get(docId);
|
|
246
|
+
if (existing) return existing;
|
|
247
|
+
const channel = new (ensureBroadcastChannel())(`${this.namespace}-doc-${encodeDocChannelId(docId)}`);
|
|
248
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
249
|
+
const onMessage = (event) => {
|
|
250
|
+
const message = event.data;
|
|
251
|
+
if (!message || message.from === this.instanceId) return;
|
|
252
|
+
if (message.kind === "doc-update") for (const entry of listeners) {
|
|
253
|
+
entry.muted = true;
|
|
254
|
+
entry.doc.import(message.payload);
|
|
255
|
+
entry.muted = false;
|
|
256
|
+
entry.resolveFirst();
|
|
257
|
+
}
|
|
258
|
+
else if (message.kind === "doc-request") {
|
|
259
|
+
const first = listeners.values().next().value;
|
|
260
|
+
if (!first) return;
|
|
261
|
+
const payload = message.docId === docId ? first.doc.export({ mode: "snapshot" }) : void 0;
|
|
262
|
+
if (!payload) return;
|
|
263
|
+
postChannelMessage(channel, {
|
|
264
|
+
kind: "doc-update",
|
|
265
|
+
docId,
|
|
266
|
+
from: this.instanceId,
|
|
267
|
+
mode: "snapshot",
|
|
268
|
+
payload
|
|
269
|
+
});
|
|
270
|
+
}
|
|
338
271
|
};
|
|
339
|
-
|
|
340
|
-
|
|
272
|
+
channel.addEventListener("message", onMessage);
|
|
273
|
+
const state = {
|
|
274
|
+
channel,
|
|
275
|
+
listeners,
|
|
276
|
+
onMessage
|
|
277
|
+
};
|
|
278
|
+
this.docStates.set(docId, state);
|
|
279
|
+
return state;
|
|
341
280
|
}
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
if (!
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
await room.leave();
|
|
350
|
-
debug("metadata room left", { roomId: target.roomId });
|
|
351
|
-
} catch (error) {
|
|
352
|
-
debug("metadata room leave failed; destroying", {
|
|
353
|
-
roomId: target.roomId,
|
|
354
|
-
error
|
|
355
|
-
});
|
|
356
|
-
await room.destroy().catch(() => {});
|
|
357
|
-
}
|
|
358
|
-
adaptor.destroy();
|
|
359
|
-
debug("metadata session destroyed", { roomId: target.roomId });
|
|
360
|
-
}
|
|
361
|
-
async ensureDocSession(docId, doc, params) {
|
|
362
|
-
debug("ensureDocSession invoked", { docId });
|
|
363
|
-
const client = this.ensureClient();
|
|
364
|
-
await client.waitConnected();
|
|
365
|
-
debug("websocket client ready for doc session", {
|
|
366
|
-
docId,
|
|
367
|
-
status: client.getStatus()
|
|
368
|
-
});
|
|
369
|
-
const existing = this.docSessions.get(docId);
|
|
370
|
-
const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
|
|
371
|
-
const roomId = normalizeRoomId(params.roomId, derivedRoomId);
|
|
372
|
-
const auth = params.auth ?? this.options.docAuth?.(docId);
|
|
373
|
-
debug("doc session params resolved", {
|
|
374
|
-
docId,
|
|
375
|
-
roomId,
|
|
376
|
-
hasAuth: Boolean(auth && auth.length)
|
|
377
|
-
});
|
|
378
|
-
if (existing && existing.doc === doc && existing.roomId === roomId) {
|
|
379
|
-
debug("reusing doc session", {
|
|
380
|
-
docId,
|
|
381
|
-
roomId,
|
|
382
|
-
refCount: existing.refCount
|
|
383
|
-
});
|
|
384
|
-
return existing;
|
|
385
|
-
}
|
|
386
|
-
if (existing) {
|
|
387
|
-
debug("doc session mismatch; leaving existing session", {
|
|
388
|
-
docId,
|
|
389
|
-
previousRoomId: existing.roomId,
|
|
390
|
-
nextRoomId: roomId
|
|
391
|
-
});
|
|
392
|
-
await this.leaveDocSession(docId).catch(() => {});
|
|
393
|
-
}
|
|
394
|
-
const adaptor = new loro_adaptors.LoroAdaptor(doc);
|
|
395
|
-
debug("joining doc room", {
|
|
396
|
-
docId,
|
|
397
|
-
roomId,
|
|
398
|
-
hasAuth: Boolean(auth && auth.length)
|
|
399
|
-
});
|
|
400
|
-
const room = await client.join({
|
|
401
|
-
roomId,
|
|
402
|
-
crdtAdaptor: adaptor,
|
|
403
|
-
auth
|
|
404
|
-
});
|
|
405
|
-
const firstSynced = room.waitForReachingServerVersion();
|
|
406
|
-
firstSynced.then(() => {
|
|
407
|
-
debug("doc session firstSynced resolved", {
|
|
408
|
-
docId,
|
|
409
|
-
roomId
|
|
410
|
-
});
|
|
411
|
-
}, (error) => {
|
|
412
|
-
debug("doc session firstSynced rejected", {
|
|
413
|
-
docId,
|
|
414
|
-
roomId,
|
|
415
|
-
error
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
const session = {
|
|
419
|
-
adaptor,
|
|
420
|
-
room,
|
|
421
|
-
firstSynced,
|
|
422
|
-
doc,
|
|
423
|
-
roomId,
|
|
424
|
-
refCount: 0
|
|
425
|
-
};
|
|
426
|
-
this.docSessions.set(docId, session);
|
|
427
|
-
return session;
|
|
428
|
-
}
|
|
429
|
-
async leaveDocSession(docId) {
|
|
430
|
-
const session = this.docSessions.get(docId);
|
|
431
|
-
if (!session) {
|
|
432
|
-
debug("leaveDocSession invoked but no session found", { docId });
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
this.docSessions.delete(docId);
|
|
436
|
-
debug("leaving doc session", {
|
|
437
|
-
docId,
|
|
438
|
-
roomId: session.roomId
|
|
439
|
-
});
|
|
440
|
-
try {
|
|
441
|
-
await session.room.leave();
|
|
442
|
-
debug("doc room left", {
|
|
443
|
-
docId,
|
|
444
|
-
roomId: session.roomId
|
|
445
|
-
});
|
|
446
|
-
} catch (error) {
|
|
447
|
-
debug("doc room leave failed; destroying", {
|
|
448
|
-
docId,
|
|
449
|
-
roomId: session.roomId,
|
|
450
|
-
error
|
|
451
|
-
});
|
|
452
|
-
await session.room.destroy().catch(() => {});
|
|
453
|
-
}
|
|
454
|
-
session.adaptor.destroy();
|
|
455
|
-
debug("doc session destroyed", {
|
|
456
|
-
docId,
|
|
457
|
-
roomId: session.roomId
|
|
458
|
-
});
|
|
281
|
+
teardownDocChannel(docId) {
|
|
282
|
+
const state = this.docStates.get(docId);
|
|
283
|
+
if (!state) return;
|
|
284
|
+
for (const entry of state.listeners) entry.unsubscribe();
|
|
285
|
+
state.channel.removeEventListener("message", state.onMessage);
|
|
286
|
+
state.channel.close();
|
|
287
|
+
this.docStates.delete(docId);
|
|
459
288
|
}
|
|
460
289
|
};
|
|
461
290
|
|
|
462
291
|
//#endregion
|
|
463
|
-
//#region src/
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
if (typeof
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (typeof
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
function encodeDocChannelId(docId) {
|
|
482
|
-
try {
|
|
483
|
-
return encodeURIComponent(docId);
|
|
292
|
+
//#region src/storage/indexeddb.ts
|
|
293
|
+
const DEFAULT_DB_NAME = "loro-repo";
|
|
294
|
+
const DEFAULT_DB_VERSION = 1;
|
|
295
|
+
const DEFAULT_DOC_STORE = "docs";
|
|
296
|
+
const DEFAULT_META_STORE = "meta";
|
|
297
|
+
const DEFAULT_ASSET_STORE = "assets";
|
|
298
|
+
const DEFAULT_DOC_UPDATE_STORE = "doc-updates";
|
|
299
|
+
const DEFAULT_META_KEY = "snapshot";
|
|
300
|
+
const textDecoder$1 = new TextDecoder();
|
|
301
|
+
function describeUnknown(cause) {
|
|
302
|
+
if (typeof cause === "string") return cause;
|
|
303
|
+
if (typeof cause === "number" || typeof cause === "boolean") return String(cause);
|
|
304
|
+
if (typeof cause === "bigint") return cause.toString();
|
|
305
|
+
if (typeof cause === "symbol") return cause.description ?? cause.toString();
|
|
306
|
+
if (typeof cause === "function") return `[function ${cause.name ?? "anonymous"}]`;
|
|
307
|
+
if (cause && typeof cause === "object") try {
|
|
308
|
+
return JSON.stringify(cause);
|
|
484
309
|
} catch {
|
|
485
|
-
return
|
|
310
|
+
return "[object]";
|
|
486
311
|
}
|
|
312
|
+
return String(cause);
|
|
487
313
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
connected = false;
|
|
500
|
-
metaState;
|
|
501
|
-
docStates = /* @__PURE__ */ new Map();
|
|
314
|
+
var IndexedDBStorageAdaptor = class {
|
|
315
|
+
idb;
|
|
316
|
+
dbName;
|
|
317
|
+
version;
|
|
318
|
+
docStore;
|
|
319
|
+
docUpdateStore;
|
|
320
|
+
metaStore;
|
|
321
|
+
assetStore;
|
|
322
|
+
metaKey;
|
|
323
|
+
dbPromise;
|
|
324
|
+
closed = false;
|
|
502
325
|
constructor(options = {}) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
this.
|
|
326
|
+
const idbFactory = globalThis.indexedDB;
|
|
327
|
+
if (!idbFactory) throw new Error("IndexedDB is not available in this environment");
|
|
328
|
+
this.idb = idbFactory;
|
|
329
|
+
this.dbName = options.dbName ?? DEFAULT_DB_NAME;
|
|
330
|
+
this.version = options.version ?? DEFAULT_DB_VERSION;
|
|
331
|
+
this.docStore = options.docStoreName ?? DEFAULT_DOC_STORE;
|
|
332
|
+
this.docUpdateStore = options.docUpdateStoreName ?? DEFAULT_DOC_UPDATE_STORE;
|
|
333
|
+
this.metaStore = options.metaStoreName ?? DEFAULT_META_STORE;
|
|
334
|
+
this.assetStore = options.assetStoreName ?? DEFAULT_ASSET_STORE;
|
|
335
|
+
this.metaKey = options.metaKey ?? DEFAULT_META_KEY;
|
|
506
336
|
}
|
|
507
|
-
async
|
|
508
|
-
|
|
337
|
+
async save(payload) {
|
|
338
|
+
const db = await this.ensureDb();
|
|
339
|
+
switch (payload.type) {
|
|
340
|
+
case "doc-snapshot": {
|
|
341
|
+
const snapshot = payload.snapshot.slice();
|
|
342
|
+
await this.storeMergedSnapshot(db, payload.docId, snapshot);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case "doc-update": {
|
|
346
|
+
const update = payload.update.slice();
|
|
347
|
+
await this.appendDocUpdate(db, payload.docId, update);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "asset": {
|
|
351
|
+
const bytes = payload.data.slice();
|
|
352
|
+
await this.putBinary(db, this.assetStore, payload.assetId, bytes);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
case "meta": {
|
|
356
|
+
const bytes = payload.update.slice();
|
|
357
|
+
await this.putBinary(db, this.metaStore, this.metaKey, bytes);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
default: throw new Error("Unsupported storage payload type");
|
|
361
|
+
}
|
|
509
362
|
}
|
|
510
|
-
async
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
363
|
+
async deleteAsset(assetId) {
|
|
364
|
+
const db = await this.ensureDb();
|
|
365
|
+
await this.deleteKey(db, this.assetStore, assetId);
|
|
366
|
+
}
|
|
367
|
+
async loadDoc(docId) {
|
|
368
|
+
const db = await this.ensureDb();
|
|
369
|
+
const snapshot = await this.getBinaryFromDb(db, this.docStore, docId);
|
|
370
|
+
const pendingUpdates = await this.getDocUpdates(db, docId);
|
|
371
|
+
if (!snapshot && pendingUpdates.length === 0) return;
|
|
372
|
+
let doc;
|
|
373
|
+
try {
|
|
374
|
+
doc = snapshot ? loro_crdt.LoroDoc.fromSnapshot(snapshot) : new loro_crdt.LoroDoc();
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw this.createError(`Failed to hydrate document snapshot for "${docId}"`, error);
|
|
516
377
|
}
|
|
517
|
-
|
|
518
|
-
|
|
378
|
+
let appliedUpdates = false;
|
|
379
|
+
for (const update of pendingUpdates) try {
|
|
380
|
+
doc.import(update);
|
|
381
|
+
appliedUpdates = true;
|
|
382
|
+
} catch (error) {
|
|
383
|
+
throw this.createError(`Failed to apply queued document update for "${docId}"`, error);
|
|
384
|
+
}
|
|
385
|
+
if (appliedUpdates) {
|
|
386
|
+
let consolidated;
|
|
387
|
+
try {
|
|
388
|
+
consolidated = doc.export({ mode: "snapshot" });
|
|
389
|
+
} catch (error) {
|
|
390
|
+
throw this.createError(`Failed to export consolidated snapshot for "${docId}"`, error);
|
|
391
|
+
}
|
|
392
|
+
await this.writeSnapshot(db, docId, consolidated);
|
|
393
|
+
await this.clearDocUpdates(db, docId);
|
|
394
|
+
}
|
|
395
|
+
return doc;
|
|
519
396
|
}
|
|
520
|
-
|
|
521
|
-
|
|
397
|
+
async loadMeta() {
|
|
398
|
+
const bytes = await this.getBinary(this.metaStore, this.metaKey);
|
|
399
|
+
if (!bytes) return void 0;
|
|
400
|
+
try {
|
|
401
|
+
const json = textDecoder$1.decode(bytes);
|
|
402
|
+
const bundle = JSON.parse(json);
|
|
403
|
+
const flock = new __loro_dev_flock.Flock();
|
|
404
|
+
flock.importJson(bundle);
|
|
405
|
+
return flock;
|
|
406
|
+
} catch (error) {
|
|
407
|
+
throw this.createError("Failed to hydrate metadata snapshot", error);
|
|
408
|
+
}
|
|
522
409
|
}
|
|
523
|
-
async
|
|
524
|
-
|
|
525
|
-
subscription.firstSyncedWithRemote.catch(() => void 0);
|
|
526
|
-
await subscription.firstSyncedWithRemote;
|
|
527
|
-
subscription.unsubscribe();
|
|
528
|
-
return { ok: true };
|
|
410
|
+
async loadAsset(assetId) {
|
|
411
|
+
return await this.getBinary(this.assetStore, assetId) ?? void 0;
|
|
529
412
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
resolveFirst: resolve,
|
|
547
|
-
firstSynced: promise
|
|
548
|
-
};
|
|
549
|
-
state.listeners.add(listener);
|
|
550
|
-
postChannelMessage(state.channel, {
|
|
551
|
-
kind: "meta-request",
|
|
552
|
-
from: this.instanceId
|
|
553
|
-
});
|
|
554
|
-
Promise.resolve(flock.exportJson()).then((bundle) => {
|
|
555
|
-
postChannelMessage(state.channel, {
|
|
556
|
-
kind: "meta-export",
|
|
557
|
-
from: this.instanceId,
|
|
558
|
-
bundle
|
|
413
|
+
async close() {
|
|
414
|
+
this.closed = true;
|
|
415
|
+
const db = await this.dbPromise;
|
|
416
|
+
if (db) db.close();
|
|
417
|
+
this.dbPromise = void 0;
|
|
418
|
+
}
|
|
419
|
+
async ensureDb() {
|
|
420
|
+
if (this.closed) throw new Error("IndexedDBStorageAdaptor has been closed");
|
|
421
|
+
if (!this.dbPromise) this.dbPromise = new Promise((resolve, reject) => {
|
|
422
|
+
const request = this.idb.open(this.dbName, this.version);
|
|
423
|
+
request.addEventListener("upgradeneeded", () => {
|
|
424
|
+
const db = request.result;
|
|
425
|
+
this.ensureStore(db, this.docStore);
|
|
426
|
+
this.ensureStore(db, this.docUpdateStore);
|
|
427
|
+
this.ensureStore(db, this.metaStore);
|
|
428
|
+
this.ensureStore(db, this.assetStore);
|
|
559
429
|
});
|
|
430
|
+
request.addEventListener("success", () => resolve(request.result), { once: true });
|
|
431
|
+
request.addEventListener("error", () => {
|
|
432
|
+
reject(this.createError(`Failed to open IndexedDB database "${this.dbName}"`, request.error));
|
|
433
|
+
}, { once: true });
|
|
560
434
|
});
|
|
561
|
-
|
|
562
|
-
return {
|
|
563
|
-
unsubscribe: () => {
|
|
564
|
-
listener.unsubscribe();
|
|
565
|
-
state.listeners.delete(listener);
|
|
566
|
-
if (!state.listeners.size) {
|
|
567
|
-
state.channel.removeEventListener("message", state.onMessage);
|
|
568
|
-
state.channel.close();
|
|
569
|
-
this.metaState = void 0;
|
|
570
|
-
}
|
|
571
|
-
},
|
|
572
|
-
firstSyncedWithRemote: listener.firstSynced,
|
|
573
|
-
get connected() {
|
|
574
|
-
return true;
|
|
575
|
-
}
|
|
576
|
-
};
|
|
435
|
+
return this.dbPromise;
|
|
577
436
|
}
|
|
578
|
-
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
subscription.unsubscribe();
|
|
583
|
-
return { ok: true };
|
|
437
|
+
ensureStore(db, storeName) {
|
|
438
|
+
const names = db.objectStoreNames;
|
|
439
|
+
if (this.storeExists(names, storeName)) return;
|
|
440
|
+
db.createObjectStore(storeName);
|
|
584
441
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
from: this.instanceId,
|
|
598
|
-
mode: "update",
|
|
599
|
-
payload
|
|
600
|
-
});
|
|
601
|
-
}),
|
|
602
|
-
resolveFirst: resolve,
|
|
603
|
-
firstSynced: promise
|
|
604
|
-
};
|
|
605
|
-
state.listeners.add(listener);
|
|
606
|
-
postChannelMessage(state.channel, {
|
|
607
|
-
kind: "doc-request",
|
|
608
|
-
docId,
|
|
609
|
-
from: this.instanceId
|
|
442
|
+
storeExists(names, storeName) {
|
|
443
|
+
if (typeof names.contains === "function") return names.contains(storeName);
|
|
444
|
+
const length = names.length ?? 0;
|
|
445
|
+
for (let index = 0; index < length; index += 1) if (names.item?.(index) === storeName) return true;
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
async storeMergedSnapshot(db, docId, incoming) {
|
|
449
|
+
await this.runInTransaction(db, this.docStore, "readwrite", async (store) => {
|
|
450
|
+
const existingRaw = await this.wrapRequest(store.get(docId), "read");
|
|
451
|
+
const existing = await this.normalizeBinary(existingRaw);
|
|
452
|
+
const merged = this.mergeSnapshots(docId, existing, incoming);
|
|
453
|
+
await this.wrapRequest(store.put(merged, docId), "write");
|
|
610
454
|
});
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
455
|
+
}
|
|
456
|
+
mergeSnapshots(docId, existing, incoming) {
|
|
457
|
+
try {
|
|
458
|
+
const doc = existing ? loro_crdt.LoroDoc.fromSnapshot(existing) : new loro_crdt.LoroDoc();
|
|
459
|
+
doc.import(incoming);
|
|
460
|
+
return doc.export({ mode: "snapshot" });
|
|
461
|
+
} catch (error) {
|
|
462
|
+
throw this.createError(`Failed to merge snapshot for "${docId}"`, error);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async appendDocUpdate(db, docId, update) {
|
|
466
|
+
await this.runInTransaction(db, this.docUpdateStore, "readwrite", async (store) => {
|
|
467
|
+
const raw = await this.wrapRequest(store.get(docId), "read");
|
|
468
|
+
const queue = await this.normalizeUpdateQueue(raw);
|
|
469
|
+
queue.push(update.slice());
|
|
470
|
+
await this.wrapRequest(store.put({ updates: queue }, docId), "write");
|
|
617
471
|
});
|
|
618
|
-
queueMicrotask(() => resolve());
|
|
619
|
-
return {
|
|
620
|
-
unsubscribe: () => {
|
|
621
|
-
listener.unsubscribe();
|
|
622
|
-
state.listeners.delete(listener);
|
|
623
|
-
if (!state.listeners.size) this.teardownDocChannel(docId);
|
|
624
|
-
},
|
|
625
|
-
firstSyncedWithRemote: listener.firstSynced,
|
|
626
|
-
get connected() {
|
|
627
|
-
return true;
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
472
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
635
|
-
const onMessage = (event) => {
|
|
636
|
-
const message = event.data;
|
|
637
|
-
if (!message || message.from === this.instanceId) return;
|
|
638
|
-
if (message.kind === "meta-export") for (const entry of listeners) {
|
|
639
|
-
entry.muted = true;
|
|
640
|
-
entry.flock.importJson(message.bundle);
|
|
641
|
-
entry.muted = false;
|
|
642
|
-
entry.resolveFirst();
|
|
643
|
-
}
|
|
644
|
-
else if (message.kind === "meta-request") {
|
|
645
|
-
const first = listeners.values().next().value;
|
|
646
|
-
if (!first) return;
|
|
647
|
-
Promise.resolve(first.flock.exportJson()).then((bundle) => {
|
|
648
|
-
postChannelMessage(channel, {
|
|
649
|
-
kind: "meta-export",
|
|
650
|
-
from: this.instanceId,
|
|
651
|
-
bundle
|
|
652
|
-
});
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
};
|
|
656
|
-
channel.addEventListener("message", onMessage);
|
|
657
|
-
this.metaState = {
|
|
658
|
-
channel,
|
|
659
|
-
listeners,
|
|
660
|
-
onMessage
|
|
661
|
-
};
|
|
662
|
-
return this.metaState;
|
|
473
|
+
async getDocUpdates(db, docId) {
|
|
474
|
+
const raw = await this.runInTransaction(db, this.docUpdateStore, "readonly", (store) => this.wrapRequest(store.get(docId), "read"));
|
|
475
|
+
return this.normalizeUpdateQueue(raw);
|
|
663
476
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if (existing) return existing;
|
|
667
|
-
const channel = new (ensureBroadcastChannel())(`${this.namespace}-doc-${encodeDocChannelId(docId)}`);
|
|
668
|
-
const listeners = /* @__PURE__ */ new Set();
|
|
669
|
-
const onMessage = (event) => {
|
|
670
|
-
const message = event.data;
|
|
671
|
-
if (!message || message.from === this.instanceId) return;
|
|
672
|
-
if (message.kind === "doc-update") for (const entry of listeners) {
|
|
673
|
-
entry.muted = true;
|
|
674
|
-
entry.doc.import(message.payload);
|
|
675
|
-
entry.muted = false;
|
|
676
|
-
entry.resolveFirst();
|
|
677
|
-
}
|
|
678
|
-
else if (message.kind === "doc-request") {
|
|
679
|
-
const first = listeners.values().next().value;
|
|
680
|
-
if (!first) return;
|
|
681
|
-
const payload = message.docId === docId ? first.doc.export({ mode: "snapshot" }) : void 0;
|
|
682
|
-
if (!payload) return;
|
|
683
|
-
postChannelMessage(channel, {
|
|
684
|
-
kind: "doc-update",
|
|
685
|
-
docId,
|
|
686
|
-
from: this.instanceId,
|
|
687
|
-
mode: "snapshot",
|
|
688
|
-
payload
|
|
689
|
-
});
|
|
690
|
-
}
|
|
691
|
-
};
|
|
692
|
-
channel.addEventListener("message", onMessage);
|
|
693
|
-
const state = {
|
|
694
|
-
channel,
|
|
695
|
-
listeners,
|
|
696
|
-
onMessage
|
|
697
|
-
};
|
|
698
|
-
this.docStates.set(docId, state);
|
|
699
|
-
return state;
|
|
477
|
+
async clearDocUpdates(db, docId) {
|
|
478
|
+
await this.runInTransaction(db, this.docUpdateStore, "readwrite", (store) => this.wrapRequest(store.delete(docId), "delete"));
|
|
700
479
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
if (!state) return;
|
|
704
|
-
for (const entry of state.listeners) entry.unsubscribe();
|
|
705
|
-
state.channel.removeEventListener("message", state.onMessage);
|
|
706
|
-
state.channel.close();
|
|
707
|
-
this.docStates.delete(docId);
|
|
480
|
+
async writeSnapshot(db, docId, snapshot) {
|
|
481
|
+
await this.putBinary(db, this.docStore, docId, snapshot.slice());
|
|
708
482
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
//#region src/storage/indexeddb.ts
|
|
713
|
-
const DEFAULT_DB_NAME = "loro-repo";
|
|
714
|
-
const DEFAULT_DB_VERSION = 1;
|
|
715
|
-
const DEFAULT_DOC_STORE = "docs";
|
|
716
|
-
const DEFAULT_META_STORE = "meta";
|
|
717
|
-
const DEFAULT_ASSET_STORE = "assets";
|
|
718
|
-
const DEFAULT_DOC_UPDATE_STORE = "doc-updates";
|
|
719
|
-
const DEFAULT_META_KEY = "snapshot";
|
|
720
|
-
const textDecoder$1 = new TextDecoder();
|
|
721
|
-
function describeUnknown(cause) {
|
|
722
|
-
if (typeof cause === "string") return cause;
|
|
723
|
-
if (typeof cause === "number" || typeof cause === "boolean") return String(cause);
|
|
724
|
-
if (typeof cause === "bigint") return cause.toString();
|
|
725
|
-
if (typeof cause === "symbol") return cause.description ?? cause.toString();
|
|
726
|
-
if (typeof cause === "function") return `[function ${cause.name ?? "anonymous"}]`;
|
|
727
|
-
if (cause && typeof cause === "object") try {
|
|
728
|
-
return JSON.stringify(cause);
|
|
729
|
-
} catch {
|
|
730
|
-
return "[object]";
|
|
731
|
-
}
|
|
732
|
-
return String(cause);
|
|
733
|
-
}
|
|
734
|
-
var IndexedDBStorageAdaptor = class {
|
|
735
|
-
idb;
|
|
736
|
-
dbName;
|
|
737
|
-
version;
|
|
738
|
-
docStore;
|
|
739
|
-
docUpdateStore;
|
|
740
|
-
metaStore;
|
|
741
|
-
assetStore;
|
|
742
|
-
metaKey;
|
|
743
|
-
dbPromise;
|
|
744
|
-
closed = false;
|
|
745
|
-
constructor(options = {}) {
|
|
746
|
-
const idbFactory = globalThis.indexedDB;
|
|
747
|
-
if (!idbFactory) throw new Error("IndexedDB is not available in this environment");
|
|
748
|
-
this.idb = idbFactory;
|
|
749
|
-
this.dbName = options.dbName ?? DEFAULT_DB_NAME;
|
|
750
|
-
this.version = options.version ?? DEFAULT_DB_VERSION;
|
|
751
|
-
this.docStore = options.docStoreName ?? DEFAULT_DOC_STORE;
|
|
752
|
-
this.docUpdateStore = options.docUpdateStoreName ?? DEFAULT_DOC_UPDATE_STORE;
|
|
753
|
-
this.metaStore = options.metaStoreName ?? DEFAULT_META_STORE;
|
|
754
|
-
this.assetStore = options.assetStoreName ?? DEFAULT_ASSET_STORE;
|
|
755
|
-
this.metaKey = options.metaKey ?? DEFAULT_META_KEY;
|
|
756
|
-
}
|
|
757
|
-
async save(payload) {
|
|
758
|
-
const db = await this.ensureDb();
|
|
759
|
-
switch (payload.type) {
|
|
760
|
-
case "doc-snapshot": {
|
|
761
|
-
const snapshot = payload.snapshot.slice();
|
|
762
|
-
await this.storeMergedSnapshot(db, payload.docId, snapshot);
|
|
763
|
-
break;
|
|
764
|
-
}
|
|
765
|
-
case "doc-update": {
|
|
766
|
-
const update = payload.update.slice();
|
|
767
|
-
await this.appendDocUpdate(db, payload.docId, update);
|
|
768
|
-
break;
|
|
769
|
-
}
|
|
770
|
-
case "asset": {
|
|
771
|
-
const bytes = payload.data.slice();
|
|
772
|
-
await this.putBinary(db, this.assetStore, payload.assetId, bytes);
|
|
773
|
-
break;
|
|
774
|
-
}
|
|
775
|
-
case "meta": {
|
|
776
|
-
const bytes = payload.update.slice();
|
|
777
|
-
await this.putBinary(db, this.metaStore, this.metaKey, bytes);
|
|
778
|
-
break;
|
|
779
|
-
}
|
|
780
|
-
default: throw new Error("Unsupported storage payload type");
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
async deleteAsset(assetId) {
|
|
784
|
-
const db = await this.ensureDb();
|
|
785
|
-
await this.deleteKey(db, this.assetStore, assetId);
|
|
786
|
-
}
|
|
787
|
-
async loadDoc(docId) {
|
|
788
|
-
const db = await this.ensureDb();
|
|
789
|
-
const snapshot = await this.getBinaryFromDb(db, this.docStore, docId);
|
|
790
|
-
const pendingUpdates = await this.getDocUpdates(db, docId);
|
|
791
|
-
if (!snapshot && pendingUpdates.length === 0) return;
|
|
792
|
-
let doc;
|
|
793
|
-
try {
|
|
794
|
-
doc = snapshot ? loro_crdt.LoroDoc.fromSnapshot(snapshot) : new loro_crdt.LoroDoc();
|
|
795
|
-
} catch (error) {
|
|
796
|
-
throw this.createError(`Failed to hydrate document snapshot for "${docId}"`, error);
|
|
797
|
-
}
|
|
798
|
-
let appliedUpdates = false;
|
|
799
|
-
for (const update of pendingUpdates) try {
|
|
800
|
-
doc.import(update);
|
|
801
|
-
appliedUpdates = true;
|
|
802
|
-
} catch (error) {
|
|
803
|
-
throw this.createError(`Failed to apply queued document update for "${docId}"`, error);
|
|
804
|
-
}
|
|
805
|
-
if (appliedUpdates) {
|
|
806
|
-
let consolidated;
|
|
807
|
-
try {
|
|
808
|
-
consolidated = doc.export({ mode: "snapshot" });
|
|
809
|
-
} catch (error) {
|
|
810
|
-
throw this.createError(`Failed to export consolidated snapshot for "${docId}"`, error);
|
|
811
|
-
}
|
|
812
|
-
await this.writeSnapshot(db, docId, consolidated);
|
|
813
|
-
await this.clearDocUpdates(db, docId);
|
|
814
|
-
}
|
|
815
|
-
return doc;
|
|
816
|
-
}
|
|
817
|
-
async loadMeta() {
|
|
818
|
-
const bytes = await this.getBinary(this.metaStore, this.metaKey);
|
|
819
|
-
if (!bytes) return void 0;
|
|
820
|
-
try {
|
|
821
|
-
const json = textDecoder$1.decode(bytes);
|
|
822
|
-
const bundle = JSON.parse(json);
|
|
823
|
-
const flock = new __loro_dev_flock.Flock();
|
|
824
|
-
flock.importJson(bundle);
|
|
825
|
-
return flock;
|
|
826
|
-
} catch (error) {
|
|
827
|
-
throw this.createError("Failed to hydrate metadata snapshot", error);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
async loadAsset(assetId) {
|
|
831
|
-
return await this.getBinary(this.assetStore, assetId) ?? void 0;
|
|
832
|
-
}
|
|
833
|
-
async close() {
|
|
834
|
-
this.closed = true;
|
|
835
|
-
const db = await this.dbPromise;
|
|
836
|
-
if (db) db.close();
|
|
837
|
-
this.dbPromise = void 0;
|
|
838
|
-
}
|
|
839
|
-
async ensureDb() {
|
|
840
|
-
if (this.closed) throw new Error("IndexedDBStorageAdaptor has been closed");
|
|
841
|
-
if (!this.dbPromise) this.dbPromise = new Promise((resolve, reject) => {
|
|
842
|
-
const request = this.idb.open(this.dbName, this.version);
|
|
843
|
-
request.addEventListener("upgradeneeded", () => {
|
|
844
|
-
const db = request.result;
|
|
845
|
-
this.ensureStore(db, this.docStore);
|
|
846
|
-
this.ensureStore(db, this.docUpdateStore);
|
|
847
|
-
this.ensureStore(db, this.metaStore);
|
|
848
|
-
this.ensureStore(db, this.assetStore);
|
|
849
|
-
});
|
|
850
|
-
request.addEventListener("success", () => resolve(request.result), { once: true });
|
|
851
|
-
request.addEventListener("error", () => {
|
|
852
|
-
reject(this.createError(`Failed to open IndexedDB database "${this.dbName}"`, request.error));
|
|
853
|
-
}, { once: true });
|
|
854
|
-
});
|
|
855
|
-
return this.dbPromise;
|
|
856
|
-
}
|
|
857
|
-
ensureStore(db, storeName) {
|
|
858
|
-
const names = db.objectStoreNames;
|
|
859
|
-
if (this.storeExists(names, storeName)) return;
|
|
860
|
-
db.createObjectStore(storeName);
|
|
861
|
-
}
|
|
862
|
-
storeExists(names, storeName) {
|
|
863
|
-
if (typeof names.contains === "function") return names.contains(storeName);
|
|
864
|
-
const length = names.length ?? 0;
|
|
865
|
-
for (let index = 0; index < length; index += 1) if (names.item?.(index) === storeName) return true;
|
|
866
|
-
return false;
|
|
867
|
-
}
|
|
868
|
-
async storeMergedSnapshot(db, docId, incoming) {
|
|
869
|
-
await this.runInTransaction(db, this.docStore, "readwrite", async (store) => {
|
|
870
|
-
const existingRaw = await this.wrapRequest(store.get(docId), "read");
|
|
871
|
-
const existing = await this.normalizeBinary(existingRaw);
|
|
872
|
-
const merged = this.mergeSnapshots(docId, existing, incoming);
|
|
873
|
-
await this.wrapRequest(store.put(merged, docId), "write");
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
mergeSnapshots(docId, existing, incoming) {
|
|
877
|
-
try {
|
|
878
|
-
const doc = existing ? loro_crdt.LoroDoc.fromSnapshot(existing) : new loro_crdt.LoroDoc();
|
|
879
|
-
doc.import(incoming);
|
|
880
|
-
return doc.export({ mode: "snapshot" });
|
|
881
|
-
} catch (error) {
|
|
882
|
-
throw this.createError(`Failed to merge snapshot for "${docId}"`, error);
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
async appendDocUpdate(db, docId, update) {
|
|
886
|
-
await this.runInTransaction(db, this.docUpdateStore, "readwrite", async (store) => {
|
|
887
|
-
const raw = await this.wrapRequest(store.get(docId), "read");
|
|
888
|
-
const queue = await this.normalizeUpdateQueue(raw);
|
|
889
|
-
queue.push(update.slice());
|
|
890
|
-
await this.wrapRequest(store.put({ updates: queue }, docId), "write");
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
async getDocUpdates(db, docId) {
|
|
894
|
-
const raw = await this.runInTransaction(db, this.docUpdateStore, "readonly", (store) => this.wrapRequest(store.get(docId), "read"));
|
|
895
|
-
return this.normalizeUpdateQueue(raw);
|
|
896
|
-
}
|
|
897
|
-
async clearDocUpdates(db, docId) {
|
|
898
|
-
await this.runInTransaction(db, this.docUpdateStore, "readwrite", (store) => this.wrapRequest(store.delete(docId), "delete"));
|
|
899
|
-
}
|
|
900
|
-
async writeSnapshot(db, docId, snapshot) {
|
|
901
|
-
await this.putBinary(db, this.docStore, docId, snapshot.slice());
|
|
902
|
-
}
|
|
903
|
-
async getBinaryFromDb(db, storeName, key) {
|
|
904
|
-
const value = await this.runInTransaction(db, storeName, "readonly", (store) => this.wrapRequest(store.get(key), "read"));
|
|
905
|
-
return this.normalizeBinary(value);
|
|
483
|
+
async getBinaryFromDb(db, storeName, key) {
|
|
484
|
+
const value = await this.runInTransaction(db, storeName, "readonly", (store) => this.wrapRequest(store.get(key), "read"));
|
|
485
|
+
return this.normalizeBinary(value);
|
|
906
486
|
}
|
|
907
487
|
async normalizeUpdateQueue(value) {
|
|
908
488
|
if (value == null) return [];
|
|
@@ -1094,22 +674,445 @@ async function removeIfExists(filePath) {
|
|
|
1094
674
|
if (error.code === "ENOENT") return;
|
|
1095
675
|
throw error;
|
|
1096
676
|
}
|
|
1097
|
-
}
|
|
1098
|
-
async function listFiles(dir) {
|
|
1099
|
-
try {
|
|
1100
|
-
return (await node_fs.promises.readdir(dir)).sort();
|
|
1101
|
-
} catch (error) {
|
|
1102
|
-
if (error.code === "ENOENT") return [];
|
|
1103
|
-
throw error;
|
|
677
|
+
}
|
|
678
|
+
async function listFiles(dir) {
|
|
679
|
+
try {
|
|
680
|
+
return (await node_fs.promises.readdir(dir)).sort();
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (error.code === "ENOENT") return [];
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async function writeFileAtomic(targetPath, data) {
|
|
687
|
+
const dir = node_path.dirname(targetPath);
|
|
688
|
+
await ensureDir(dir);
|
|
689
|
+
const tempPath = node_path.join(dir, `.tmp-${(0, node_crypto.randomUUID)()}`);
|
|
690
|
+
await node_fs.promises.writeFile(tempPath, data);
|
|
691
|
+
await node_fs.promises.rename(tempPath, targetPath);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
//#endregion
|
|
695
|
+
//#region src/internal/debug.ts
|
|
696
|
+
const getEnv = () => {
|
|
697
|
+
if (typeof globalThis !== "object" || globalThis === null) return;
|
|
698
|
+
return globalThis.process?.env;
|
|
699
|
+
};
|
|
700
|
+
const rawNamespaceConfig = (getEnv()?.LORO_REPO_DEBUG ?? "").trim();
|
|
701
|
+
const normalizedNamespaces = rawNamespaceConfig.length > 0 ? rawNamespaceConfig.split(/[\s,]+/).map((token) => token.toLowerCase()).filter(Boolean) : [];
|
|
702
|
+
const wildcardTokens = new Set([
|
|
703
|
+
"*",
|
|
704
|
+
"1",
|
|
705
|
+
"true",
|
|
706
|
+
"all"
|
|
707
|
+
]);
|
|
708
|
+
const namespaceSet = new Set(normalizedNamespaces);
|
|
709
|
+
const hasWildcard = namespaceSet.size > 0 && normalizedNamespaces.some((token) => wildcardTokens.has(token));
|
|
710
|
+
const isDebugEnabled = (namespace) => {
|
|
711
|
+
if (!namespaceSet.size) return false;
|
|
712
|
+
if (!namespace) return hasWildcard;
|
|
713
|
+
const normalized = namespace.toLowerCase();
|
|
714
|
+
if (hasWildcard) return true;
|
|
715
|
+
if (namespaceSet.has(normalized)) return true;
|
|
716
|
+
const [root] = normalized.split(":");
|
|
717
|
+
return namespaceSet.has(root);
|
|
718
|
+
};
|
|
719
|
+
const createDebugLogger = (namespace) => {
|
|
720
|
+
const normalized = namespace.toLowerCase();
|
|
721
|
+
return (...args) => {
|
|
722
|
+
if (!isDebugEnabled(normalized)) return;
|
|
723
|
+
const prefix = `[loro-repo:${namespace}]`;
|
|
724
|
+
if (args.length === 0) {
|
|
725
|
+
console.info(prefix);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
console.info(prefix, ...args);
|
|
729
|
+
};
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
//#endregion
|
|
733
|
+
//#region src/transport/websocket.ts
|
|
734
|
+
const debug = createDebugLogger("transport:websocket");
|
|
735
|
+
function withTimeout(promise, timeoutMs) {
|
|
736
|
+
if (!timeoutMs || timeoutMs <= 0) return promise;
|
|
737
|
+
return new Promise((resolve, reject) => {
|
|
738
|
+
const timer = setTimeout(() => {
|
|
739
|
+
reject(/* @__PURE__ */ new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
740
|
+
}, timeoutMs);
|
|
741
|
+
promise.then((value) => {
|
|
742
|
+
clearTimeout(timer);
|
|
743
|
+
resolve(value);
|
|
744
|
+
}).catch((error) => {
|
|
745
|
+
clearTimeout(timer);
|
|
746
|
+
reject(error);
|
|
747
|
+
});
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
function normalizeRoomId(roomId, fallback) {
|
|
751
|
+
if (typeof roomId === "string" && roomId.length > 0) return roomId;
|
|
752
|
+
if (roomId instanceof Uint8Array && roomId.length > 0) try {
|
|
753
|
+
return (0, loro_protocol.bytesToHex)(roomId);
|
|
754
|
+
} catch {
|
|
755
|
+
return fallback;
|
|
756
|
+
}
|
|
757
|
+
return fallback;
|
|
758
|
+
}
|
|
759
|
+
function bytesEqual(a, b) {
|
|
760
|
+
if (a === b) return true;
|
|
761
|
+
if (!a || !b) return false;
|
|
762
|
+
if (a.length !== b.length) return false;
|
|
763
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false;
|
|
764
|
+
return true;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* loro-websocket backed {@link TransportAdapter} implementation for LoroRepo.
|
|
768
|
+
* It uses loro-protocol as the underlying protocol for the transport.
|
|
769
|
+
*/
|
|
770
|
+
var WebSocketTransportAdapter = class {
|
|
771
|
+
options;
|
|
772
|
+
client;
|
|
773
|
+
metadataSession;
|
|
774
|
+
docSessions = /* @__PURE__ */ new Map();
|
|
775
|
+
constructor(options) {
|
|
776
|
+
this.options = options;
|
|
777
|
+
}
|
|
778
|
+
async connect(_options) {
|
|
779
|
+
const client = this.ensureClient();
|
|
780
|
+
debug("connect requested", { status: client.getStatus() });
|
|
781
|
+
try {
|
|
782
|
+
await client.connect();
|
|
783
|
+
debug("client.connect resolved");
|
|
784
|
+
await client.waitConnected();
|
|
785
|
+
debug("client.waitConnected resolved", { status: client.getStatus() });
|
|
786
|
+
} catch (error) {
|
|
787
|
+
debug("connect failed", error);
|
|
788
|
+
throw error;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
async close() {
|
|
792
|
+
debug("close requested", {
|
|
793
|
+
docSessions: this.docSessions.size,
|
|
794
|
+
metadataSession: Boolean(this.metadataSession)
|
|
795
|
+
});
|
|
796
|
+
for (const [docId] of this.docSessions) await this.leaveDocSession(docId).catch(() => {});
|
|
797
|
+
this.docSessions.clear();
|
|
798
|
+
await this.teardownMetadataSession().catch(() => {});
|
|
799
|
+
if (this.client) {
|
|
800
|
+
const client = this.client;
|
|
801
|
+
this.client = void 0;
|
|
802
|
+
client.destroy();
|
|
803
|
+
debug("websocket client destroyed");
|
|
804
|
+
}
|
|
805
|
+
debug("close completed");
|
|
806
|
+
}
|
|
807
|
+
isConnected() {
|
|
808
|
+
return this.client?.getStatus() === "connected";
|
|
809
|
+
}
|
|
810
|
+
async syncMeta(flock, options) {
|
|
811
|
+
debug("syncMeta requested", { roomId: this.options.metadataRoomId });
|
|
812
|
+
try {
|
|
813
|
+
let auth;
|
|
814
|
+
if (this.options.metadataAuth) if (typeof this.options.metadataAuth === "function") auth = await this.options.metadataAuth();
|
|
815
|
+
else auth = this.options.metadataAuth;
|
|
816
|
+
await withTimeout((await this.ensureMetadataSession(flock, {
|
|
817
|
+
roomId: this.options.metadataRoomId ?? "repo:meta",
|
|
818
|
+
auth
|
|
819
|
+
})).firstSynced, options?.timeout);
|
|
820
|
+
debug("syncMeta completed", { roomId: this.options.metadataRoomId });
|
|
821
|
+
return { ok: true };
|
|
822
|
+
} catch (error) {
|
|
823
|
+
debug("syncMeta failed", error);
|
|
824
|
+
return { ok: false };
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
joinMetaRoom(flock, params) {
|
|
828
|
+
const fallback = this.options.metadataRoomId ?? "";
|
|
829
|
+
const roomId = normalizeRoomId(params?.roomId, fallback);
|
|
830
|
+
if (!roomId) throw new Error("Metadata room id not configured");
|
|
831
|
+
const session = (async () => {
|
|
832
|
+
let auth;
|
|
833
|
+
const authWay = params?.auth ?? this.options.metadataAuth;
|
|
834
|
+
if (typeof authWay === "function") auth = await authWay();
|
|
835
|
+
else auth = authWay;
|
|
836
|
+
debug("joinMetaRoom requested", {
|
|
837
|
+
roomId,
|
|
838
|
+
hasAuth: Boolean(auth && auth.length)
|
|
839
|
+
});
|
|
840
|
+
return this.ensureMetadataSession(flock, {
|
|
841
|
+
roomId,
|
|
842
|
+
auth
|
|
843
|
+
});
|
|
844
|
+
})();
|
|
845
|
+
const firstSynced = session.then((session$1) => session$1.firstSynced);
|
|
846
|
+
const getConnected = () => this.isConnected();
|
|
847
|
+
const subscription = {
|
|
848
|
+
unsubscribe: () => {
|
|
849
|
+
session.then((session$1) => {
|
|
850
|
+
session$1.refCount = Math.max(0, session$1.refCount - 1);
|
|
851
|
+
debug("metadata session refCount decremented", {
|
|
852
|
+
roomId: session$1.roomId,
|
|
853
|
+
refCount: session$1.refCount
|
|
854
|
+
});
|
|
855
|
+
if (session$1.refCount === 0) {
|
|
856
|
+
debug("tearing down metadata session due to refCount=0", { roomId: session$1.roomId });
|
|
857
|
+
this.teardownMetadataSession(session$1).catch(() => {});
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
},
|
|
861
|
+
firstSyncedWithRemote: firstSynced,
|
|
862
|
+
get connected() {
|
|
863
|
+
return getConnected();
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
session.then((session$1) => {
|
|
867
|
+
session$1.refCount += 1;
|
|
868
|
+
debug("metadata session refCount incremented", {
|
|
869
|
+
roomId: session$1.roomId,
|
|
870
|
+
refCount: session$1.refCount
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
return subscription;
|
|
874
|
+
}
|
|
875
|
+
async syncDoc(docId, doc, options) {
|
|
876
|
+
debug("syncDoc requested", { docId });
|
|
877
|
+
try {
|
|
878
|
+
const session = await this.ensureDocSession(docId, doc, {});
|
|
879
|
+
await withTimeout(session.firstSynced, options?.timeout);
|
|
880
|
+
debug("syncDoc completed", {
|
|
881
|
+
docId,
|
|
882
|
+
roomId: session.roomId
|
|
883
|
+
});
|
|
884
|
+
return { ok: true };
|
|
885
|
+
} catch (error) {
|
|
886
|
+
debug("syncDoc failed", {
|
|
887
|
+
docId,
|
|
888
|
+
error
|
|
889
|
+
});
|
|
890
|
+
return { ok: false };
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
joinDocRoom(docId, doc, params) {
|
|
894
|
+
debug("joinDocRoom requested", {
|
|
895
|
+
docId,
|
|
896
|
+
roomParamType: params?.roomId ? typeof params.roomId === "string" ? "string" : "uint8array" : void 0,
|
|
897
|
+
hasAuthOverride: Boolean(params?.auth && params.auth.length)
|
|
898
|
+
});
|
|
899
|
+
const ensure = this.ensureDocSession(docId, doc, params ?? {});
|
|
900
|
+
const firstSynced = ensure.then((session) => session.firstSynced);
|
|
901
|
+
const getConnected = () => this.isConnected();
|
|
902
|
+
const subscription = {
|
|
903
|
+
unsubscribe: () => {
|
|
904
|
+
ensure.then((session) => {
|
|
905
|
+
session.refCount = Math.max(0, session.refCount - 1);
|
|
906
|
+
debug("doc session refCount decremented", {
|
|
907
|
+
docId,
|
|
908
|
+
roomId: session.roomId,
|
|
909
|
+
refCount: session.refCount
|
|
910
|
+
});
|
|
911
|
+
if (session.refCount === 0) this.leaveDocSession(docId).catch(() => {});
|
|
912
|
+
});
|
|
913
|
+
},
|
|
914
|
+
firstSyncedWithRemote: firstSynced,
|
|
915
|
+
get connected() {
|
|
916
|
+
return getConnected();
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
ensure.then((session) => {
|
|
920
|
+
session.refCount += 1;
|
|
921
|
+
debug("doc session refCount incremented", {
|
|
922
|
+
docId,
|
|
923
|
+
roomId: session.roomId,
|
|
924
|
+
refCount: session.refCount
|
|
925
|
+
});
|
|
926
|
+
});
|
|
927
|
+
return subscription;
|
|
928
|
+
}
|
|
929
|
+
ensureClient() {
|
|
930
|
+
if (this.client) {
|
|
931
|
+
debug("reusing websocket client", { status: this.client.getStatus() });
|
|
932
|
+
return this.client;
|
|
933
|
+
}
|
|
934
|
+
const { url, client: clientOptions } = this.options;
|
|
935
|
+
debug("creating websocket client", {
|
|
936
|
+
url,
|
|
937
|
+
clientOptionsKeys: clientOptions ? Object.keys(clientOptions) : []
|
|
938
|
+
});
|
|
939
|
+
const client = new loro_websocket.LoroWebsocketClient({
|
|
940
|
+
url,
|
|
941
|
+
...clientOptions
|
|
942
|
+
});
|
|
943
|
+
this.client = client;
|
|
944
|
+
return client;
|
|
945
|
+
}
|
|
946
|
+
async ensureMetadataSession(flock, params) {
|
|
947
|
+
debug("ensureMetadataSession invoked", {
|
|
948
|
+
roomId: params.roomId,
|
|
949
|
+
hasAuth: Boolean(params.auth && params.auth.length)
|
|
950
|
+
});
|
|
951
|
+
const client = this.ensureClient();
|
|
952
|
+
await client.waitConnected();
|
|
953
|
+
debug("websocket client ready for metadata session", { status: client.getStatus() });
|
|
954
|
+
if (this.metadataSession && this.metadataSession.flock === flock && this.metadataSession.roomId === params.roomId && bytesEqual(this.metadataSession.auth, params.auth)) {
|
|
955
|
+
debug("reusing metadata session", {
|
|
956
|
+
roomId: this.metadataSession.roomId,
|
|
957
|
+
refCount: this.metadataSession.refCount
|
|
958
|
+
});
|
|
959
|
+
return this.metadataSession;
|
|
960
|
+
}
|
|
961
|
+
if (this.metadataSession) {
|
|
962
|
+
debug("tearing down previous metadata session", { roomId: this.metadataSession.roomId });
|
|
963
|
+
await this.teardownMetadataSession(this.metadataSession).catch(() => {});
|
|
964
|
+
}
|
|
965
|
+
const adaptor = new loro_adaptors_flock.FlockAdaptor(flock, this.options.metadataAdaptorConfig);
|
|
966
|
+
debug("joining metadata room", {
|
|
967
|
+
roomId: params.roomId,
|
|
968
|
+
hasAuth: Boolean(params.auth && params.auth.length)
|
|
969
|
+
});
|
|
970
|
+
const room = await client.join({
|
|
971
|
+
roomId: params.roomId,
|
|
972
|
+
crdtAdaptor: adaptor,
|
|
973
|
+
auth: params.auth
|
|
974
|
+
});
|
|
975
|
+
const firstSynced = room.waitForReachingServerVersion();
|
|
976
|
+
firstSynced.then(() => {
|
|
977
|
+
debug("metadata session firstSynced resolved", { roomId: params.roomId });
|
|
978
|
+
}, (error) => {
|
|
979
|
+
debug("metadata session firstSynced rejected", {
|
|
980
|
+
roomId: params.roomId,
|
|
981
|
+
error
|
|
982
|
+
});
|
|
983
|
+
});
|
|
984
|
+
const session = {
|
|
985
|
+
adaptor,
|
|
986
|
+
room,
|
|
987
|
+
firstSynced,
|
|
988
|
+
flock,
|
|
989
|
+
roomId: params.roomId,
|
|
990
|
+
auth: params.auth,
|
|
991
|
+
refCount: 0
|
|
992
|
+
};
|
|
993
|
+
this.metadataSession = session;
|
|
994
|
+
return session;
|
|
995
|
+
}
|
|
996
|
+
async teardownMetadataSession(session) {
|
|
997
|
+
const target = session ?? this.metadataSession;
|
|
998
|
+
if (!target) return;
|
|
999
|
+
debug("teardownMetadataSession invoked", { roomId: target.roomId });
|
|
1000
|
+
if (this.metadataSession === target) this.metadataSession = void 0;
|
|
1001
|
+
const { adaptor, room } = target;
|
|
1002
|
+
try {
|
|
1003
|
+
await room.leave();
|
|
1004
|
+
debug("metadata room left", { roomId: target.roomId });
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
debug("metadata room leave failed; destroying", {
|
|
1007
|
+
roomId: target.roomId,
|
|
1008
|
+
error
|
|
1009
|
+
});
|
|
1010
|
+
await room.destroy().catch(() => {});
|
|
1011
|
+
}
|
|
1012
|
+
adaptor.destroy();
|
|
1013
|
+
debug("metadata session destroyed", { roomId: target.roomId });
|
|
1014
|
+
}
|
|
1015
|
+
async ensureDocSession(docId, doc, params) {
|
|
1016
|
+
debug("ensureDocSession invoked", { docId });
|
|
1017
|
+
const client = this.ensureClient();
|
|
1018
|
+
await client.waitConnected();
|
|
1019
|
+
debug("websocket client ready for doc session", {
|
|
1020
|
+
docId,
|
|
1021
|
+
status: client.getStatus()
|
|
1022
|
+
});
|
|
1023
|
+
const existing = this.docSessions.get(docId);
|
|
1024
|
+
const derivedRoomId = this.options.docRoomId?.(docId) ?? docId;
|
|
1025
|
+
const roomId = normalizeRoomId(params.roomId, derivedRoomId);
|
|
1026
|
+
let auth;
|
|
1027
|
+
auth = await (params.auth ?? this.options.docAuth?.(docId));
|
|
1028
|
+
debug("doc session params resolved", {
|
|
1029
|
+
docId,
|
|
1030
|
+
roomId,
|
|
1031
|
+
hasAuth: Boolean(auth && auth.length)
|
|
1032
|
+
});
|
|
1033
|
+
if (existing && existing.doc === doc && existing.roomId === roomId) {
|
|
1034
|
+
debug("reusing doc session", {
|
|
1035
|
+
docId,
|
|
1036
|
+
roomId,
|
|
1037
|
+
refCount: existing.refCount
|
|
1038
|
+
});
|
|
1039
|
+
return existing;
|
|
1040
|
+
}
|
|
1041
|
+
if (existing) {
|
|
1042
|
+
debug("doc session mismatch; leaving existing session", {
|
|
1043
|
+
docId,
|
|
1044
|
+
previousRoomId: existing.roomId,
|
|
1045
|
+
nextRoomId: roomId
|
|
1046
|
+
});
|
|
1047
|
+
await this.leaveDocSession(docId).catch(() => {});
|
|
1048
|
+
}
|
|
1049
|
+
const adaptor = new loro_adaptors_loro.LoroAdaptor(doc);
|
|
1050
|
+
debug("joining doc room", {
|
|
1051
|
+
docId,
|
|
1052
|
+
roomId,
|
|
1053
|
+
hasAuth: Boolean(auth && auth.length)
|
|
1054
|
+
});
|
|
1055
|
+
const room = await client.join({
|
|
1056
|
+
roomId,
|
|
1057
|
+
crdtAdaptor: adaptor,
|
|
1058
|
+
auth
|
|
1059
|
+
});
|
|
1060
|
+
const firstSynced = room.waitForReachingServerVersion();
|
|
1061
|
+
firstSynced.then(() => {
|
|
1062
|
+
debug("doc session firstSynced resolved", {
|
|
1063
|
+
docId,
|
|
1064
|
+
roomId
|
|
1065
|
+
});
|
|
1066
|
+
}, (error) => {
|
|
1067
|
+
debug("doc session firstSynced rejected", {
|
|
1068
|
+
docId,
|
|
1069
|
+
roomId,
|
|
1070
|
+
error
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
const session = {
|
|
1074
|
+
adaptor,
|
|
1075
|
+
room,
|
|
1076
|
+
firstSynced,
|
|
1077
|
+
doc,
|
|
1078
|
+
roomId,
|
|
1079
|
+
refCount: 0
|
|
1080
|
+
};
|
|
1081
|
+
this.docSessions.set(docId, session);
|
|
1082
|
+
return session;
|
|
1083
|
+
}
|
|
1084
|
+
async leaveDocSession(docId) {
|
|
1085
|
+
const session = this.docSessions.get(docId);
|
|
1086
|
+
if (!session) {
|
|
1087
|
+
debug("leaveDocSession invoked but no session found", { docId });
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
this.docSessions.delete(docId);
|
|
1091
|
+
debug("leaving doc session", {
|
|
1092
|
+
docId,
|
|
1093
|
+
roomId: session.roomId
|
|
1094
|
+
});
|
|
1095
|
+
try {
|
|
1096
|
+
await session.room.leave();
|
|
1097
|
+
debug("doc room left", {
|
|
1098
|
+
docId,
|
|
1099
|
+
roomId: session.roomId
|
|
1100
|
+
});
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
debug("doc room leave failed; destroying", {
|
|
1103
|
+
docId,
|
|
1104
|
+
roomId: session.roomId,
|
|
1105
|
+
error
|
|
1106
|
+
});
|
|
1107
|
+
await session.room.destroy().catch(() => {});
|
|
1108
|
+
}
|
|
1109
|
+
session.adaptor.destroy();
|
|
1110
|
+
debug("doc session destroyed", {
|
|
1111
|
+
docId,
|
|
1112
|
+
roomId: session.roomId
|
|
1113
|
+
});
|
|
1104
1114
|
}
|
|
1105
|
-
}
|
|
1106
|
-
async function writeFileAtomic(targetPath, data) {
|
|
1107
|
-
const dir = node_path.dirname(targetPath);
|
|
1108
|
-
await ensureDir(dir);
|
|
1109
|
-
const tempPath = node_path.join(dir, `.tmp-${(0, node_crypto.randomUUID)()}`);
|
|
1110
|
-
await node_fs.promises.writeFile(tempPath, data);
|
|
1111
|
-
await node_fs.promises.rename(tempPath, targetPath);
|
|
1112
|
-
}
|
|
1115
|
+
};
|
|
1113
1116
|
|
|
1114
1117
|
//#endregion
|
|
1115
1118
|
//#region src/internal/event-bus.ts
|
|
@@ -1130,223 +1133,35 @@ var RepoEventBus = class {
|
|
|
1130
1133
|
for (const entry of this.watchers) if (this.shouldNotify(entry.filter, event)) entry.listener(event);
|
|
1131
1134
|
}
|
|
1132
1135
|
clear() {
|
|
1133
|
-
this.watchers.clear();
|
|
1134
|
-
this.eventByStack.length = 0;
|
|
1135
|
-
}
|
|
1136
|
-
pushEventBy(by) {
|
|
1137
|
-
this.eventByStack.push(by);
|
|
1138
|
-
}
|
|
1139
|
-
popEventBy() {
|
|
1140
|
-
this.eventByStack.pop();
|
|
1141
|
-
}
|
|
1142
|
-
resolveEventBy(defaultBy) {
|
|
1143
|
-
const index = this.eventByStack.length - 1;
|
|
1144
|
-
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
1145
|
-
}
|
|
1146
|
-
shouldNotify(filter, event) {
|
|
1147
|
-
if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
|
|
1148
|
-
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
1149
|
-
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
1150
|
-
const docId = (() => {
|
|
1151
|
-
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
1152
|
-
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
1153
|
-
})();
|
|
1154
|
-
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
1155
|
-
if (filter.docIds && !docId) return false;
|
|
1156
|
-
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
1157
|
-
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
1158
|
-
}
|
|
1159
|
-
return true;
|
|
1160
|
-
}
|
|
1161
|
-
};
|
|
1162
|
-
|
|
1163
|
-
//#endregion
|
|
1164
|
-
//#region src/utils.ts
|
|
1165
|
-
async function streamToUint8Array(stream) {
|
|
1166
|
-
const reader = stream.getReader();
|
|
1167
|
-
const chunks = [];
|
|
1168
|
-
let total = 0;
|
|
1169
|
-
while (true) {
|
|
1170
|
-
const { done, value } = await reader.read();
|
|
1171
|
-
if (done) break;
|
|
1172
|
-
if (value) {
|
|
1173
|
-
chunks.push(value);
|
|
1174
|
-
total += value.byteLength;
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
const buffer = new Uint8Array(total);
|
|
1178
|
-
let offset = 0;
|
|
1179
|
-
for (const chunk of chunks) {
|
|
1180
|
-
buffer.set(chunk, offset);
|
|
1181
|
-
offset += chunk.byteLength;
|
|
1182
|
-
}
|
|
1183
|
-
return buffer;
|
|
1184
|
-
}
|
|
1185
|
-
async function assetContentToUint8Array(content) {
|
|
1186
|
-
if (content instanceof Uint8Array) return content;
|
|
1187
|
-
if (ArrayBuffer.isView(content)) return new Uint8Array(content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
|
|
1188
|
-
if (typeof Blob !== "undefined" && content instanceof Blob) return new Uint8Array(await content.arrayBuffer());
|
|
1189
|
-
if (typeof ReadableStream !== "undefined" && content instanceof ReadableStream) return streamToUint8Array(content);
|
|
1190
|
-
throw new TypeError("Unsupported asset content type");
|
|
1191
|
-
}
|
|
1192
|
-
function bytesToHex(bytes) {
|
|
1193
|
-
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1194
|
-
}
|
|
1195
|
-
async function computeSha256(bytes) {
|
|
1196
|
-
const globalCrypto = globalThis.crypto;
|
|
1197
|
-
if (globalCrypto?.subtle && typeof globalCrypto.subtle.digest === "function") {
|
|
1198
|
-
const digest = await globalCrypto.subtle.digest("SHA-256", bytes);
|
|
1199
|
-
return bytesToHex(new Uint8Array(digest));
|
|
1200
|
-
}
|
|
1201
|
-
try {
|
|
1202
|
-
const { createHash } = await import("node:crypto");
|
|
1203
|
-
const hash = createHash("sha256");
|
|
1204
|
-
hash.update(bytes);
|
|
1205
|
-
return hash.digest("hex");
|
|
1206
|
-
} catch {
|
|
1207
|
-
throw new Error("SHA-256 digest is not available in this environment");
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
function cloneJsonValue(value) {
|
|
1211
|
-
if (value === null) return null;
|
|
1212
|
-
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
1213
|
-
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
1214
|
-
if (Array.isArray(value)) {
|
|
1215
|
-
const arr = [];
|
|
1216
|
-
for (const entry of value) {
|
|
1217
|
-
const cloned = cloneJsonValue(entry);
|
|
1218
|
-
if (cloned !== void 0) arr.push(cloned);
|
|
1219
|
-
}
|
|
1220
|
-
return arr;
|
|
1221
|
-
}
|
|
1222
|
-
if (value && typeof value === "object") {
|
|
1223
|
-
const input = value;
|
|
1224
|
-
const obj = {};
|
|
1225
|
-
for (const [key, entry] of Object.entries(input)) {
|
|
1226
|
-
const cloned = cloneJsonValue(entry);
|
|
1227
|
-
if (cloned !== void 0) obj[key] = cloned;
|
|
1228
|
-
}
|
|
1229
|
-
return obj;
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
function cloneJsonObject(value) {
|
|
1233
|
-
const cloned = cloneJsonValue(value);
|
|
1234
|
-
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1235
|
-
return {};
|
|
1236
|
-
}
|
|
1237
|
-
function asJsonObject(value) {
|
|
1238
|
-
const cloned = cloneJsonValue(value);
|
|
1239
|
-
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1240
|
-
}
|
|
1241
|
-
function isJsonObjectValue(value) {
|
|
1242
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1243
|
-
}
|
|
1244
|
-
function stableStringify(value) {
|
|
1245
|
-
if (value === null) return "null";
|
|
1246
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
1247
|
-
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
1248
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1249
|
-
if (!isJsonObjectValue(value)) return "null";
|
|
1250
|
-
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
1251
|
-
}
|
|
1252
|
-
function jsonEquals(a, b) {
|
|
1253
|
-
if (a === void 0 && b === void 0) return true;
|
|
1254
|
-
if (a === void 0 || b === void 0) return false;
|
|
1255
|
-
return stableStringify(a) === stableStringify(b);
|
|
1256
|
-
}
|
|
1257
|
-
function diffJsonObjects(previous, next) {
|
|
1258
|
-
const patch = {};
|
|
1259
|
-
const keys = /* @__PURE__ */ new Set();
|
|
1260
|
-
if (previous) for (const key of Object.keys(previous)) keys.add(key);
|
|
1261
|
-
for (const key of Object.keys(next)) keys.add(key);
|
|
1262
|
-
for (const key of keys) {
|
|
1263
|
-
const prevValue = previous ? previous[key] : void 0;
|
|
1264
|
-
const nextValue = next[key];
|
|
1265
|
-
if (!jsonEquals(prevValue, nextValue)) {
|
|
1266
|
-
if (nextValue === void 0 && previous && key in previous) {
|
|
1267
|
-
patch[key] = null;
|
|
1268
|
-
continue;
|
|
1269
|
-
}
|
|
1270
|
-
const cloned = cloneJsonValue(nextValue);
|
|
1271
|
-
if (cloned !== void 0) patch[key] = cloned;
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
return patch;
|
|
1275
|
-
}
|
|
1276
|
-
function assetMetaToJson(meta) {
|
|
1277
|
-
const json = {
|
|
1278
|
-
assetId: meta.assetId,
|
|
1279
|
-
size: meta.size,
|
|
1280
|
-
createdAt: meta.createdAt
|
|
1281
|
-
};
|
|
1282
|
-
if (meta.mime !== void 0) json.mime = meta.mime;
|
|
1283
|
-
if (meta.policy !== void 0) json.policy = meta.policy;
|
|
1284
|
-
if (meta.tag !== void 0) json.tag = meta.tag;
|
|
1285
|
-
return json;
|
|
1286
|
-
}
|
|
1287
|
-
function assetMetaFromJson(value) {
|
|
1288
|
-
const obj = asJsonObject(value);
|
|
1289
|
-
if (!obj) return void 0;
|
|
1290
|
-
const assetId = typeof obj.assetId === "string" ? obj.assetId : void 0;
|
|
1291
|
-
if (!assetId) return void 0;
|
|
1292
|
-
const size = typeof obj.size === "number" ? obj.size : void 0;
|
|
1293
|
-
const createdAt = typeof obj.createdAt === "number" ? obj.createdAt : void 0;
|
|
1294
|
-
if (size === void 0 || createdAt === void 0) return void 0;
|
|
1295
|
-
return {
|
|
1296
|
-
assetId,
|
|
1297
|
-
size,
|
|
1298
|
-
createdAt,
|
|
1299
|
-
...typeof obj.mime === "string" ? { mime: obj.mime } : {},
|
|
1300
|
-
...typeof obj.policy === "string" ? { policy: obj.policy } : {},
|
|
1301
|
-
...typeof obj.tag === "string" ? { tag: obj.tag } : {}
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
function assetMetadataEqual(a, b) {
|
|
1305
|
-
if (!a && !b) return true;
|
|
1306
|
-
if (!a || !b) return false;
|
|
1307
|
-
return stableStringify(assetMetaToJson(a)) === stableStringify(assetMetaToJson(b));
|
|
1308
|
-
}
|
|
1309
|
-
function cloneRepoAssetMetadata(meta) {
|
|
1310
|
-
return {
|
|
1311
|
-
assetId: meta.assetId,
|
|
1312
|
-
size: meta.size,
|
|
1313
|
-
createdAt: meta.createdAt,
|
|
1314
|
-
...meta.mime !== void 0 ? { mime: meta.mime } : {},
|
|
1315
|
-
...meta.policy !== void 0 ? { policy: meta.policy } : {},
|
|
1316
|
-
...meta.tag !== void 0 ? { tag: meta.tag } : {}
|
|
1317
|
-
};
|
|
1318
|
-
}
|
|
1319
|
-
function toReadableStream(bytes) {
|
|
1320
|
-
return new ReadableStream({ start(controller) {
|
|
1321
|
-
controller.enqueue(bytes);
|
|
1322
|
-
controller.close();
|
|
1323
|
-
} });
|
|
1324
|
-
}
|
|
1325
|
-
function canonicalizeFrontiers(frontiers) {
|
|
1326
|
-
const json = [...frontiers].sort((a, b) => {
|
|
1327
|
-
if (a.peer < b.peer) return -1;
|
|
1328
|
-
if (a.peer > b.peer) return 1;
|
|
1329
|
-
return a.counter - b.counter;
|
|
1330
|
-
}).map((f) => ({
|
|
1331
|
-
peer: f.peer,
|
|
1332
|
-
counter: f.counter
|
|
1333
|
-
}));
|
|
1334
|
-
return {
|
|
1335
|
-
json,
|
|
1336
|
-
key: stableStringify(json)
|
|
1337
|
-
};
|
|
1338
|
-
}
|
|
1339
|
-
function includesFrontiers(vv, frontiers) {
|
|
1340
|
-
for (const { peer, counter } of frontiers) if ((vv.get(peer) ?? 0) <= counter) return false;
|
|
1341
|
-
return true;
|
|
1342
|
-
}
|
|
1343
|
-
function matchesQuery(docId, _metadata, query) {
|
|
1344
|
-
if (!query) return true;
|
|
1345
|
-
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
1346
|
-
if (query.start && docId < query.start) return false;
|
|
1347
|
-
if (query.end && docId > query.end) return false;
|
|
1348
|
-
return true;
|
|
1349
|
-
}
|
|
1136
|
+
this.watchers.clear();
|
|
1137
|
+
this.eventByStack.length = 0;
|
|
1138
|
+
}
|
|
1139
|
+
pushEventBy(by) {
|
|
1140
|
+
this.eventByStack.push(by);
|
|
1141
|
+
}
|
|
1142
|
+
popEventBy() {
|
|
1143
|
+
this.eventByStack.pop();
|
|
1144
|
+
}
|
|
1145
|
+
resolveEventBy(defaultBy) {
|
|
1146
|
+
const index = this.eventByStack.length - 1;
|
|
1147
|
+
return index >= 0 ? this.eventByStack[index] : defaultBy;
|
|
1148
|
+
}
|
|
1149
|
+
shouldNotify(filter, event) {
|
|
1150
|
+
if (!filter.docIds && !filter.kinds && !filter.metadataFields && !filter.by) return true;
|
|
1151
|
+
if (filter.kinds && !filter.kinds.includes(event.kind)) return false;
|
|
1152
|
+
if (filter.by && !filter.by.includes(event.by)) return false;
|
|
1153
|
+
const docId = (() => {
|
|
1154
|
+
if (event.kind === "doc-metadata" || event.kind === "doc-frontiers") return event.docId;
|
|
1155
|
+
if (event.kind === "asset-link" || event.kind === "asset-unlink") return event.docId;
|
|
1156
|
+
})();
|
|
1157
|
+
if (filter.docIds && docId && !filter.docIds.includes(docId)) return false;
|
|
1158
|
+
if (filter.docIds && !docId) return false;
|
|
1159
|
+
if (filter.metadataFields && event.kind === "doc-metadata") {
|
|
1160
|
+
if (!Object.keys(event.patch).some((key) => filter.metadataFields?.includes(key))) return false;
|
|
1161
|
+
}
|
|
1162
|
+
return true;
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1350
1165
|
|
|
1351
1166
|
//#endregion
|
|
1352
1167
|
//#region src/internal/logging.ts
|
|
@@ -1365,23 +1180,18 @@ var DocManager = class {
|
|
|
1365
1180
|
getMetaFlock;
|
|
1366
1181
|
eventBus;
|
|
1367
1182
|
persistMeta;
|
|
1368
|
-
state;
|
|
1369
1183
|
docs = /* @__PURE__ */ new Map();
|
|
1370
1184
|
docSubscriptions = /* @__PURE__ */ new Map();
|
|
1371
1185
|
docFrontierUpdates = /* @__PURE__ */ new Map();
|
|
1372
1186
|
docPersistedVersions = /* @__PURE__ */ new Map();
|
|
1373
|
-
get docFrontierKeys() {
|
|
1374
|
-
return this.state.docFrontierKeys;
|
|
1375
|
-
}
|
|
1376
1187
|
constructor(options) {
|
|
1377
1188
|
this.storage = options.storage;
|
|
1378
1189
|
this.docFrontierDebounceMs = options.docFrontierDebounceMs;
|
|
1379
1190
|
this.getMetaFlock = options.getMetaFlock;
|
|
1380
1191
|
this.eventBus = options.eventBus;
|
|
1381
1192
|
this.persistMeta = options.persistMeta;
|
|
1382
|
-
this.state = options.state;
|
|
1383
1193
|
}
|
|
1384
|
-
async
|
|
1194
|
+
async openPersistedDoc(docId) {
|
|
1385
1195
|
return await this.ensureDoc(docId);
|
|
1386
1196
|
}
|
|
1387
1197
|
async openDetachedDoc(docId) {
|
|
@@ -1428,38 +1238,27 @@ var DocManager = class {
|
|
|
1428
1238
|
}
|
|
1429
1239
|
async updateDocFrontiers(docId, doc, defaultBy) {
|
|
1430
1240
|
const frontiers = doc.oplogFrontiers();
|
|
1431
|
-
const
|
|
1432
|
-
const
|
|
1241
|
+
const vv = doc.version();
|
|
1242
|
+
const existingFrontiers = this.readFrontiersFromFlock(docId);
|
|
1433
1243
|
let mutated = false;
|
|
1434
1244
|
const metaFlock = this.metaFlock;
|
|
1435
|
-
const
|
|
1436
|
-
for (const entry of existingKeys) {
|
|
1437
|
-
if (entry === key) continue;
|
|
1438
|
-
let oldFrontiers;
|
|
1439
|
-
try {
|
|
1440
|
-
oldFrontiers = JSON.parse(entry);
|
|
1441
|
-
} catch {
|
|
1442
|
-
continue;
|
|
1443
|
-
}
|
|
1444
|
-
if (includesFrontiers(vv, oldFrontiers)) {
|
|
1445
|
-
metaFlock.delete([
|
|
1446
|
-
"f",
|
|
1447
|
-
docId,
|
|
1448
|
-
entry
|
|
1449
|
-
]);
|
|
1450
|
-
mutated = true;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
if (!existingKeys.has(key)) {
|
|
1245
|
+
for (const f of frontiers) if (existingFrontiers.get(f.peer) !== f.counter) {
|
|
1454
1246
|
metaFlock.put([
|
|
1455
1247
|
"f",
|
|
1456
1248
|
docId,
|
|
1457
|
-
|
|
1458
|
-
],
|
|
1249
|
+
f.peer
|
|
1250
|
+
], f.counter);
|
|
1459
1251
|
mutated = true;
|
|
1460
1252
|
}
|
|
1461
1253
|
if (mutated) {
|
|
1462
|
-
|
|
1254
|
+
for (const [peer, counter] of existingFrontiers) {
|
|
1255
|
+
const docCounterEnd = vv.get(peer);
|
|
1256
|
+
if (docCounterEnd != null && docCounterEnd > counter) metaFlock.delete([
|
|
1257
|
+
"f",
|
|
1258
|
+
docId,
|
|
1259
|
+
peer
|
|
1260
|
+
]);
|
|
1261
|
+
}
|
|
1463
1262
|
await this.persistMeta();
|
|
1464
1263
|
}
|
|
1465
1264
|
const by = this.eventBus.resolveEventBy(defaultBy);
|
|
@@ -1511,37 +1310,22 @@ var DocManager = class {
|
|
|
1511
1310
|
this.docFrontierUpdates.clear();
|
|
1512
1311
|
this.docs.clear();
|
|
1513
1312
|
this.docPersistedVersions.clear();
|
|
1514
|
-
this.docFrontierKeys.clear();
|
|
1515
1313
|
}
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
const frontierRows = this.metaFlock.scan({ prefix: ["f"] });
|
|
1519
|
-
for (const row of frontierRows) {
|
|
1520
|
-
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1521
|
-
const docId = row.key[1];
|
|
1522
|
-
const frontierKey = row.key[2];
|
|
1523
|
-
if (typeof docId !== "string" || typeof frontierKey !== "string") continue;
|
|
1524
|
-
const set = nextFrontierKeys.get(docId) ?? /* @__PURE__ */ new Set();
|
|
1525
|
-
set.add(frontierKey);
|
|
1526
|
-
nextFrontierKeys.set(docId, set);
|
|
1527
|
-
}
|
|
1528
|
-
this.docFrontierKeys.clear();
|
|
1529
|
-
for (const [docId, keys] of nextFrontierKeys) this.docFrontierKeys.set(docId, keys);
|
|
1314
|
+
get metaFlock() {
|
|
1315
|
+
return this.getMetaFlock();
|
|
1530
1316
|
}
|
|
1531
|
-
|
|
1317
|
+
readFrontiersFromFlock(docId) {
|
|
1532
1318
|
const rows = this.metaFlock.scan({ prefix: ["f", docId] });
|
|
1533
|
-
const
|
|
1319
|
+
const frontiers = /* @__PURE__ */ new Map();
|
|
1534
1320
|
for (const row of rows) {
|
|
1535
1321
|
if (!Array.isArray(row.key) || row.key.length < 3) continue;
|
|
1536
|
-
|
|
1537
|
-
const
|
|
1538
|
-
if (typeof
|
|
1322
|
+
const peer = row.key[2];
|
|
1323
|
+
const counter = row.value;
|
|
1324
|
+
if (typeof peer !== "string") continue;
|
|
1325
|
+
if (typeof counter !== "number" || !Number.isFinite(counter)) continue;
|
|
1326
|
+
frontiers.set(peer, counter);
|
|
1539
1327
|
}
|
|
1540
|
-
|
|
1541
|
-
else this.docFrontierKeys.delete(docId);
|
|
1542
|
-
}
|
|
1543
|
-
get metaFlock() {
|
|
1544
|
-
return this.getMetaFlock();
|
|
1328
|
+
return frontiers;
|
|
1545
1329
|
}
|
|
1546
1330
|
registerDoc(docId, doc) {
|
|
1547
1331
|
this.docs.set(docId, doc);
|
|
@@ -1654,6 +1438,176 @@ var DocManager = class {
|
|
|
1654
1438
|
}
|
|
1655
1439
|
};
|
|
1656
1440
|
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/utils.ts
|
|
1443
|
+
async function streamToUint8Array(stream) {
|
|
1444
|
+
const reader = stream.getReader();
|
|
1445
|
+
const chunks = [];
|
|
1446
|
+
let total = 0;
|
|
1447
|
+
while (true) {
|
|
1448
|
+
const { done, value } = await reader.read();
|
|
1449
|
+
if (done) break;
|
|
1450
|
+
if (value) {
|
|
1451
|
+
chunks.push(value);
|
|
1452
|
+
total += value.byteLength;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
const buffer = new Uint8Array(total);
|
|
1456
|
+
let offset = 0;
|
|
1457
|
+
for (const chunk of chunks) {
|
|
1458
|
+
buffer.set(chunk, offset);
|
|
1459
|
+
offset += chunk.byteLength;
|
|
1460
|
+
}
|
|
1461
|
+
return buffer;
|
|
1462
|
+
}
|
|
1463
|
+
async function assetContentToUint8Array(content) {
|
|
1464
|
+
if (content instanceof Uint8Array) return content;
|
|
1465
|
+
if (ArrayBuffer.isView(content)) return new Uint8Array(content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength));
|
|
1466
|
+
if (typeof Blob !== "undefined" && content instanceof Blob) return new Uint8Array(await content.arrayBuffer());
|
|
1467
|
+
if (typeof ReadableStream !== "undefined" && content instanceof ReadableStream) return streamToUint8Array(content);
|
|
1468
|
+
throw new TypeError("Unsupported asset content type");
|
|
1469
|
+
}
|
|
1470
|
+
function bytesToHex(bytes) {
|
|
1471
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1472
|
+
}
|
|
1473
|
+
async function computeSha256(bytes) {
|
|
1474
|
+
const globalCrypto = globalThis.crypto;
|
|
1475
|
+
if (globalCrypto?.subtle && typeof globalCrypto.subtle.digest === "function") {
|
|
1476
|
+
const digest = await globalCrypto.subtle.digest("SHA-256", bytes);
|
|
1477
|
+
return bytesToHex(new Uint8Array(digest));
|
|
1478
|
+
}
|
|
1479
|
+
try {
|
|
1480
|
+
const { createHash } = await import("node:crypto");
|
|
1481
|
+
const hash = createHash("sha256");
|
|
1482
|
+
hash.update(bytes);
|
|
1483
|
+
return hash.digest("hex");
|
|
1484
|
+
} catch {
|
|
1485
|
+
throw new Error("SHA-256 digest is not available in this environment");
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function cloneJsonValue(value) {
|
|
1489
|
+
if (value === null) return null;
|
|
1490
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
1491
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
|
|
1492
|
+
if (Array.isArray(value)) {
|
|
1493
|
+
const arr = [];
|
|
1494
|
+
for (const entry of value) {
|
|
1495
|
+
const cloned = cloneJsonValue(entry);
|
|
1496
|
+
if (cloned !== void 0) arr.push(cloned);
|
|
1497
|
+
}
|
|
1498
|
+
return arr;
|
|
1499
|
+
}
|
|
1500
|
+
if (value && typeof value === "object") {
|
|
1501
|
+
const input = value;
|
|
1502
|
+
const obj = {};
|
|
1503
|
+
for (const [key, entry] of Object.entries(input)) {
|
|
1504
|
+
const cloned = cloneJsonValue(entry);
|
|
1505
|
+
if (cloned !== void 0) obj[key] = cloned;
|
|
1506
|
+
}
|
|
1507
|
+
return obj;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function cloneJsonObject(value) {
|
|
1511
|
+
const cloned = cloneJsonValue(value);
|
|
1512
|
+
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1513
|
+
return {};
|
|
1514
|
+
}
|
|
1515
|
+
function asJsonObject(value) {
|
|
1516
|
+
const cloned = cloneJsonValue(value);
|
|
1517
|
+
if (cloned && typeof cloned === "object" && !Array.isArray(cloned)) return cloned;
|
|
1518
|
+
}
|
|
1519
|
+
function isJsonObjectValue(value) {
|
|
1520
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1521
|
+
}
|
|
1522
|
+
function stableStringify(value) {
|
|
1523
|
+
if (value === null) return "null";
|
|
1524
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
1525
|
+
if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
|
|
1526
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
1527
|
+
if (!isJsonObjectValue(value)) return "null";
|
|
1528
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
1529
|
+
}
|
|
1530
|
+
function jsonEquals(a, b) {
|
|
1531
|
+
if (a === void 0 && b === void 0) return true;
|
|
1532
|
+
if (a === void 0 || b === void 0) return false;
|
|
1533
|
+
return stableStringify(a) === stableStringify(b);
|
|
1534
|
+
}
|
|
1535
|
+
function diffJsonObjects(previous, next) {
|
|
1536
|
+
const patch = {};
|
|
1537
|
+
const keys = /* @__PURE__ */ new Set();
|
|
1538
|
+
if (previous) for (const key of Object.keys(previous)) keys.add(key);
|
|
1539
|
+
for (const key of Object.keys(next)) keys.add(key);
|
|
1540
|
+
for (const key of keys) {
|
|
1541
|
+
const prevValue = previous ? previous[key] : void 0;
|
|
1542
|
+
const nextValue = next[key];
|
|
1543
|
+
if (!jsonEquals(prevValue, nextValue)) {
|
|
1544
|
+
if (nextValue === void 0 && previous && key in previous) {
|
|
1545
|
+
patch[key] = null;
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
const cloned = cloneJsonValue(nextValue);
|
|
1549
|
+
if (cloned !== void 0) patch[key] = cloned;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
return patch;
|
|
1553
|
+
}
|
|
1554
|
+
function assetMetaToJson(meta) {
|
|
1555
|
+
const json = {
|
|
1556
|
+
assetId: meta.assetId,
|
|
1557
|
+
size: meta.size,
|
|
1558
|
+
createdAt: meta.createdAt
|
|
1559
|
+
};
|
|
1560
|
+
if (meta.mime !== void 0) json.mime = meta.mime;
|
|
1561
|
+
if (meta.policy !== void 0) json.policy = meta.policy;
|
|
1562
|
+
if (meta.tag !== void 0) json.tag = meta.tag;
|
|
1563
|
+
return json;
|
|
1564
|
+
}
|
|
1565
|
+
function assetMetaFromJson(value) {
|
|
1566
|
+
const obj = asJsonObject(value);
|
|
1567
|
+
if (!obj) return void 0;
|
|
1568
|
+
const assetId = typeof obj.assetId === "string" ? obj.assetId : void 0;
|
|
1569
|
+
if (!assetId) return void 0;
|
|
1570
|
+
const size = typeof obj.size === "number" ? obj.size : void 0;
|
|
1571
|
+
const createdAt = typeof obj.createdAt === "number" ? obj.createdAt : void 0;
|
|
1572
|
+
if (size === void 0 || createdAt === void 0) return void 0;
|
|
1573
|
+
return {
|
|
1574
|
+
assetId,
|
|
1575
|
+
size,
|
|
1576
|
+
createdAt,
|
|
1577
|
+
...typeof obj.mime === "string" ? { mime: obj.mime } : {},
|
|
1578
|
+
...typeof obj.policy === "string" ? { policy: obj.policy } : {},
|
|
1579
|
+
...typeof obj.tag === "string" ? { tag: obj.tag } : {}
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
function assetMetadataEqual(a, b) {
|
|
1583
|
+
if (!a && !b) return true;
|
|
1584
|
+
if (!a || !b) return false;
|
|
1585
|
+
return stableStringify(assetMetaToJson(a)) === stableStringify(assetMetaToJson(b));
|
|
1586
|
+
}
|
|
1587
|
+
function cloneRepoAssetMetadata(meta) {
|
|
1588
|
+
return {
|
|
1589
|
+
assetId: meta.assetId,
|
|
1590
|
+
size: meta.size,
|
|
1591
|
+
createdAt: meta.createdAt,
|
|
1592
|
+
...meta.mime !== void 0 ? { mime: meta.mime } : {},
|
|
1593
|
+
...meta.policy !== void 0 ? { policy: meta.policy } : {},
|
|
1594
|
+
...meta.tag !== void 0 ? { tag: meta.tag } : {}
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
function toReadableStream(bytes) {
|
|
1598
|
+
return new ReadableStream({ start(controller) {
|
|
1599
|
+
controller.enqueue(bytes);
|
|
1600
|
+
controller.close();
|
|
1601
|
+
} });
|
|
1602
|
+
}
|
|
1603
|
+
function matchesQuery(docId, _metadata, query) {
|
|
1604
|
+
if (!query) return true;
|
|
1605
|
+
if (query.prefix && !docId.startsWith(query.prefix)) return false;
|
|
1606
|
+
if (query.start && docId < query.start) return false;
|
|
1607
|
+
if (query.end && docId > query.end) return false;
|
|
1608
|
+
return true;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1657
1611
|
//#endregion
|
|
1658
1612
|
//#region src/internal/metadata-manager.ts
|
|
1659
1613
|
var MetadataManager = class {
|
|
@@ -1895,13 +1849,11 @@ var AssetManager = class {
|
|
|
1895
1849
|
if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
|
|
1896
1850
|
const existing = this.assets.get(assetId);
|
|
1897
1851
|
if (existing) {
|
|
1898
|
-
if (
|
|
1899
|
-
|
|
1900
|
-
existing.data = clone;
|
|
1901
|
-
if (this.storage) await this.storage.save({
|
|
1852
|
+
if (this.storage) {
|
|
1853
|
+
if (!await this.storage.loadAsset(assetId)) await this.storage.save({
|
|
1902
1854
|
type: "asset",
|
|
1903
1855
|
assetId,
|
|
1904
|
-
data:
|
|
1856
|
+
data: bytes.slice()
|
|
1905
1857
|
});
|
|
1906
1858
|
}
|
|
1907
1859
|
let metadataMutated = false;
|
|
@@ -1928,7 +1880,7 @@ var AssetManager = class {
|
|
|
1928
1880
|
await this.persistMeta();
|
|
1929
1881
|
this.eventBus.emit({
|
|
1930
1882
|
kind: "asset-metadata",
|
|
1931
|
-
asset: this.createAssetDownload(assetId, metadata$1,
|
|
1883
|
+
asset: this.createAssetDownload(assetId, metadata$1, bytes),
|
|
1932
1884
|
by: "local"
|
|
1933
1885
|
});
|
|
1934
1886
|
}
|
|
@@ -1958,7 +1910,8 @@ var AssetManager = class {
|
|
|
1958
1910
|
assetId,
|
|
1959
1911
|
data: storedBytes.slice()
|
|
1960
1912
|
});
|
|
1961
|
-
this.rememberAsset(metadata
|
|
1913
|
+
this.rememberAsset(metadata);
|
|
1914
|
+
this.markAssetAsOrphan(assetId, metadata);
|
|
1962
1915
|
this.updateDocAssetMetadata(assetId, metadata);
|
|
1963
1916
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
1964
1917
|
await this.persistMeta();
|
|
@@ -1979,13 +1932,11 @@ var AssetManager = class {
|
|
|
1979
1932
|
const existing = this.assets.get(assetId);
|
|
1980
1933
|
if (existing) {
|
|
1981
1934
|
metadata = existing.metadata;
|
|
1982
|
-
if (
|
|
1983
|
-
|
|
1984
|
-
existing.data = clone;
|
|
1985
|
-
if (this.storage) await this.storage.save({
|
|
1935
|
+
if (this.storage) {
|
|
1936
|
+
if (!await this.storage.loadAsset(assetId)) await this.storage.save({
|
|
1986
1937
|
type: "asset",
|
|
1987
1938
|
assetId,
|
|
1988
|
-
data:
|
|
1939
|
+
data: bytes.slice()
|
|
1989
1940
|
});
|
|
1990
1941
|
}
|
|
1991
1942
|
let nextMetadata = metadata;
|
|
@@ -2025,11 +1976,11 @@ var AssetManager = class {
|
|
|
2025
1976
|
await this.persistMeta();
|
|
2026
1977
|
this.eventBus.emit({
|
|
2027
1978
|
kind: "asset-metadata",
|
|
2028
|
-
asset: this.createAssetDownload(assetId, metadata,
|
|
1979
|
+
asset: this.createAssetDownload(assetId, metadata, bytes),
|
|
2029
1980
|
by: "local"
|
|
2030
1981
|
});
|
|
2031
1982
|
} else metadata = existing.metadata;
|
|
2032
|
-
storedBytes =
|
|
1983
|
+
storedBytes = bytes;
|
|
2033
1984
|
this.rememberAsset(metadata);
|
|
2034
1985
|
} else {
|
|
2035
1986
|
metadata = {
|
|
@@ -2055,7 +2006,7 @@ var AssetManager = class {
|
|
|
2055
2006
|
assetId,
|
|
2056
2007
|
data: storedBytes.slice()
|
|
2057
2008
|
});
|
|
2058
|
-
this.rememberAsset(metadata
|
|
2009
|
+
this.rememberAsset(metadata);
|
|
2059
2010
|
this.updateDocAssetMetadata(assetId, metadata);
|
|
2060
2011
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
2061
2012
|
created = true;
|
|
@@ -2063,9 +2014,7 @@ var AssetManager = class {
|
|
|
2063
2014
|
const mapping = this.docAssets.get(docId) ?? /* @__PURE__ */ new Map();
|
|
2064
2015
|
mapping.set(assetId, metadata);
|
|
2065
2016
|
this.docAssets.set(docId, mapping);
|
|
2066
|
-
|
|
2067
|
-
refs.add(docId);
|
|
2068
|
-
this.assetToDocRefs.set(assetId, refs);
|
|
2017
|
+
this.addDocReference(assetId, docId);
|
|
2069
2018
|
this.metaFlock.put([
|
|
2070
2019
|
"ld",
|
|
2071
2020
|
docId,
|
|
@@ -2095,20 +2044,7 @@ var AssetManager = class {
|
|
|
2095
2044
|
docId,
|
|
2096
2045
|
assetId
|
|
2097
2046
|
]);
|
|
2098
|
-
|
|
2099
|
-
if (refs) {
|
|
2100
|
-
refs.delete(docId);
|
|
2101
|
-
if (refs.size === 0) {
|
|
2102
|
-
this.assetToDocRefs.delete(assetId);
|
|
2103
|
-
const record = this.assets.get(assetId);
|
|
2104
|
-
if (record) this.orphanedAssets.set(assetId, {
|
|
2105
|
-
metadata: record.metadata,
|
|
2106
|
-
deletedAt: Date.now()
|
|
2107
|
-
});
|
|
2108
|
-
this.metaFlock.delete(["a", assetId]);
|
|
2109
|
-
this.assets.delete(assetId);
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2047
|
+
this.removeDocAssetReference(assetId, docId);
|
|
2112
2048
|
await this.persistMeta();
|
|
2113
2049
|
this.eventBus.emit({
|
|
2114
2050
|
kind: "asset-unlink",
|
|
@@ -2195,12 +2131,11 @@ var AssetManager = class {
|
|
|
2195
2131
|
this.handleAssetRemoval(assetId, by);
|
|
2196
2132
|
return;
|
|
2197
2133
|
}
|
|
2198
|
-
|
|
2199
|
-
this.rememberAsset(metadata, existingData);
|
|
2134
|
+
this.rememberAsset(metadata);
|
|
2200
2135
|
this.updateDocAssetMetadata(assetId, cloneRepoAssetMetadata(metadata));
|
|
2201
2136
|
if (!previous || !assetMetadataEqual(previous.metadata, metadata)) this.eventBus.emit({
|
|
2202
2137
|
kind: "asset-metadata",
|
|
2203
|
-
asset: this.createAssetDownload(assetId, metadata
|
|
2138
|
+
asset: this.createAssetDownload(assetId, metadata),
|
|
2204
2139
|
by
|
|
2205
2140
|
});
|
|
2206
2141
|
}
|
|
@@ -2215,11 +2150,7 @@ var AssetManager = class {
|
|
|
2215
2150
|
if (typeof assetId !== "string") continue;
|
|
2216
2151
|
const metadata = assetMetaFromJson(row.value);
|
|
2217
2152
|
if (!metadata) continue;
|
|
2218
|
-
|
|
2219
|
-
nextAssets.set(assetId, {
|
|
2220
|
-
metadata,
|
|
2221
|
-
data: existing?.data
|
|
2222
|
-
});
|
|
2153
|
+
nextAssets.set(assetId, { metadata });
|
|
2223
2154
|
}
|
|
2224
2155
|
const nextDocAssets = /* @__PURE__ */ new Map();
|
|
2225
2156
|
const linkRows = this.metaFlock.scan({ prefix: ["ld"] });
|
|
@@ -2255,12 +2186,18 @@ var AssetManager = class {
|
|
|
2255
2186
|
this.assetToDocRefs.set(assetId, refs);
|
|
2256
2187
|
}
|
|
2257
2188
|
this.assets.clear();
|
|
2258
|
-
for (const record of nextAssets.values()) this.rememberAsset(record.metadata
|
|
2189
|
+
for (const record of nextAssets.values()) this.rememberAsset(record.metadata);
|
|
2190
|
+
for (const assetId of nextAssets.keys()) {
|
|
2191
|
+
const refs = this.assetToDocRefs.get(assetId);
|
|
2192
|
+
if (!refs || refs.size === 0) {
|
|
2193
|
+
if (!this.orphanedAssets.has(assetId)) this.markAssetAsOrphan(assetId, nextAssets.get(assetId).metadata);
|
|
2194
|
+
} else this.orphanedAssets.delete(assetId);
|
|
2195
|
+
}
|
|
2259
2196
|
for (const [assetId, record] of nextAssets) {
|
|
2260
2197
|
const previous = prevAssets.get(assetId)?.metadata;
|
|
2261
2198
|
if (!assetMetadataEqual(previous, record.metadata)) this.eventBus.emit({
|
|
2262
2199
|
kind: "asset-metadata",
|
|
2263
|
-
asset: this.createAssetDownload(assetId, record.metadata
|
|
2200
|
+
asset: this.createAssetDownload(assetId, record.metadata),
|
|
2264
2201
|
by
|
|
2265
2202
|
});
|
|
2266
2203
|
}
|
|
@@ -2356,36 +2293,24 @@ var AssetManager = class {
|
|
|
2356
2293
|
};
|
|
2357
2294
|
}
|
|
2358
2295
|
async materializeAsset(assetId) {
|
|
2359
|
-
|
|
2360
|
-
if (record?.data) return {
|
|
2361
|
-
metadata: record.metadata,
|
|
2362
|
-
bytes: record.data.slice()
|
|
2363
|
-
};
|
|
2296
|
+
const record = this.assets.get(assetId);
|
|
2364
2297
|
if (record && this.storage) {
|
|
2365
2298
|
const stored = await this.storage.loadAsset(assetId);
|
|
2366
|
-
if (stored) {
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
metadata: record.metadata,
|
|
2371
|
-
bytes: clone
|
|
2372
|
-
};
|
|
2373
|
-
}
|
|
2299
|
+
if (stored) return {
|
|
2300
|
+
metadata: record.metadata,
|
|
2301
|
+
bytes: stored
|
|
2302
|
+
};
|
|
2374
2303
|
}
|
|
2375
2304
|
if (!record && this.storage) {
|
|
2376
2305
|
const stored = await this.storage.loadAsset(assetId);
|
|
2377
2306
|
if (stored) {
|
|
2378
2307
|
const metadata$1 = this.getAssetMetadata(assetId);
|
|
2379
2308
|
if (!metadata$1) throw new Error(`Missing metadata for asset ${assetId}`);
|
|
2380
|
-
|
|
2381
|
-
this.assets.set(assetId, {
|
|
2382
|
-
metadata: metadata$1,
|
|
2383
|
-
data: clone.slice()
|
|
2384
|
-
});
|
|
2309
|
+
this.assets.set(assetId, { metadata: metadata$1 });
|
|
2385
2310
|
this.updateDocAssetMetadata(assetId, metadata$1);
|
|
2386
2311
|
return {
|
|
2387
2312
|
metadata: metadata$1,
|
|
2388
|
-
bytes:
|
|
2313
|
+
bytes: stored
|
|
2389
2314
|
};
|
|
2390
2315
|
}
|
|
2391
2316
|
}
|
|
@@ -2401,10 +2326,7 @@ var AssetManager = class {
|
|
|
2401
2326
|
...remote.policy ? { policy: remote.policy } : {},
|
|
2402
2327
|
...remote.tag ? { tag: remote.tag } : {}
|
|
2403
2328
|
};
|
|
2404
|
-
this.assets.set(assetId, {
|
|
2405
|
-
metadata,
|
|
2406
|
-
data: remoteBytes.slice()
|
|
2407
|
-
});
|
|
2329
|
+
this.assets.set(assetId, { metadata });
|
|
2408
2330
|
this.updateDocAssetMetadata(assetId, metadata);
|
|
2409
2331
|
this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
|
|
2410
2332
|
await this.persistMeta();
|
|
@@ -2426,18 +2348,14 @@ var AssetManager = class {
|
|
|
2426
2348
|
if (assets) assets.set(assetId, metadata);
|
|
2427
2349
|
}
|
|
2428
2350
|
}
|
|
2429
|
-
rememberAsset(metadata
|
|
2430
|
-
|
|
2431
|
-
this.assets.set(metadata.assetId, {
|
|
2432
|
-
metadata,
|
|
2433
|
-
data
|
|
2434
|
-
});
|
|
2435
|
-
this.orphanedAssets.delete(metadata.assetId);
|
|
2351
|
+
rememberAsset(metadata) {
|
|
2352
|
+
this.assets.set(metadata.assetId, { metadata });
|
|
2436
2353
|
}
|
|
2437
2354
|
addDocReference(assetId, docId) {
|
|
2438
2355
|
const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
|
|
2439
2356
|
refs.add(docId);
|
|
2440
2357
|
this.assetToDocRefs.set(assetId, refs);
|
|
2358
|
+
this.orphanedAssets.delete(assetId);
|
|
2441
2359
|
}
|
|
2442
2360
|
removeDocAssetReference(assetId, docId) {
|
|
2443
2361
|
const refs = this.assetToDocRefs.get(assetId);
|
|
@@ -2487,13 +2405,11 @@ var FlockHydrator = class {
|
|
|
2487
2405
|
const nextMetadata = this.readAllDocMetadata();
|
|
2488
2406
|
this.metadataManager.replaceAll(nextMetadata, by);
|
|
2489
2407
|
this.assetManager.hydrateFromFlock(by);
|
|
2490
|
-
this.docManager.hydrateFrontierKeys();
|
|
2491
2408
|
}
|
|
2492
2409
|
applyEvents(events, by) {
|
|
2493
2410
|
if (!events.length) return;
|
|
2494
2411
|
const docMetadataIds = /* @__PURE__ */ new Set();
|
|
2495
2412
|
const docAssetIds = /* @__PURE__ */ new Set();
|
|
2496
|
-
const docFrontiersIds = /* @__PURE__ */ new Set();
|
|
2497
2413
|
const assetIds = /* @__PURE__ */ new Set();
|
|
2498
2414
|
for (const event of events) {
|
|
2499
2415
|
const key = event.key;
|
|
@@ -2510,15 +2426,11 @@ var FlockHydrator = class {
|
|
|
2510
2426
|
const assetId = key[2];
|
|
2511
2427
|
if (typeof docId === "string") docAssetIds.add(docId);
|
|
2512
2428
|
if (typeof assetId === "string") assetIds.add(assetId);
|
|
2513
|
-
} else if (root === "f") {
|
|
2514
|
-
const docId = key[1];
|
|
2515
|
-
if (typeof docId === "string") docFrontiersIds.add(docId);
|
|
2516
2429
|
}
|
|
2517
2430
|
}
|
|
2518
2431
|
for (const assetId of assetIds) this.assetManager.refreshAssetMetadataEntry(assetId, by);
|
|
2519
2432
|
for (const docId of docMetadataIds) this.metadataManager.refreshFromFlock(docId, by);
|
|
2520
2433
|
for (const docId of docAssetIds) this.assetManager.refreshDocAssetsEntry(docId, by);
|
|
2521
|
-
for (const docId of docFrontiersIds) this.docManager.refreshDocFrontierKeys(docId);
|
|
2522
2434
|
}
|
|
2523
2435
|
readAllDocMetadata() {
|
|
2524
2436
|
const nextMetadata = /* @__PURE__ */ new Map();
|
|
@@ -2729,8 +2641,7 @@ function createRepoState() {
|
|
|
2729
2641
|
docAssets: /* @__PURE__ */ new Map(),
|
|
2730
2642
|
assets: /* @__PURE__ */ new Map(),
|
|
2731
2643
|
orphanedAssets: /* @__PURE__ */ new Map(),
|
|
2732
|
-
assetToDocRefs: /* @__PURE__ */ new Map()
|
|
2733
|
-
docFrontierKeys: /* @__PURE__ */ new Map()
|
|
2644
|
+
assetToDocRefs: /* @__PURE__ */ new Map()
|
|
2734
2645
|
};
|
|
2735
2646
|
}
|
|
2736
2647
|
|
|
@@ -2766,8 +2677,7 @@ var LoroRepo = class LoroRepo {
|
|
|
2766
2677
|
docFrontierDebounceMs,
|
|
2767
2678
|
getMetaFlock: () => this.metaFlock,
|
|
2768
2679
|
eventBus: this.eventBus,
|
|
2769
|
-
persistMeta: () => this.persistMeta()
|
|
2770
|
-
state: this.state
|
|
2680
|
+
persistMeta: () => this.persistMeta()
|
|
2771
2681
|
});
|
|
2772
2682
|
this.metadataManager = new MetadataManager({
|
|
2773
2683
|
getMetaFlock: () => this.metaFlock,
|
|
@@ -2856,7 +2766,7 @@ var LoroRepo = class LoroRepo {
|
|
|
2856
2766
|
*/
|
|
2857
2767
|
async openPersistedDoc(docId) {
|
|
2858
2768
|
return {
|
|
2859
|
-
doc: await this.docManager.
|
|
2769
|
+
doc: await this.docManager.openPersistedDoc(docId),
|
|
2860
2770
|
syncOnce: () => {
|
|
2861
2771
|
return this.sync({
|
|
2862
2772
|
scope: "doc",
|