@xnetjs/react 0.0.2 → 0.0.3
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 +206 -2
- package/dist/chunk-4ND4LERM.js +4333 -0
- package/dist/chunk-6VOICQZ3.js +760 -0
- package/dist/chunk-EJ5RW5GI.js +93 -0
- package/dist/chunk-IHTMVTTE.js +1108 -0
- package/dist/chunk-JCOFKBOB.js +11 -0
- package/dist/chunk-KSHTDZ2V.js +893 -0
- package/dist/chunk-QHNYQVUM.js +989 -0
- package/dist/context-CFu9i136.d.ts +392 -0
- package/dist/core.d.ts +372 -0
- package/dist/core.js +29 -0
- package/dist/database.d.ts +383 -0
- package/dist/database.js +19 -0
- package/dist/experimental-Dmz4kYvX.d.ts +1560 -0
- package/dist/experimental.d.ts +15 -0
- package/dist/experimental.js +248 -0
- package/dist/index.d.ts +601 -2700
- package/dist/index.js +4781 -4427
- package/dist/{instrumentation-Cn94kn8-.d.ts → instrumentation-CpIuG2y5.d.ts} +32 -1
- package/dist/internal.d.ts +32 -22
- package/dist/internal.js +16 -4
- package/dist/telemetry-context-B7r6H1KW.d.ts +35 -0
- package/dist/useNodeStore-DTCSBF51.d.ts +28 -0
- package/dist/useQuery-D7ajycrc.d.ts +302 -0
- package/package.json +28 -10
- package/dist/chunk-CWHCGYDW.js +0 -2238
package/dist/chunk-CWHCGYDW.js
DELETED
|
@@ -1,2238 +0,0 @@
|
|
|
1
|
-
// src/context/security-context.tsx
|
|
2
|
-
import { DEFAULT_SECURITY_LEVEL } from "@xnetjs/crypto";
|
|
3
|
-
import { MemoryPQKeyRegistry } from "@xnetjs/identity";
|
|
4
|
-
import { createContext, useContext, useState, useMemo, useEffect } from "react";
|
|
5
|
-
import { jsx } from "react/jsx-runtime";
|
|
6
|
-
var SecurityContext = createContext(null);
|
|
7
|
-
function SecurityProvider({
|
|
8
|
-
children,
|
|
9
|
-
level: initialLevel = DEFAULT_SECURITY_LEVEL,
|
|
10
|
-
minVerificationLevel: initialMinLevel = 0,
|
|
11
|
-
verificationPolicy: initialPolicy = "strict",
|
|
12
|
-
registry: providedRegistry,
|
|
13
|
-
keyBundle: initialBundle
|
|
14
|
-
}) {
|
|
15
|
-
const [level, setLevel] = useState(initialLevel);
|
|
16
|
-
const [minVerificationLevel, setMinVerificationLevel] = useState(initialMinLevel);
|
|
17
|
-
const [verificationPolicy, setVerificationPolicy] = useState(
|
|
18
|
-
initialPolicy
|
|
19
|
-
);
|
|
20
|
-
const [keyBundle, setKeyBundle] = useState(initialBundle);
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
setLevel(initialLevel);
|
|
23
|
-
}, [initialLevel]);
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
setMinVerificationLevel(initialMinLevel);
|
|
26
|
-
}, [initialMinLevel]);
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
setVerificationPolicy(initialPolicy);
|
|
29
|
-
}, [initialPolicy]);
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
setKeyBundle(initialBundle);
|
|
32
|
-
}, [initialBundle]);
|
|
33
|
-
const registry = useMemo(() => providedRegistry ?? new MemoryPQKeyRegistry(), [providedRegistry]);
|
|
34
|
-
const value = useMemo(
|
|
35
|
-
() => ({
|
|
36
|
-
level,
|
|
37
|
-
minVerificationLevel,
|
|
38
|
-
verificationPolicy,
|
|
39
|
-
registry,
|
|
40
|
-
keyBundle,
|
|
41
|
-
setLevel,
|
|
42
|
-
setMinVerificationLevel,
|
|
43
|
-
setVerificationPolicy,
|
|
44
|
-
setKeyBundle
|
|
45
|
-
}),
|
|
46
|
-
[level, minVerificationLevel, verificationPolicy, registry, keyBundle]
|
|
47
|
-
);
|
|
48
|
-
return /* @__PURE__ */ jsx(SecurityContext.Provider, { value, children });
|
|
49
|
-
}
|
|
50
|
-
function useSecurityContext() {
|
|
51
|
-
const context = useContext(SecurityContext);
|
|
52
|
-
if (!context) {
|
|
53
|
-
throw new Error("useSecurityContext must be used within SecurityProvider");
|
|
54
|
-
}
|
|
55
|
-
return context;
|
|
56
|
-
}
|
|
57
|
-
function useSecurityContextOptional() {
|
|
58
|
-
return useContext(SecurityContext);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// src/context/telemetry-context.ts
|
|
62
|
-
import { createContext as createContext2, useContext as useContext2 } from "react";
|
|
63
|
-
var TelemetryContext = createContext2(null);
|
|
64
|
-
function useTelemetryReporter() {
|
|
65
|
-
return useContext2(TelemetryContext);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// src/hooks/usePlugins.ts
|
|
69
|
-
import { createContext as createContext3, useContext as useContext3, useState as useState2, useEffect as useEffect2 } from "react";
|
|
70
|
-
var PluginRegistryContext = createContext3(null);
|
|
71
|
-
function usePluginRegistry() {
|
|
72
|
-
const registry = useContext3(PluginRegistryContext);
|
|
73
|
-
if (!registry) {
|
|
74
|
-
throw new Error(
|
|
75
|
-
"usePluginRegistry must be used within XNetProvider with plugins enabled. Ensure XNetProvider has platform: 'electron' or platform: 'web' in its config."
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
return registry;
|
|
79
|
-
}
|
|
80
|
-
function usePluginRegistryOptional() {
|
|
81
|
-
return useContext3(PluginRegistryContext);
|
|
82
|
-
}
|
|
83
|
-
function usePlugins() {
|
|
84
|
-
const registry = usePluginRegistry();
|
|
85
|
-
const [plugins, setPlugins] = useState2(() => registry.getAll());
|
|
86
|
-
useEffect2(() => {
|
|
87
|
-
setPlugins(registry.getAll());
|
|
88
|
-
const disposable = registry.onChange(() => {
|
|
89
|
-
setPlugins(registry.getAll());
|
|
90
|
-
});
|
|
91
|
-
return () => disposable.dispose();
|
|
92
|
-
}, [registry]);
|
|
93
|
-
return plugins;
|
|
94
|
-
}
|
|
95
|
-
function getTypedRegistry(contributions, type) {
|
|
96
|
-
switch (type) {
|
|
97
|
-
case "views":
|
|
98
|
-
return contributions.views;
|
|
99
|
-
case "commands":
|
|
100
|
-
return contributions.commands;
|
|
101
|
-
case "slashCommands":
|
|
102
|
-
return contributions.slashCommands;
|
|
103
|
-
case "sidebar":
|
|
104
|
-
return contributions.sidebar;
|
|
105
|
-
case "editor":
|
|
106
|
-
return contributions.editor;
|
|
107
|
-
case "propertyHandlers":
|
|
108
|
-
return contributions.propertyHandlers;
|
|
109
|
-
case "blocks":
|
|
110
|
-
return contributions.blocks;
|
|
111
|
-
case "settings":
|
|
112
|
-
return contributions.settings;
|
|
113
|
-
default:
|
|
114
|
-
throw new Error(`Unknown contribution type: ${type}`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function useContributions(type) {
|
|
118
|
-
const registry = usePluginRegistry();
|
|
119
|
-
const contributions = registry.getContributions();
|
|
120
|
-
const typedRegistry = getTypedRegistry(contributions, type);
|
|
121
|
-
const [items, setItems] = useState2(() => typedRegistry.getAll());
|
|
122
|
-
useEffect2(() => {
|
|
123
|
-
setItems(typedRegistry.getAll());
|
|
124
|
-
const unsubscribe = typedRegistry.onChange(() => {
|
|
125
|
-
setItems(typedRegistry.getAll());
|
|
126
|
-
});
|
|
127
|
-
return unsubscribe;
|
|
128
|
-
}, [typedRegistry]);
|
|
129
|
-
return items;
|
|
130
|
-
}
|
|
131
|
-
function useViews() {
|
|
132
|
-
return useContributions("views");
|
|
133
|
-
}
|
|
134
|
-
function useCommands() {
|
|
135
|
-
return useContributions("commands");
|
|
136
|
-
}
|
|
137
|
-
function useSlashCommands() {
|
|
138
|
-
return useContributions("slashCommands");
|
|
139
|
-
}
|
|
140
|
-
function useSidebarItems() {
|
|
141
|
-
return useContributions("sidebar");
|
|
142
|
-
}
|
|
143
|
-
function useEditorExtensions() {
|
|
144
|
-
return useContributions("editor");
|
|
145
|
-
}
|
|
146
|
-
function useEditorExtensionsSafe() {
|
|
147
|
-
const registry = usePluginRegistryOptional();
|
|
148
|
-
const [items, setItems] = useState2([]);
|
|
149
|
-
useEffect2(() => {
|
|
150
|
-
if (!registry) {
|
|
151
|
-
setItems([]);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const contributions = registry.getContributions();
|
|
155
|
-
const typedRegistry = contributions.editor;
|
|
156
|
-
setItems(typedRegistry.getAll());
|
|
157
|
-
const unsubscribe = typedRegistry.onChange(() => {
|
|
158
|
-
setItems(typedRegistry.getAll());
|
|
159
|
-
});
|
|
160
|
-
return unsubscribe;
|
|
161
|
-
}, [registry]);
|
|
162
|
-
return items;
|
|
163
|
-
}
|
|
164
|
-
function useView(type) {
|
|
165
|
-
const views = useViews();
|
|
166
|
-
return views.find((v) => v.type === type);
|
|
167
|
-
}
|
|
168
|
-
function useCommand(id) {
|
|
169
|
-
const commands = useCommands();
|
|
170
|
-
return commands.find((c) => c.id === id);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// src/sync/blob-sync.ts
|
|
174
|
-
var BLOB_SYNC_ROOM = "xnet-blob-sync";
|
|
175
|
-
var MAX_INLINE_SIZE = 256 * 1024;
|
|
176
|
-
function createBlobSyncProvider(config) {
|
|
177
|
-
const { blobStore, connection, onBlobReceived } = config;
|
|
178
|
-
let cleanup = null;
|
|
179
|
-
const pendingRequests = /* @__PURE__ */ new Set();
|
|
180
|
-
function toBase642(data) {
|
|
181
|
-
let binary = "";
|
|
182
|
-
for (let i = 0; i < data.length; i++) {
|
|
183
|
-
binary += String.fromCharCode(data[i]);
|
|
184
|
-
}
|
|
185
|
-
return btoa(binary);
|
|
186
|
-
}
|
|
187
|
-
function fromBase64(str) {
|
|
188
|
-
const binary = atob(str);
|
|
189
|
-
const bytes = new Uint8Array(binary.length);
|
|
190
|
-
for (let i = 0; i < binary.length; i++) {
|
|
191
|
-
bytes[i] = binary.charCodeAt(i);
|
|
192
|
-
}
|
|
193
|
-
return bytes;
|
|
194
|
-
}
|
|
195
|
-
async function handleMessage(data) {
|
|
196
|
-
const msg = data;
|
|
197
|
-
switch (msg.type) {
|
|
198
|
-
case "blob-want": {
|
|
199
|
-
for (const cid of msg.cids) {
|
|
200
|
-
const blobData = await blobStore.get(cid);
|
|
201
|
-
if (blobData) {
|
|
202
|
-
if (blobData.byteLength <= MAX_INLINE_SIZE) {
|
|
203
|
-
connection.publish(BLOB_SYNC_ROOM, {
|
|
204
|
-
type: "blob-data",
|
|
205
|
-
cid,
|
|
206
|
-
data: toBase642(blobData)
|
|
207
|
-
});
|
|
208
|
-
} else {
|
|
209
|
-
connection.publish(BLOB_SYNC_ROOM, {
|
|
210
|
-
type: "blob-data",
|
|
211
|
-
cid,
|
|
212
|
-
data: toBase642(blobData)
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
} else {
|
|
216
|
-
connection.publish(BLOB_SYNC_ROOM, {
|
|
217
|
-
type: "blob-not-found",
|
|
218
|
-
cid
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
break;
|
|
223
|
-
}
|
|
224
|
-
case "blob-data": {
|
|
225
|
-
const blobData = fromBase64(msg.data);
|
|
226
|
-
await blobStore.put(blobData);
|
|
227
|
-
pendingRequests.delete(msg.cid);
|
|
228
|
-
onBlobReceived?.(msg.cid);
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
case "blob-not-found": {
|
|
232
|
-
pendingRequests.delete(msg.cid);
|
|
233
|
-
break;
|
|
234
|
-
}
|
|
235
|
-
case "blob-have": {
|
|
236
|
-
const needed = [];
|
|
237
|
-
for (const cid of msg.cids) {
|
|
238
|
-
if (!await blobStore.has(cid)) {
|
|
239
|
-
needed.push(cid);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
if (needed.length > 0) {
|
|
243
|
-
connection.publish(BLOB_SYNC_ROOM, {
|
|
244
|
-
type: "blob-want",
|
|
245
|
-
cids: needed
|
|
246
|
-
});
|
|
247
|
-
for (const cid of needed) {
|
|
248
|
-
pendingRequests.add(cid);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
break;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
return {
|
|
256
|
-
start() {
|
|
257
|
-
if (cleanup) return;
|
|
258
|
-
cleanup = connection.joinRoom(BLOB_SYNC_ROOM, (data) => {
|
|
259
|
-
handleMessage(data);
|
|
260
|
-
});
|
|
261
|
-
},
|
|
262
|
-
stop() {
|
|
263
|
-
if (cleanup) {
|
|
264
|
-
cleanup();
|
|
265
|
-
cleanup = null;
|
|
266
|
-
}
|
|
267
|
-
pendingRequests.clear();
|
|
268
|
-
},
|
|
269
|
-
async requestBlobs(cids) {
|
|
270
|
-
const missing = [];
|
|
271
|
-
for (const cid of cids) {
|
|
272
|
-
if (!await blobStore.has(cid)) {
|
|
273
|
-
missing.push(cid);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (missing.length > 0) {
|
|
277
|
-
connection.publish(BLOB_SYNC_ROOM, {
|
|
278
|
-
type: "blob-want",
|
|
279
|
-
cids: missing
|
|
280
|
-
});
|
|
281
|
-
for (const cid of missing) {
|
|
282
|
-
pendingRequests.add(cid);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
},
|
|
286
|
-
announceHave(cids) {
|
|
287
|
-
if (cids.length > 0) {
|
|
288
|
-
connection.publish(BLOB_SYNC_ROOM, {
|
|
289
|
-
type: "blob-have",
|
|
290
|
-
cids
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
},
|
|
294
|
-
get pendingCount() {
|
|
295
|
-
return pendingRequests.size;
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// src/sync/connection-manager.ts
|
|
301
|
-
function log(...args) {
|
|
302
|
-
if (typeof localStorage !== "undefined" && localStorage.getItem("xnet:sync:debug") === "true") {
|
|
303
|
-
console.log("[ConnectionManager]", ...args);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
function createConnectionManager(config) {
|
|
307
|
-
let ws = null;
|
|
308
|
-
let status = "disconnected";
|
|
309
|
-
let reconnectAttempts = 0;
|
|
310
|
-
let reconnectTimer = null;
|
|
311
|
-
let destroyed = false;
|
|
312
|
-
let connectInProgress = false;
|
|
313
|
-
const reconnectDelay = config.reconnectDelay ?? 2e3;
|
|
314
|
-
const maxReconnects = config.maxReconnects ?? Infinity;
|
|
315
|
-
const rooms = /* @__PURE__ */ new Map();
|
|
316
|
-
const statusListeners = /* @__PURE__ */ new Set();
|
|
317
|
-
const messageListeners = /* @__PURE__ */ new Set();
|
|
318
|
-
const pendingSubscriptions = /* @__PURE__ */ new Map();
|
|
319
|
-
function setStatus(s) {
|
|
320
|
-
log("Status changed:", status, "->", s);
|
|
321
|
-
status = s;
|
|
322
|
-
for (const handler of statusListeners) {
|
|
323
|
-
try {
|
|
324
|
-
handler(s);
|
|
325
|
-
} catch {
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
function send(msg) {
|
|
330
|
-
if (ws?.readyState === WebSocket.OPEN) {
|
|
331
|
-
log("Sending:", msg);
|
|
332
|
-
ws.send(JSON.stringify(msg));
|
|
333
|
-
} else {
|
|
334
|
-
log("Cannot send, WebSocket not open. readyState:", ws?.readyState);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
function handleMessage(event) {
|
|
338
|
-
try {
|
|
339
|
-
const msg = JSON.parse(event.data);
|
|
340
|
-
if (msg.type === "pong") return;
|
|
341
|
-
log("Received message:", msg.type, msg.topic ? `topic=${msg.topic}` : "");
|
|
342
|
-
if (msg.type === "subscribed" && Array.isArray(msg.topics)) {
|
|
343
|
-
for (const topic of msg.topics) {
|
|
344
|
-
const pending = pendingSubscriptions.get(topic);
|
|
345
|
-
if (pending) {
|
|
346
|
-
log("Subscription confirmed for room:", topic);
|
|
347
|
-
pending.resolve();
|
|
348
|
-
pendingSubscriptions.delete(topic);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
if (msg.type === "publish" && msg.topic) {
|
|
354
|
-
const handlers = rooms.get(msg.topic);
|
|
355
|
-
if (handlers) {
|
|
356
|
-
log("Dispatching to", handlers.size, "handler(s) for room:", msg.topic);
|
|
357
|
-
for (const handler of handlers) {
|
|
358
|
-
try {
|
|
359
|
-
handler(msg.data);
|
|
360
|
-
} catch (err) {
|
|
361
|
-
log("Handler error:", err);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
} else {
|
|
365
|
-
log("No handlers for room:", msg.topic);
|
|
366
|
-
}
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
if (msg && typeof msg === "object") {
|
|
370
|
-
for (const handler of messageListeners) {
|
|
371
|
-
try {
|
|
372
|
-
handler(msg);
|
|
373
|
-
} catch {
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
} catch (err) {
|
|
378
|
-
log("Failed to parse message:", err);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
async function resolveAuthToken() {
|
|
382
|
-
let token = config.ucanToken ?? "";
|
|
383
|
-
if (!token && config.getUCANToken) {
|
|
384
|
-
try {
|
|
385
|
-
token = await config.getUCANToken();
|
|
386
|
-
} catch {
|
|
387
|
-
token = "";
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
return token;
|
|
391
|
-
}
|
|
392
|
-
function buildProtocols(token) {
|
|
393
|
-
const protocols = ["xnet-sync.v1"];
|
|
394
|
-
if (token && !/[\s,]/.test(token)) {
|
|
395
|
-
protocols.push(`xnet-auth.${token}`);
|
|
396
|
-
}
|
|
397
|
-
return protocols;
|
|
398
|
-
}
|
|
399
|
-
async function doConnect() {
|
|
400
|
-
if (destroyed) {
|
|
401
|
-
log("doConnect called but manager is destroyed");
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
if (connectInProgress) {
|
|
405
|
-
log("doConnect called but connection already in progress");
|
|
406
|
-
return;
|
|
407
|
-
}
|
|
408
|
-
connectInProgress = true;
|
|
409
|
-
log("Connecting to:", config.url);
|
|
410
|
-
setStatus("connecting");
|
|
411
|
-
try {
|
|
412
|
-
const token = await resolveAuthToken();
|
|
413
|
-
ws = new WebSocket(config.url, buildProtocols(token));
|
|
414
|
-
ws.onopen = () => {
|
|
415
|
-
connectInProgress = false;
|
|
416
|
-
log("WebSocket connected");
|
|
417
|
-
setStatus("connected");
|
|
418
|
-
reconnectAttempts = 0;
|
|
419
|
-
if (rooms.size > 0) {
|
|
420
|
-
const roomList = Array.from(rooms.keys());
|
|
421
|
-
log("Re-subscribing to", roomList.length, "room(s):", roomList);
|
|
422
|
-
send({ type: "subscribe", topics: roomList });
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
ws.onmessage = handleMessage;
|
|
426
|
-
ws.onclose = (event) => {
|
|
427
|
-
connectInProgress = false;
|
|
428
|
-
log("WebSocket closed, code:", event.code, "reason:", event.reason || "(none)");
|
|
429
|
-
ws = null;
|
|
430
|
-
setStatus("disconnected");
|
|
431
|
-
scheduleReconnect();
|
|
432
|
-
};
|
|
433
|
-
ws.onerror = (event) => {
|
|
434
|
-
connectInProgress = false;
|
|
435
|
-
log("WebSocket error:", event);
|
|
436
|
-
setStatus("error");
|
|
437
|
-
};
|
|
438
|
-
} catch (err) {
|
|
439
|
-
connectInProgress = false;
|
|
440
|
-
log("Failed to create WebSocket:", err);
|
|
441
|
-
setStatus("error");
|
|
442
|
-
scheduleReconnect();
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
function scheduleReconnect() {
|
|
446
|
-
if (destroyed || reconnectAttempts >= maxReconnects) return;
|
|
447
|
-
if (reconnectTimer) return;
|
|
448
|
-
reconnectAttempts++;
|
|
449
|
-
reconnectTimer = setTimeout(() => {
|
|
450
|
-
reconnectTimer = null;
|
|
451
|
-
void doConnect();
|
|
452
|
-
}, reconnectDelay);
|
|
453
|
-
}
|
|
454
|
-
return {
|
|
455
|
-
get status() {
|
|
456
|
-
return status;
|
|
457
|
-
},
|
|
458
|
-
get roomCount() {
|
|
459
|
-
return rooms.size;
|
|
460
|
-
},
|
|
461
|
-
connect() {
|
|
462
|
-
destroyed = false;
|
|
463
|
-
void doConnect();
|
|
464
|
-
},
|
|
465
|
-
disconnect() {
|
|
466
|
-
destroyed = true;
|
|
467
|
-
if (reconnectTimer) {
|
|
468
|
-
clearTimeout(reconnectTimer);
|
|
469
|
-
reconnectTimer = null;
|
|
470
|
-
}
|
|
471
|
-
if (ws) {
|
|
472
|
-
if (rooms.size > 0) {
|
|
473
|
-
send({ type: "unsubscribe", topics: Array.from(rooms.keys()) });
|
|
474
|
-
}
|
|
475
|
-
ws.close(1e3, "Client disconnect");
|
|
476
|
-
ws = null;
|
|
477
|
-
}
|
|
478
|
-
setStatus("disconnected");
|
|
479
|
-
},
|
|
480
|
-
joinRoom(room, handler) {
|
|
481
|
-
log("Joining room:", room);
|
|
482
|
-
let handlers = rooms.get(room);
|
|
483
|
-
if (!handlers) {
|
|
484
|
-
handlers = /* @__PURE__ */ new Set();
|
|
485
|
-
rooms.set(room, handlers);
|
|
486
|
-
log("New room subscription, sending subscribe message");
|
|
487
|
-
send({ type: "subscribe", topics: [room] });
|
|
488
|
-
}
|
|
489
|
-
handlers.add(handler);
|
|
490
|
-
log("Room", room, "now has", handlers.size, "handler(s)");
|
|
491
|
-
return () => {
|
|
492
|
-
log("Leaving room:", room);
|
|
493
|
-
handlers.delete(handler);
|
|
494
|
-
if (handlers.size === 0) {
|
|
495
|
-
rooms.delete(room);
|
|
496
|
-
pendingSubscriptions.delete(room);
|
|
497
|
-
send({ type: "unsubscribe", topics: [room] });
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
},
|
|
501
|
-
joinRoomAsync(room, handler) {
|
|
502
|
-
log("Joining room async:", room);
|
|
503
|
-
let handlers = rooms.get(room);
|
|
504
|
-
const isNewRoom = !handlers;
|
|
505
|
-
if (!handlers) {
|
|
506
|
-
handlers = /* @__PURE__ */ new Set();
|
|
507
|
-
rooms.set(room, handlers);
|
|
508
|
-
}
|
|
509
|
-
handlers.add(handler);
|
|
510
|
-
log("Room", room, "now has", handlers.size, "handler(s)");
|
|
511
|
-
let ready;
|
|
512
|
-
if (isNewRoom && status === "connected") {
|
|
513
|
-
log("New room subscription (async), sending subscribe message");
|
|
514
|
-
ready = new Promise((resolve, _reject) => {
|
|
515
|
-
pendingSubscriptions.set(room, { resolve, reject: () => resolve() });
|
|
516
|
-
send({ type: "subscribe", topics: [room] });
|
|
517
|
-
setTimeout(() => {
|
|
518
|
-
if (pendingSubscriptions.has(room)) {
|
|
519
|
-
pendingSubscriptions.delete(room);
|
|
520
|
-
log("Subscription confirmation timeout for room:", room, "- proceeding anyway");
|
|
521
|
-
resolve();
|
|
522
|
-
}
|
|
523
|
-
}, 5e3);
|
|
524
|
-
});
|
|
525
|
-
} else {
|
|
526
|
-
if (isNewRoom) {
|
|
527
|
-
log("New room added but not connected, will subscribe when connected");
|
|
528
|
-
}
|
|
529
|
-
ready = Promise.resolve();
|
|
530
|
-
}
|
|
531
|
-
const unsubscribe = () => {
|
|
532
|
-
log("Leaving room:", room);
|
|
533
|
-
handlers.delete(handler);
|
|
534
|
-
if (handlers.size === 0) {
|
|
535
|
-
rooms.delete(room);
|
|
536
|
-
pendingSubscriptions.delete(room);
|
|
537
|
-
send({ type: "unsubscribe", topics: [room] });
|
|
538
|
-
}
|
|
539
|
-
};
|
|
540
|
-
return { unsubscribe, ready };
|
|
541
|
-
},
|
|
542
|
-
leaveRoom(room) {
|
|
543
|
-
rooms.delete(room);
|
|
544
|
-
send({ type: "unsubscribe", topics: [room] });
|
|
545
|
-
},
|
|
546
|
-
publish(room, data) {
|
|
547
|
-
log("Publishing to room:", room, "type:", data.type);
|
|
548
|
-
send({ type: "publish", topic: room, data });
|
|
549
|
-
},
|
|
550
|
-
sendRaw(message) {
|
|
551
|
-
send(message);
|
|
552
|
-
},
|
|
553
|
-
onMessage(handler) {
|
|
554
|
-
messageListeners.add(handler);
|
|
555
|
-
return () => messageListeners.delete(handler);
|
|
556
|
-
},
|
|
557
|
-
onStatus(handler) {
|
|
558
|
-
statusListeners.add(handler);
|
|
559
|
-
return () => statusListeners.delete(handler);
|
|
560
|
-
}
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
// src/sync/meta-bridge.ts
|
|
565
|
-
var METABRIDGE_ORIGIN = "metabridge";
|
|
566
|
-
var METABRIDGE_SEED_ORIGIN = "metabridge-seed";
|
|
567
|
-
function createMetaBridge(store, options) {
|
|
568
|
-
const warnOnExternal = options?.warnOnExternalMetaChanges ?? true;
|
|
569
|
-
function writePropertiesToMeta(doc, properties, origin) {
|
|
570
|
-
const metaMap = doc.getMap("meta");
|
|
571
|
-
doc.transact(() => {
|
|
572
|
-
for (const [key, value] of Object.entries(properties)) {
|
|
573
|
-
if (key.startsWith("_")) continue;
|
|
574
|
-
metaMap.set(key, value);
|
|
575
|
-
}
|
|
576
|
-
}, origin);
|
|
577
|
-
}
|
|
578
|
-
function setupMetaMonitor(doc, nodeId) {
|
|
579
|
-
if (!warnOnExternal) return () => {
|
|
580
|
-
};
|
|
581
|
-
const metaMap = doc.getMap("meta");
|
|
582
|
-
const observer = (event, transaction) => {
|
|
583
|
-
const origin = transaction.origin;
|
|
584
|
-
if (origin === METABRIDGE_ORIGIN || origin === METABRIDGE_SEED_ORIGIN) {
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
if (origin === null || origin === "local") {
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
const changedKeys = Array.from(event.changes.keys.keys());
|
|
591
|
-
console.warn(
|
|
592
|
-
`[MetaBridge] Meta map change from non-MetaBridge source (BLOCKED from NodeStore):`,
|
|
593
|
-
{
|
|
594
|
-
nodeId,
|
|
595
|
-
keys: changedKeys,
|
|
596
|
-
origin,
|
|
597
|
-
hint: "Property changes should go through mutate(), not Y.Doc meta map"
|
|
598
|
-
}
|
|
599
|
-
);
|
|
600
|
-
};
|
|
601
|
-
metaMap.observe(observer);
|
|
602
|
-
return () => metaMap.unobserve(observer);
|
|
603
|
-
}
|
|
604
|
-
return {
|
|
605
|
-
observe(nodeId, doc) {
|
|
606
|
-
const unsubscribeStore = store.subscribe((event) => {
|
|
607
|
-
if (event.change.payload.nodeId !== nodeId) return;
|
|
608
|
-
const properties = event.change.payload.properties;
|
|
609
|
-
if (!properties || Object.keys(properties).length === 0) return;
|
|
610
|
-
writePropertiesToMeta(doc, properties, METABRIDGE_ORIGIN);
|
|
611
|
-
});
|
|
612
|
-
const unsubscribeMonitor = setupMetaMonitor(doc, nodeId);
|
|
613
|
-
return () => {
|
|
614
|
-
unsubscribeStore();
|
|
615
|
-
unsubscribeMonitor();
|
|
616
|
-
};
|
|
617
|
-
},
|
|
618
|
-
async seed(nodeId, doc) {
|
|
619
|
-
const node = await store.get(nodeId);
|
|
620
|
-
if (!node) return;
|
|
621
|
-
const properties = node.properties;
|
|
622
|
-
if (!properties || Object.keys(properties).length === 0) return;
|
|
623
|
-
writePropertiesToMeta(doc, properties, METABRIDGE_SEED_ORIGIN);
|
|
624
|
-
},
|
|
625
|
-
// Backward compatibility alias
|
|
626
|
-
async applyNow(nodeId, doc) {
|
|
627
|
-
return this.seed(nodeId, doc);
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// src/sync/node-pool.ts
|
|
633
|
-
import * as Y from "yjs";
|
|
634
|
-
function createNodePool(config) {
|
|
635
|
-
const entries = /* @__PURE__ */ new Map();
|
|
636
|
-
const persistTimers = /* @__PURE__ */ new Map();
|
|
637
|
-
const maxWarm = config.maxWarm ?? 50;
|
|
638
|
-
const persistDelay = config.persistDelay ?? 2e3;
|
|
639
|
-
async function loadDoc(nodeId) {
|
|
640
|
-
const doc = new Y.Doc({ guid: nodeId, gc: false });
|
|
641
|
-
const content = await config.storage.getDocumentContent(nodeId);
|
|
642
|
-
if (content && content.length > 0) {
|
|
643
|
-
Y.applyUpdate(doc, content);
|
|
644
|
-
}
|
|
645
|
-
return doc;
|
|
646
|
-
}
|
|
647
|
-
function schedulePersist(nodeId) {
|
|
648
|
-
const existing = persistTimers.get(nodeId);
|
|
649
|
-
if (existing) clearTimeout(existing);
|
|
650
|
-
persistTimers.set(
|
|
651
|
-
nodeId,
|
|
652
|
-
setTimeout(async () => {
|
|
653
|
-
persistTimers.delete(nodeId);
|
|
654
|
-
const entry = entries.get(nodeId);
|
|
655
|
-
if (entry && entry.dirty) {
|
|
656
|
-
const content = Y.encodeStateAsUpdate(entry.doc);
|
|
657
|
-
await config.storage.setDocumentContent(nodeId, content);
|
|
658
|
-
entry.dirty = false;
|
|
659
|
-
}
|
|
660
|
-
}, persistDelay)
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
async function evictIfNeeded() {
|
|
664
|
-
let warmCount = 0;
|
|
665
|
-
const warmEntries = [];
|
|
666
|
-
for (const [id, entry] of entries) {
|
|
667
|
-
if (entry.state === "warm") {
|
|
668
|
-
warmCount++;
|
|
669
|
-
warmEntries.push([id, entry]);
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
if (warmCount <= maxWarm) return;
|
|
673
|
-
warmEntries.sort((a, b) => a[1].lastAccess - b[1].lastAccess);
|
|
674
|
-
const toEvict = warmEntries.slice(0, warmCount - maxWarm);
|
|
675
|
-
await Promise.all(
|
|
676
|
-
toEvict.map(async ([id, entry]) => {
|
|
677
|
-
try {
|
|
678
|
-
const content = Y.encodeStateAsUpdate(entry.doc);
|
|
679
|
-
await config.storage.setDocumentContent(id, content);
|
|
680
|
-
} catch (err) {
|
|
681
|
-
console.error(`[NodePool] Failed to persist document ${id} during eviction:`, err);
|
|
682
|
-
}
|
|
683
|
-
if (entry.unobserveMeta) {
|
|
684
|
-
entry.unobserveMeta();
|
|
685
|
-
}
|
|
686
|
-
config.onDocEvict?.(id, entry.doc);
|
|
687
|
-
entry.doc.destroy();
|
|
688
|
-
entries.delete(id);
|
|
689
|
-
})
|
|
690
|
-
);
|
|
691
|
-
}
|
|
692
|
-
return {
|
|
693
|
-
async acquire(nodeId) {
|
|
694
|
-
let entry = entries.get(nodeId);
|
|
695
|
-
if (entry) {
|
|
696
|
-
entry.refCount++;
|
|
697
|
-
entry.state = "active";
|
|
698
|
-
entry.lastAccess = Date.now();
|
|
699
|
-
return entry.doc;
|
|
700
|
-
}
|
|
701
|
-
const doc = await loadDoc(nodeId);
|
|
702
|
-
doc.on("update", () => {
|
|
703
|
-
const e = entries.get(nodeId);
|
|
704
|
-
if (e) {
|
|
705
|
-
e.dirty = true;
|
|
706
|
-
schedulePersist(nodeId);
|
|
707
|
-
config.onDocUpdate?.(nodeId, doc);
|
|
708
|
-
}
|
|
709
|
-
});
|
|
710
|
-
const unobserveMeta = config.metaBridge.observe(nodeId, doc);
|
|
711
|
-
entry = {
|
|
712
|
-
doc,
|
|
713
|
-
state: "active",
|
|
714
|
-
refCount: 1,
|
|
715
|
-
lastAccess: Date.now(),
|
|
716
|
-
dirty: false,
|
|
717
|
-
unobserveMeta
|
|
718
|
-
};
|
|
719
|
-
entries.set(nodeId, entry);
|
|
720
|
-
return doc;
|
|
721
|
-
},
|
|
722
|
-
release(nodeId) {
|
|
723
|
-
const entry = entries.get(nodeId);
|
|
724
|
-
if (!entry) return;
|
|
725
|
-
entry.refCount = Math.max(0, entry.refCount - 1);
|
|
726
|
-
if (entry.refCount === 0) {
|
|
727
|
-
entry.state = "warm";
|
|
728
|
-
entry.lastAccess = Date.now();
|
|
729
|
-
void evictIfNeeded();
|
|
730
|
-
}
|
|
731
|
-
},
|
|
732
|
-
has(nodeId) {
|
|
733
|
-
return entries.has(nodeId);
|
|
734
|
-
},
|
|
735
|
-
getState(nodeId) {
|
|
736
|
-
return entries.get(nodeId)?.state ?? null;
|
|
737
|
-
},
|
|
738
|
-
get size() {
|
|
739
|
-
return entries.size;
|
|
740
|
-
},
|
|
741
|
-
async flushAll() {
|
|
742
|
-
for (const timer of persistTimers.values()) {
|
|
743
|
-
clearTimeout(timer);
|
|
744
|
-
}
|
|
745
|
-
persistTimers.clear();
|
|
746
|
-
const promises = [];
|
|
747
|
-
for (const [id, entry] of entries) {
|
|
748
|
-
if (entry.dirty) {
|
|
749
|
-
const content = Y.encodeStateAsUpdate(entry.doc);
|
|
750
|
-
promises.push(
|
|
751
|
-
config.storage.setDocumentContent(id, content).then(() => {
|
|
752
|
-
entry.dirty = false;
|
|
753
|
-
})
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
await Promise.all(promises);
|
|
758
|
-
},
|
|
759
|
-
async destroy() {
|
|
760
|
-
await this.flushAll();
|
|
761
|
-
for (const [, entry] of entries) {
|
|
762
|
-
if (entry.unobserveMeta) entry.unobserveMeta();
|
|
763
|
-
entry.doc.destroy();
|
|
764
|
-
}
|
|
765
|
-
entries.clear();
|
|
766
|
-
}
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
// src/sync/node-store-sync-provider.ts
|
|
771
|
-
import { base64ToBytes, bytesToBase64 } from "@xnetjs/crypto";
|
|
772
|
-
var KNOWN_CHANGE_TYPES = /* @__PURE__ */ new Set(["node-change"]);
|
|
773
|
-
var NodeStoreSyncProvider = class {
|
|
774
|
-
constructor(store, room) {
|
|
775
|
-
this.store = store;
|
|
776
|
-
this.room = room;
|
|
777
|
-
}
|
|
778
|
-
lastSyncedLamport = 0;
|
|
779
|
-
connection = null;
|
|
780
|
-
roomCleanup = null;
|
|
781
|
-
statusCleanup = null;
|
|
782
|
-
messageCleanup = null;
|
|
783
|
-
storeCleanup = null;
|
|
784
|
-
unknownChangeTypeListeners = /* @__PURE__ */ new Set();
|
|
785
|
-
/**
|
|
786
|
-
* Subscribe to unknown change type events.
|
|
787
|
-
* These are changes received from peers with types this version doesn't know how to process.
|
|
788
|
-
* The changes are still stored in the change log for forward compatibility.
|
|
789
|
-
*
|
|
790
|
-
* @param listener - Callback invoked when an unknown change type is received
|
|
791
|
-
* @returns Unsubscribe function
|
|
792
|
-
*/
|
|
793
|
-
onUnknownChangeType(listener) {
|
|
794
|
-
this.unknownChangeTypeListeners.add(listener);
|
|
795
|
-
return () => {
|
|
796
|
-
this.unknownChangeTypeListeners.delete(listener);
|
|
797
|
-
};
|
|
798
|
-
}
|
|
799
|
-
emitUnknownChangeType(change, peerId) {
|
|
800
|
-
for (const listener of this.unknownChangeTypeListeners) {
|
|
801
|
-
try {
|
|
802
|
-
listener(change, peerId);
|
|
803
|
-
} catch (err) {
|
|
804
|
-
console.error("Error in unknown change type listener:", err);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
attach(connection) {
|
|
809
|
-
this.connection = connection;
|
|
810
|
-
this.roomCleanup = connection.joinRoom(this.room, (data) => {
|
|
811
|
-
this.handleRoomMessage(data);
|
|
812
|
-
});
|
|
813
|
-
this.messageCleanup = connection.onMessage((message) => {
|
|
814
|
-
this.handleDirectMessage(message);
|
|
815
|
-
});
|
|
816
|
-
this.statusCleanup = connection.onStatus((status) => {
|
|
817
|
-
if (status === "connected") {
|
|
818
|
-
void this.syncLocalChanges();
|
|
819
|
-
this.requestSync();
|
|
820
|
-
}
|
|
821
|
-
});
|
|
822
|
-
this.storeCleanup = this.store.subscribe((event) => {
|
|
823
|
-
if (event.isRemote) return;
|
|
824
|
-
this.broadcastChange(event.change);
|
|
825
|
-
});
|
|
826
|
-
if (connection.status === "connected") {
|
|
827
|
-
void this.syncLocalChanges();
|
|
828
|
-
this.requestSync();
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
detach() {
|
|
832
|
-
this.roomCleanup?.();
|
|
833
|
-
this.statusCleanup?.();
|
|
834
|
-
this.messageCleanup?.();
|
|
835
|
-
this.storeCleanup?.();
|
|
836
|
-
this.roomCleanup = null;
|
|
837
|
-
this.statusCleanup = null;
|
|
838
|
-
this.messageCleanup = null;
|
|
839
|
-
this.storeCleanup = null;
|
|
840
|
-
this.connection = null;
|
|
841
|
-
}
|
|
842
|
-
handleRoomMessage(data) {
|
|
843
|
-
if (data.type === "node-change") {
|
|
844
|
-
const change = data.change;
|
|
845
|
-
void this.handleRemoteChange(change);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
handleDirectMessage(message) {
|
|
849
|
-
if (message.type !== "node-sync-response") return;
|
|
850
|
-
const response = message;
|
|
851
|
-
if (response.room !== this.room) return;
|
|
852
|
-
void this.handleSyncResponse(response);
|
|
853
|
-
}
|
|
854
|
-
requestSync() {
|
|
855
|
-
if (!this.connection) return;
|
|
856
|
-
this.connection.sendRaw({
|
|
857
|
-
type: "node-sync-request",
|
|
858
|
-
room: this.room,
|
|
859
|
-
sinceLamport: this.lastSyncedLamport
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
async syncLocalChanges() {
|
|
863
|
-
if (!this.connection || this.connection.status !== "connected") return;
|
|
864
|
-
const changes = await this.store.getChangesSince(this.lastSyncedLamport);
|
|
865
|
-
if (changes.length === 0) return;
|
|
866
|
-
changes.sort((a, b) => a.lamport.time - b.lamport.time);
|
|
867
|
-
for (const change of changes) {
|
|
868
|
-
this.broadcastChange(change);
|
|
869
|
-
this.lastSyncedLamport = Math.max(this.lastSyncedLamport, change.lamport.time);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
broadcastChange(change) {
|
|
873
|
-
if (!this.connection || this.connection.status !== "connected") return;
|
|
874
|
-
const serialized = this.serializeChange(change);
|
|
875
|
-
this.connection.publish(this.room, {
|
|
876
|
-
type: "node-change",
|
|
877
|
-
room: this.room,
|
|
878
|
-
change: serialized
|
|
879
|
-
});
|
|
880
|
-
this.lastSyncedLamport = Math.max(this.lastSyncedLamport, change.lamport.time);
|
|
881
|
-
}
|
|
882
|
-
async handleRemoteChange(serialized, peerId = "unknown") {
|
|
883
|
-
const change = this.deserializeChange(serialized);
|
|
884
|
-
if (serialized.lamportTime > this.lastSyncedLamport) {
|
|
885
|
-
this.lastSyncedLamport = serialized.lamportTime;
|
|
886
|
-
}
|
|
887
|
-
if (!KNOWN_CHANGE_TYPES.has(change.type)) {
|
|
888
|
-
console.warn(
|
|
889
|
-
`Received unknown change type "${change.type}" from peer ${peerId}. Change stored but not processed (forward compatibility).`
|
|
890
|
-
);
|
|
891
|
-
this.emitUnknownChangeType(change, peerId);
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
await this.store.applyRemoteChange(change);
|
|
895
|
-
}
|
|
896
|
-
async handleSyncResponse(response, peerId = "hub") {
|
|
897
|
-
if (response.changes.length > 0) {
|
|
898
|
-
const allChanges = response.changes.map((s) => this.deserializeChange(s));
|
|
899
|
-
const knownChanges = [];
|
|
900
|
-
for (const change of allChanges) {
|
|
901
|
-
if (KNOWN_CHANGE_TYPES.has(change.type)) {
|
|
902
|
-
knownChanges.push(change);
|
|
903
|
-
} else {
|
|
904
|
-
console.warn(
|
|
905
|
-
`Received unknown change type "${change.type}" from ${peerId}. Change stored but not processed (forward compatibility).`
|
|
906
|
-
);
|
|
907
|
-
this.emitUnknownChangeType(change, peerId);
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
if (knownChanges.length > 0) {
|
|
911
|
-
await this.store.applyRemoteChanges(knownChanges);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
this.lastSyncedLamport = Math.max(this.lastSyncedLamport, response.highWaterMark);
|
|
915
|
-
}
|
|
916
|
-
serializeChange(change) {
|
|
917
|
-
return {
|
|
918
|
-
id: change.id,
|
|
919
|
-
type: change.type,
|
|
920
|
-
hash: change.hash,
|
|
921
|
-
room: this.room,
|
|
922
|
-
nodeId: change.payload.nodeId,
|
|
923
|
-
schemaId: change.payload.schemaId,
|
|
924
|
-
lamportTime: change.lamport.time,
|
|
925
|
-
lamportAuthor: change.lamport.author,
|
|
926
|
-
authorDid: change.authorDID,
|
|
927
|
-
wallTime: change.wallTime,
|
|
928
|
-
parentHash: change.parentHash,
|
|
929
|
-
payload: change.payload,
|
|
930
|
-
signatureB64: bytesToBase64(change.signature),
|
|
931
|
-
batchId: change.batchId,
|
|
932
|
-
batchIndex: change.batchIndex,
|
|
933
|
-
batchSize: change.batchSize
|
|
934
|
-
};
|
|
935
|
-
}
|
|
936
|
-
deserializeChange(serialized) {
|
|
937
|
-
return {
|
|
938
|
-
id: serialized.id,
|
|
939
|
-
type: serialized.type,
|
|
940
|
-
hash: serialized.hash,
|
|
941
|
-
parentHash: serialized.parentHash,
|
|
942
|
-
authorDID: serialized.authorDid,
|
|
943
|
-
signature: base64ToBytes(serialized.signatureB64),
|
|
944
|
-
wallTime: serialized.wallTime,
|
|
945
|
-
lamport: { time: serialized.lamportTime, author: serialized.lamportAuthor },
|
|
946
|
-
payload: serialized.payload,
|
|
947
|
-
batchId: serialized.batchId,
|
|
948
|
-
batchIndex: serialized.batchIndex,
|
|
949
|
-
batchSize: serialized.batchSize
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
};
|
|
953
|
-
|
|
954
|
-
// src/sync/offline-queue.ts
|
|
955
|
-
function toBase64(data) {
|
|
956
|
-
let binary = "";
|
|
957
|
-
for (let i = 0; i < data.length; i++) {
|
|
958
|
-
binary += String.fromCharCode(data[i]);
|
|
959
|
-
}
|
|
960
|
-
return btoa(binary);
|
|
961
|
-
}
|
|
962
|
-
var SAVE_DEBOUNCE_MS = 100;
|
|
963
|
-
function createOfflineQueue(config) {
|
|
964
|
-
const storageKey = config.storageKey ?? "_xnet_offline_queue";
|
|
965
|
-
const maxSize = config.maxSize ?? 1e3;
|
|
966
|
-
let entries = [];
|
|
967
|
-
let storageNodeReady = false;
|
|
968
|
-
const encoder = new TextEncoder();
|
|
969
|
-
const decoder = new TextDecoder();
|
|
970
|
-
let saveTimer = null;
|
|
971
|
-
let saveResolvers = [];
|
|
972
|
-
const ensureStorageNode = async () => {
|
|
973
|
-
if (storageNodeReady) {
|
|
974
|
-
return;
|
|
975
|
-
}
|
|
976
|
-
const existing = await config.storage.getNode(storageKey);
|
|
977
|
-
if (existing) {
|
|
978
|
-
storageNodeReady = true;
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
const now = Date.now();
|
|
982
|
-
const systemDid = "did:key:offline-queue";
|
|
983
|
-
const node = {
|
|
984
|
-
id: storageKey,
|
|
985
|
-
schemaId: "xnet://xnet.system/OfflineQueueState",
|
|
986
|
-
properties: {},
|
|
987
|
-
timestamps: {},
|
|
988
|
-
deleted: true,
|
|
989
|
-
deletedAt: {
|
|
990
|
-
lamport: { time: 0, author: systemDid },
|
|
991
|
-
wallTime: now
|
|
992
|
-
},
|
|
993
|
-
createdAt: now,
|
|
994
|
-
createdBy: systemDid,
|
|
995
|
-
updatedAt: now,
|
|
996
|
-
updatedBy: systemDid
|
|
997
|
-
};
|
|
998
|
-
await config.storage.setNode(node);
|
|
999
|
-
storageNodeReady = true;
|
|
1000
|
-
};
|
|
1001
|
-
const debouncedSave = () => {
|
|
1002
|
-
return new Promise((resolve) => {
|
|
1003
|
-
saveResolvers.push(resolve);
|
|
1004
|
-
if (saveTimer) {
|
|
1005
|
-
clearTimeout(saveTimer);
|
|
1006
|
-
}
|
|
1007
|
-
saveTimer = setTimeout(async () => {
|
|
1008
|
-
saveTimer = null;
|
|
1009
|
-
const resolvers = saveResolvers;
|
|
1010
|
-
saveResolvers = [];
|
|
1011
|
-
try {
|
|
1012
|
-
await ensureStorageNode();
|
|
1013
|
-
const json = JSON.stringify(entries);
|
|
1014
|
-
const bytes = encoder.encode(json);
|
|
1015
|
-
await config.storage.setDocumentContent(storageKey, bytes);
|
|
1016
|
-
} catch (err) {
|
|
1017
|
-
console.warn("[OfflineQueue] Failed to persist:", err);
|
|
1018
|
-
}
|
|
1019
|
-
resolvers.forEach((r) => r());
|
|
1020
|
-
}, SAVE_DEBOUNCE_MS);
|
|
1021
|
-
});
|
|
1022
|
-
};
|
|
1023
|
-
return {
|
|
1024
|
-
async enqueue(nodeId, update) {
|
|
1025
|
-
entries.push({
|
|
1026
|
-
nodeId,
|
|
1027
|
-
update: toBase64(update),
|
|
1028
|
-
queuedAt: Date.now()
|
|
1029
|
-
});
|
|
1030
|
-
if (entries.length > maxSize) {
|
|
1031
|
-
entries = entries.slice(entries.length - maxSize);
|
|
1032
|
-
}
|
|
1033
|
-
await debouncedSave();
|
|
1034
|
-
},
|
|
1035
|
-
async drain(handler) {
|
|
1036
|
-
let drained = 0;
|
|
1037
|
-
while (entries.length > 0) {
|
|
1038
|
-
const entry = entries[0];
|
|
1039
|
-
try {
|
|
1040
|
-
await handler(entry);
|
|
1041
|
-
entries.shift();
|
|
1042
|
-
drained++;
|
|
1043
|
-
} catch {
|
|
1044
|
-
break;
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
if (drained > 0) {
|
|
1048
|
-
await this.save();
|
|
1049
|
-
}
|
|
1050
|
-
return drained;
|
|
1051
|
-
},
|
|
1052
|
-
get size() {
|
|
1053
|
-
return entries.length;
|
|
1054
|
-
},
|
|
1055
|
-
async load() {
|
|
1056
|
-
try {
|
|
1057
|
-
const content = await config.storage.getDocumentContent(storageKey);
|
|
1058
|
-
if (content && content.length > 0) {
|
|
1059
|
-
const json = decoder.decode(content);
|
|
1060
|
-
const parsed = JSON.parse(json);
|
|
1061
|
-
if (Array.isArray(parsed)) {
|
|
1062
|
-
entries = parsed;
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
} catch {
|
|
1066
|
-
entries = [];
|
|
1067
|
-
}
|
|
1068
|
-
},
|
|
1069
|
-
async save() {
|
|
1070
|
-
if (saveTimer) {
|
|
1071
|
-
clearTimeout(saveTimer);
|
|
1072
|
-
saveTimer = null;
|
|
1073
|
-
const resolvers = saveResolvers;
|
|
1074
|
-
saveResolvers = [];
|
|
1075
|
-
resolvers.forEach((r) => r());
|
|
1076
|
-
}
|
|
1077
|
-
try {
|
|
1078
|
-
await ensureStorageNode();
|
|
1079
|
-
const json = JSON.stringify(entries);
|
|
1080
|
-
const bytes = encoder.encode(json);
|
|
1081
|
-
await config.storage.setDocumentContent(storageKey, bytes);
|
|
1082
|
-
} catch (err) {
|
|
1083
|
-
console.warn("[OfflineQueue] Failed to persist:", err);
|
|
1084
|
-
}
|
|
1085
|
-
},
|
|
1086
|
-
async clear() {
|
|
1087
|
-
entries = [];
|
|
1088
|
-
await this.save();
|
|
1089
|
-
}
|
|
1090
|
-
};
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
// src/sync/registry.ts
|
|
1094
|
-
function createRegistry(config) {
|
|
1095
|
-
const trackTTL = config.trackTTL ?? 7 * 24 * 60 * 60 * 1e3;
|
|
1096
|
-
const storageKey = config.storageKey ?? "_xnet_tracked_nodes";
|
|
1097
|
-
const tracked = /* @__PURE__ */ new Map();
|
|
1098
|
-
return {
|
|
1099
|
-
track(nodeId, schemaId) {
|
|
1100
|
-
const existing = tracked.get(nodeId);
|
|
1101
|
-
if (existing) {
|
|
1102
|
-
existing.lastOpened = Date.now();
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
tracked.set(nodeId, {
|
|
1106
|
-
nodeId,
|
|
1107
|
-
schemaId,
|
|
1108
|
-
lastOpened: Date.now(),
|
|
1109
|
-
lastSynced: 0,
|
|
1110
|
-
pinned: false
|
|
1111
|
-
});
|
|
1112
|
-
},
|
|
1113
|
-
untrack(nodeId) {
|
|
1114
|
-
tracked.delete(nodeId);
|
|
1115
|
-
},
|
|
1116
|
-
pin(nodeId) {
|
|
1117
|
-
const entry = tracked.get(nodeId);
|
|
1118
|
-
if (entry) entry.pinned = true;
|
|
1119
|
-
},
|
|
1120
|
-
unpin(nodeId) {
|
|
1121
|
-
const entry = tracked.get(nodeId);
|
|
1122
|
-
if (entry) entry.pinned = false;
|
|
1123
|
-
},
|
|
1124
|
-
touch(nodeId) {
|
|
1125
|
-
const entry = tracked.get(nodeId);
|
|
1126
|
-
if (entry) entry.lastOpened = Date.now();
|
|
1127
|
-
},
|
|
1128
|
-
markSynced(nodeId) {
|
|
1129
|
-
const entry = tracked.get(nodeId);
|
|
1130
|
-
if (entry) entry.lastSynced = Date.now();
|
|
1131
|
-
},
|
|
1132
|
-
getTracked() {
|
|
1133
|
-
const now = Date.now();
|
|
1134
|
-
const result = [];
|
|
1135
|
-
for (const entry of tracked.values()) {
|
|
1136
|
-
if (entry.pinned || now - entry.lastOpened < trackTTL) {
|
|
1137
|
-
result.push(entry);
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
return result;
|
|
1141
|
-
},
|
|
1142
|
-
isTracked(nodeId) {
|
|
1143
|
-
const entry = tracked.get(nodeId);
|
|
1144
|
-
if (!entry) return false;
|
|
1145
|
-
return entry.pinned || Date.now() - entry.lastOpened < trackTTL;
|
|
1146
|
-
},
|
|
1147
|
-
async load() {
|
|
1148
|
-
try {
|
|
1149
|
-
const entries = await config.storage.get(storageKey);
|
|
1150
|
-
if (entries) {
|
|
1151
|
-
for (const entry of entries) {
|
|
1152
|
-
tracked.set(entry.nodeId, entry);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
} catch {
|
|
1156
|
-
}
|
|
1157
|
-
},
|
|
1158
|
-
async save() {
|
|
1159
|
-
const entries = Array.from(tracked.values());
|
|
1160
|
-
try {
|
|
1161
|
-
await config.storage.set(storageKey, entries);
|
|
1162
|
-
} catch (err) {
|
|
1163
|
-
console.warn("[Registry] Failed to persist:", err);
|
|
1164
|
-
}
|
|
1165
|
-
},
|
|
1166
|
-
prune() {
|
|
1167
|
-
const now = Date.now();
|
|
1168
|
-
let pruned = 0;
|
|
1169
|
-
for (const [id, entry] of tracked) {
|
|
1170
|
-
if (!entry.pinned && now - entry.lastOpened >= trackTTL) {
|
|
1171
|
-
tracked.delete(id);
|
|
1172
|
-
pruned++;
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
return pruned;
|
|
1176
|
-
}
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// src/sync/sync-manager.ts
|
|
1181
|
-
import {
|
|
1182
|
-
Awareness,
|
|
1183
|
-
applyAwarenessUpdate,
|
|
1184
|
-
encodeAwarenessUpdate,
|
|
1185
|
-
removeAwarenessStates
|
|
1186
|
-
} from "y-protocols/awareness";
|
|
1187
|
-
import * as Y2 from "yjs";
|
|
1188
|
-
if (typeof localStorage !== "undefined" && localStorage.getItem("xnet:sync:debug") === "true") {
|
|
1189
|
-
console.log("[SyncManager] Module loaded from source!");
|
|
1190
|
-
}
|
|
1191
|
-
function log2(...args) {
|
|
1192
|
-
if (typeof localStorage !== "undefined" && localStorage.getItem("xnet:sync:debug") === "true") {
|
|
1193
|
-
console.log("[SyncManager]", ...args);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
function createRegistryStorageAdapter(storage) {
|
|
1197
|
-
const encoder = new TextEncoder();
|
|
1198
|
-
const decoder = new TextDecoder();
|
|
1199
|
-
return {
|
|
1200
|
-
async get(key) {
|
|
1201
|
-
try {
|
|
1202
|
-
const content = await storage.getDocumentContent(key);
|
|
1203
|
-
if (!content || content.length === 0) return null;
|
|
1204
|
-
const json = decoder.decode(content);
|
|
1205
|
-
return JSON.parse(json);
|
|
1206
|
-
} catch {
|
|
1207
|
-
return null;
|
|
1208
|
-
}
|
|
1209
|
-
},
|
|
1210
|
-
async set(key, entries) {
|
|
1211
|
-
const json = JSON.stringify(entries);
|
|
1212
|
-
const bytes = encoder.encode(json);
|
|
1213
|
-
await storage.setDocumentContent(key, bytes);
|
|
1214
|
-
}
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
function extractBlobCids(doc) {
|
|
1218
|
-
const cids = [];
|
|
1219
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1220
|
-
for (const name of ["default", "prosemirror", ""]) {
|
|
1221
|
-
let fragment;
|
|
1222
|
-
try {
|
|
1223
|
-
fragment = doc.getXmlFragment(name);
|
|
1224
|
-
} catch {
|
|
1225
|
-
continue;
|
|
1226
|
-
}
|
|
1227
|
-
if (!fragment || fragment.length === 0) continue;
|
|
1228
|
-
walkXmlFragment(fragment, (node) => {
|
|
1229
|
-
if (node instanceof Y2.XmlElement) {
|
|
1230
|
-
const cid = node.getAttribute("cid");
|
|
1231
|
-
if (typeof cid === "string" && cid.length > 0 && !seen.has(cid)) {
|
|
1232
|
-
seen.add(cid);
|
|
1233
|
-
cids.push(cid);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
const meta = doc.getMap("meta");
|
|
1239
|
-
if (meta) {
|
|
1240
|
-
walkYMap(meta, (value) => {
|
|
1241
|
-
if (typeof value === "object" && value !== null && "cid" in value) {
|
|
1242
|
-
const cid = value.cid;
|
|
1243
|
-
if (typeof cid === "string" && cid.length > 0 && !seen.has(cid)) {
|
|
1244
|
-
seen.add(cid);
|
|
1245
|
-
cids.push(cid);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
});
|
|
1249
|
-
}
|
|
1250
|
-
return cids;
|
|
1251
|
-
}
|
|
1252
|
-
function walkXmlFragment(node, visitor) {
|
|
1253
|
-
for (let i = 0; i < node.length; i++) {
|
|
1254
|
-
const child = node.get(i);
|
|
1255
|
-
if (child instanceof Y2.XmlElement) {
|
|
1256
|
-
visitor(child);
|
|
1257
|
-
walkXmlFragment(child, visitor);
|
|
1258
|
-
} else if (child instanceof Y2.XmlText) {
|
|
1259
|
-
visitor(child);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
function walkYMap(map, visitor) {
|
|
1264
|
-
map.forEach((value) => {
|
|
1265
|
-
if (value instanceof Y2.Map) {
|
|
1266
|
-
walkYMap(value, visitor);
|
|
1267
|
-
} else if (Array.isArray(value)) {
|
|
1268
|
-
for (const item of value) {
|
|
1269
|
-
visitor(item);
|
|
1270
|
-
}
|
|
1271
|
-
} else {
|
|
1272
|
-
visitor(value);
|
|
1273
|
-
}
|
|
1274
|
-
});
|
|
1275
|
-
}
|
|
1276
|
-
function createSyncManager(config) {
|
|
1277
|
-
const metaBridge = createMetaBridge(config.nodeStore);
|
|
1278
|
-
const pool = createNodePool({
|
|
1279
|
-
storage: config.storage,
|
|
1280
|
-
metaBridge,
|
|
1281
|
-
maxWarm: config.poolSize ?? 50,
|
|
1282
|
-
onDocUpdate: config.onDocUpdate,
|
|
1283
|
-
onDocEvict: (nodeId, doc) => {
|
|
1284
|
-
broadcastDocs.delete(nodeId);
|
|
1285
|
-
config.onDocEvict?.(nodeId, doc);
|
|
1286
|
-
}
|
|
1287
|
-
});
|
|
1288
|
-
const registry = createRegistry({
|
|
1289
|
-
storage: createRegistryStorageAdapter(config.storage),
|
|
1290
|
-
trackTTL: config.trackTTL
|
|
1291
|
-
});
|
|
1292
|
-
const connection = createConnectionManager({
|
|
1293
|
-
url: config.signalingUrl,
|
|
1294
|
-
ucanToken: config.ucanToken,
|
|
1295
|
-
getUCANToken: config.getUCANToken
|
|
1296
|
-
});
|
|
1297
|
-
const offlineQueue = createOfflineQueue({
|
|
1298
|
-
storage: config.storage
|
|
1299
|
-
});
|
|
1300
|
-
const nodeSyncProvider = config.nodeSyncRoom ? new NodeStoreSyncProvider(config.nodeStore, config.nodeSyncRoom) : null;
|
|
1301
|
-
let blobSync = null;
|
|
1302
|
-
if (config.blobStore) {
|
|
1303
|
-
const underlyingStore = config.blobStore;
|
|
1304
|
-
const announcingStore = {
|
|
1305
|
-
get: (cid) => underlyingStore.get(cid),
|
|
1306
|
-
has: (cid) => underlyingStore.has(cid),
|
|
1307
|
-
async put(data) {
|
|
1308
|
-
const cid = await underlyingStore.put(data);
|
|
1309
|
-
if (blobSync) {
|
|
1310
|
-
blobSync.announceHave([cid]);
|
|
1311
|
-
}
|
|
1312
|
-
return cid;
|
|
1313
|
-
}
|
|
1314
|
-
};
|
|
1315
|
-
blobSync = createBlobSyncProvider({
|
|
1316
|
-
blobStore: announcingStore,
|
|
1317
|
-
connection
|
|
1318
|
-
});
|
|
1319
|
-
}
|
|
1320
|
-
const roomCleanups = /* @__PURE__ */ new Map();
|
|
1321
|
-
const awarenessMap = /* @__PURE__ */ new Map();
|
|
1322
|
-
const awarenessSnapshots = /* @__PURE__ */ new Map();
|
|
1323
|
-
const awarenessSnapshotListeners = /* @__PURE__ */ new Map();
|
|
1324
|
-
const broadcastDocs = /* @__PURE__ */ new Set();
|
|
1325
|
-
const peerId = Math.random().toString(36).slice(2, 10);
|
|
1326
|
-
let statusHandlerCleanup = null;
|
|
1327
|
-
function toBase642(data) {
|
|
1328
|
-
let binary = "";
|
|
1329
|
-
for (let i = 0; i < data.length; i++) {
|
|
1330
|
-
binary += String.fromCharCode(data[i]);
|
|
1331
|
-
}
|
|
1332
|
-
return btoa(binary);
|
|
1333
|
-
}
|
|
1334
|
-
function fromBase64(str) {
|
|
1335
|
-
const binary = atob(str);
|
|
1336
|
-
const bytes = new Uint8Array(binary.length);
|
|
1337
|
-
for (let i = 0; i < binary.length; i++) {
|
|
1338
|
-
bytes[i] = binary.charCodeAt(i);
|
|
1339
|
-
}
|
|
1340
|
-
return bytes;
|
|
1341
|
-
}
|
|
1342
|
-
async function joinNodeRoom(nodeId) {
|
|
1343
|
-
if (roomCleanups.has(nodeId)) {
|
|
1344
|
-
log2("Already joined room for node:", nodeId);
|
|
1345
|
-
return;
|
|
1346
|
-
}
|
|
1347
|
-
const room = `xnet-doc-${nodeId}`;
|
|
1348
|
-
log2("Joining room:", room);
|
|
1349
|
-
const { unsubscribe, ready } = connection.joinRoomAsync(room, (data) => {
|
|
1350
|
-
handleSyncMessage(nodeId, data);
|
|
1351
|
-
});
|
|
1352
|
-
roomCleanups.set(nodeId, unsubscribe);
|
|
1353
|
-
await ready;
|
|
1354
|
-
if (pool.has(nodeId)) {
|
|
1355
|
-
log2("Doc already in pool, sending initial sync-step1");
|
|
1356
|
-
const doc = await pool.acquire(nodeId);
|
|
1357
|
-
getOrCreateAwareness(nodeId, doc);
|
|
1358
|
-
const sv = Y2.encodeStateVector(doc);
|
|
1359
|
-
log2("Sending sync-step1 for node:", nodeId, "SV size:", sv.length);
|
|
1360
|
-
connection.publish(room, {
|
|
1361
|
-
type: "sync-step1",
|
|
1362
|
-
from: peerId,
|
|
1363
|
-
sv: toBase642(sv)
|
|
1364
|
-
});
|
|
1365
|
-
pool.release(nodeId);
|
|
1366
|
-
} else {
|
|
1367
|
-
log2("Doc not in pool yet for node:", nodeId);
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
function leaveNodeRoom(nodeId) {
|
|
1371
|
-
const cleanup = roomCleanups.get(nodeId);
|
|
1372
|
-
if (cleanup) {
|
|
1373
|
-
cleanup();
|
|
1374
|
-
roomCleanups.delete(nodeId);
|
|
1375
|
-
}
|
|
1376
|
-
const awareness = awarenessMap.get(nodeId);
|
|
1377
|
-
if (awareness) {
|
|
1378
|
-
removeAwarenessStates(awareness, [awareness.clientID], "local");
|
|
1379
|
-
}
|
|
1380
|
-
awarenessMap.delete(nodeId);
|
|
1381
|
-
awarenessSnapshots.delete(nodeId);
|
|
1382
|
-
awarenessSnapshotListeners.delete(nodeId);
|
|
1383
|
-
broadcastDocs.delete(nodeId);
|
|
1384
|
-
}
|
|
1385
|
-
function getOrCreateAwareness(nodeId, doc) {
|
|
1386
|
-
const existing = awarenessMap.get(nodeId);
|
|
1387
|
-
if (existing) return existing;
|
|
1388
|
-
const awareness = new Awareness(doc);
|
|
1389
|
-
awarenessMap.set(nodeId, awareness);
|
|
1390
|
-
awareness.on(
|
|
1391
|
-
"update",
|
|
1392
|
-
({ added, updated, removed }, origin) => {
|
|
1393
|
-
if (origin === "remote") return;
|
|
1394
|
-
const changed = [...added, ...updated, ...removed];
|
|
1395
|
-
if (changed.length === 0) return;
|
|
1396
|
-
if (connection.status !== "connected") return;
|
|
1397
|
-
const room = `xnet-doc-${nodeId}`;
|
|
1398
|
-
const update = encodeAwarenessUpdate(awareness, changed);
|
|
1399
|
-
connection.publish(room, {
|
|
1400
|
-
type: "awareness",
|
|
1401
|
-
from: peerId,
|
|
1402
|
-
update: toBase642(update)
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
);
|
|
1406
|
-
return awareness;
|
|
1407
|
-
}
|
|
1408
|
-
function emitAwarenessSnapshot(nodeId, users) {
|
|
1409
|
-
awarenessSnapshots.set(nodeId, users);
|
|
1410
|
-
const listeners = awarenessSnapshotListeners.get(nodeId);
|
|
1411
|
-
if (!listeners) return;
|
|
1412
|
-
for (const handler of listeners) {
|
|
1413
|
-
try {
|
|
1414
|
-
handler(users);
|
|
1415
|
-
} catch {
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
async function handleSyncMessage(nodeId, data) {
|
|
1420
|
-
if (data.from === peerId) {
|
|
1421
|
-
log2("Ignoring own message");
|
|
1422
|
-
return;
|
|
1423
|
-
}
|
|
1424
|
-
log2("Received message for node:", nodeId, "type:", data.type, "from:", data.from);
|
|
1425
|
-
const doc = await pool.acquire(nodeId);
|
|
1426
|
-
const room = `xnet-doc-${nodeId}`;
|
|
1427
|
-
try {
|
|
1428
|
-
switch (data.type) {
|
|
1429
|
-
case "sync-step1": {
|
|
1430
|
-
const remoteSV = fromBase64(data.sv);
|
|
1431
|
-
const diff = Y2.encodeStateAsUpdate(doc, remoteSV);
|
|
1432
|
-
log2(
|
|
1433
|
-
"Received sync-step1, remote SV size:",
|
|
1434
|
-
remoteSV.length,
|
|
1435
|
-
"sending diff size:",
|
|
1436
|
-
diff.length
|
|
1437
|
-
);
|
|
1438
|
-
connection.publish(room, {
|
|
1439
|
-
type: "sync-step2",
|
|
1440
|
-
from: peerId,
|
|
1441
|
-
to: data.from,
|
|
1442
|
-
update: toBase642(diff)
|
|
1443
|
-
});
|
|
1444
|
-
break;
|
|
1445
|
-
}
|
|
1446
|
-
case "sync-step2": {
|
|
1447
|
-
if (data.to && data.to !== peerId) {
|
|
1448
|
-
log2("Ignoring sync-step2 addressed to different peer:", data.to);
|
|
1449
|
-
break;
|
|
1450
|
-
}
|
|
1451
|
-
const update = fromBase64(data.update);
|
|
1452
|
-
log2("Received sync-step2, applying update size:", update.length);
|
|
1453
|
-
log2("Doc state before update - meta keys:", doc.getMap("meta").size);
|
|
1454
|
-
Y2.applyUpdate(doc, update, "remote");
|
|
1455
|
-
log2("Doc state after update - meta keys:", doc.getMap("meta").size);
|
|
1456
|
-
registry.markSynced(nodeId);
|
|
1457
|
-
log2("Marked node as synced:", nodeId);
|
|
1458
|
-
break;
|
|
1459
|
-
}
|
|
1460
|
-
case "sync-update": {
|
|
1461
|
-
const update = fromBase64(data.update);
|
|
1462
|
-
log2("Received sync-update, size:", update.length);
|
|
1463
|
-
Y2.applyUpdate(doc, update, "remote");
|
|
1464
|
-
break;
|
|
1465
|
-
}
|
|
1466
|
-
case "awareness": {
|
|
1467
|
-
const update = fromBase64(data.update);
|
|
1468
|
-
const awareness = getOrCreateAwareness(nodeId, doc);
|
|
1469
|
-
applyAwarenessUpdate(awareness, update, "remote");
|
|
1470
|
-
break;
|
|
1471
|
-
}
|
|
1472
|
-
case "awareness-snapshot": {
|
|
1473
|
-
const users = Array.isArray(data.users) ? data.users : [];
|
|
1474
|
-
emitAwarenessSnapshot(nodeId, users);
|
|
1475
|
-
break;
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
} finally {
|
|
1479
|
-
pool.release(nodeId);
|
|
1480
|
-
}
|
|
1481
|
-
}
|
|
1482
|
-
function setupDocBroadcast(nodeId, doc) {
|
|
1483
|
-
if (broadcastDocs.has(nodeId)) return;
|
|
1484
|
-
broadcastDocs.add(nodeId);
|
|
1485
|
-
const room = `xnet-doc-${nodeId}`;
|
|
1486
|
-
doc.on("update", (update, origin) => {
|
|
1487
|
-
if (origin === "remote") return;
|
|
1488
|
-
if (connection.status === "connected") {
|
|
1489
|
-
connection.publish(room, {
|
|
1490
|
-
type: "sync-update",
|
|
1491
|
-
from: peerId,
|
|
1492
|
-
update: toBase642(update)
|
|
1493
|
-
});
|
|
1494
|
-
} else {
|
|
1495
|
-
offlineQueue.enqueue(nodeId, update);
|
|
1496
|
-
}
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
async function drainOfflineQueue() {
|
|
1500
|
-
if (offlineQueue.size === 0) return;
|
|
1501
|
-
await offlineQueue.drain(async (entry) => {
|
|
1502
|
-
const room = `xnet-doc-${entry.nodeId}`;
|
|
1503
|
-
connection.publish(room, {
|
|
1504
|
-
type: "sync-update",
|
|
1505
|
-
from: peerId,
|
|
1506
|
-
update: entry.update
|
|
1507
|
-
// Already base64 encoded
|
|
1508
|
-
});
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
1511
|
-
function sendSyncStep1(nodeId, doc) {
|
|
1512
|
-
const room = `xnet-doc-${nodeId}`;
|
|
1513
|
-
const sv = Y2.encodeStateVector(doc);
|
|
1514
|
-
log2(
|
|
1515
|
-
"sendSyncStep1 for node:",
|
|
1516
|
-
nodeId,
|
|
1517
|
-
"SV size:",
|
|
1518
|
-
sv.length,
|
|
1519
|
-
"connection status:",
|
|
1520
|
-
connection.status
|
|
1521
|
-
);
|
|
1522
|
-
connection.publish(room, {
|
|
1523
|
-
type: "sync-step1",
|
|
1524
|
-
from: peerId,
|
|
1525
|
-
sv: toBase642(sv)
|
|
1526
|
-
});
|
|
1527
|
-
}
|
|
1528
|
-
return {
|
|
1529
|
-
async start() {
|
|
1530
|
-
log2("Starting SyncManager...");
|
|
1531
|
-
await registry.load();
|
|
1532
|
-
log2("Registry loaded, tracked nodes:", registry.getTracked().length);
|
|
1533
|
-
await offlineQueue.load();
|
|
1534
|
-
log2("Offline queue loaded, size:", offlineQueue.size);
|
|
1535
|
-
log2("Connecting to signaling server...");
|
|
1536
|
-
connection.connect();
|
|
1537
|
-
nodeSyncProvider?.attach(connection);
|
|
1538
|
-
blobSync?.start();
|
|
1539
|
-
statusHandlerCleanup = connection.onStatus((s) => {
|
|
1540
|
-
log2("Connection status changed in start():", s);
|
|
1541
|
-
if (s === "connected") {
|
|
1542
|
-
drainOfflineQueue();
|
|
1543
|
-
for (const nodeId of roomCleanups.keys()) {
|
|
1544
|
-
if (pool.has(nodeId)) {
|
|
1545
|
-
pool.acquire(nodeId).then((doc) => {
|
|
1546
|
-
sendSyncStep1(nodeId, doc);
|
|
1547
|
-
pool.release(nodeId);
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
});
|
|
1553
|
-
const tracked = registry.getTracked();
|
|
1554
|
-
log2("Joining rooms for", tracked.length, "tracked nodes");
|
|
1555
|
-
await Promise.all(tracked.map((entry) => joinNodeRoom(entry.nodeId)));
|
|
1556
|
-
log2("SyncManager started");
|
|
1557
|
-
},
|
|
1558
|
-
async stop() {
|
|
1559
|
-
blobSync?.stop();
|
|
1560
|
-
nodeSyncProvider?.detach();
|
|
1561
|
-
if (statusHandlerCleanup) {
|
|
1562
|
-
statusHandlerCleanup();
|
|
1563
|
-
statusHandlerCleanup = null;
|
|
1564
|
-
}
|
|
1565
|
-
for (const nodeId of Array.from(roomCleanups.keys())) {
|
|
1566
|
-
leaveNodeRoom(nodeId);
|
|
1567
|
-
}
|
|
1568
|
-
connection.disconnect();
|
|
1569
|
-
await pool.flushAll();
|
|
1570
|
-
registry.prune();
|
|
1571
|
-
await registry.save();
|
|
1572
|
-
await offlineQueue.save();
|
|
1573
|
-
await pool.destroy();
|
|
1574
|
-
},
|
|
1575
|
-
track(nodeId, schemaId) {
|
|
1576
|
-
registry.track(nodeId, schemaId);
|
|
1577
|
-
joinNodeRoom(nodeId).catch((err) => {
|
|
1578
|
-
log2("Error joining room for track:", err);
|
|
1579
|
-
});
|
|
1580
|
-
},
|
|
1581
|
-
untrack(nodeId) {
|
|
1582
|
-
registry.untrack(nodeId);
|
|
1583
|
-
leaveNodeRoom(nodeId);
|
|
1584
|
-
},
|
|
1585
|
-
async acquire(nodeId) {
|
|
1586
|
-
console.log("[SyncManager] acquire() called for:", nodeId);
|
|
1587
|
-
log2("Acquiring doc for node:", nodeId);
|
|
1588
|
-
registry.touch(nodeId);
|
|
1589
|
-
const doc = await pool.acquire(nodeId);
|
|
1590
|
-
log2("Doc acquired from pool, guid:", doc.guid, "meta keys:", doc.getMap("meta").size);
|
|
1591
|
-
setupDocBroadcast(nodeId, doc);
|
|
1592
|
-
getOrCreateAwareness(nodeId, doc);
|
|
1593
|
-
if (!roomCleanups.has(nodeId)) {
|
|
1594
|
-
await joinNodeRoom(nodeId);
|
|
1595
|
-
}
|
|
1596
|
-
if (connection.status === "connected") {
|
|
1597
|
-
log2("Connection is connected, sending sync-step1");
|
|
1598
|
-
sendSyncStep1(nodeId, doc);
|
|
1599
|
-
} else {
|
|
1600
|
-
log2("Connection not connected, status:", connection.status);
|
|
1601
|
-
}
|
|
1602
|
-
if (blobSync) {
|
|
1603
|
-
const cids = extractBlobCids(doc);
|
|
1604
|
-
if (cids.length > 0) {
|
|
1605
|
-
blobSync.requestBlobs(cids);
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
return doc;
|
|
1609
|
-
},
|
|
1610
|
-
release(nodeId) {
|
|
1611
|
-
pool.release(nodeId);
|
|
1612
|
-
},
|
|
1613
|
-
getAwareness(nodeId) {
|
|
1614
|
-
return awarenessMap.get(nodeId) ?? null;
|
|
1615
|
-
},
|
|
1616
|
-
onAwarenessSnapshot(nodeId, handler) {
|
|
1617
|
-
const listeners = awarenessSnapshotListeners.get(nodeId) ?? /* @__PURE__ */ new Set();
|
|
1618
|
-
listeners.add(handler);
|
|
1619
|
-
awarenessSnapshotListeners.set(nodeId, listeners);
|
|
1620
|
-
const existing = awarenessSnapshots.get(nodeId);
|
|
1621
|
-
if (existing) {
|
|
1622
|
-
handler(existing);
|
|
1623
|
-
}
|
|
1624
|
-
return () => {
|
|
1625
|
-
const current = awarenessSnapshotListeners.get(nodeId);
|
|
1626
|
-
if (!current) return;
|
|
1627
|
-
current.delete(handler);
|
|
1628
|
-
if (current.size === 0) {
|
|
1629
|
-
awarenessSnapshotListeners.delete(nodeId);
|
|
1630
|
-
}
|
|
1631
|
-
};
|
|
1632
|
-
},
|
|
1633
|
-
async requestBlobs(cids) {
|
|
1634
|
-
if (blobSync) {
|
|
1635
|
-
await blobSync.requestBlobs(cids);
|
|
1636
|
-
}
|
|
1637
|
-
},
|
|
1638
|
-
announceBlobs(cids) {
|
|
1639
|
-
if (blobSync) {
|
|
1640
|
-
blobSync.announceHave(cids);
|
|
1641
|
-
}
|
|
1642
|
-
},
|
|
1643
|
-
get status() {
|
|
1644
|
-
return connection.status;
|
|
1645
|
-
},
|
|
1646
|
-
get connection() {
|
|
1647
|
-
return connection;
|
|
1648
|
-
},
|
|
1649
|
-
get poolSize() {
|
|
1650
|
-
return pool.size;
|
|
1651
|
-
},
|
|
1652
|
-
get trackedCount() {
|
|
1653
|
-
return registry.getTracked().length;
|
|
1654
|
-
},
|
|
1655
|
-
get queueSize() {
|
|
1656
|
-
return offlineQueue.size;
|
|
1657
|
-
},
|
|
1658
|
-
get pendingBlobCount() {
|
|
1659
|
-
return blobSync?.pendingCount ?? 0;
|
|
1660
|
-
},
|
|
1661
|
-
on(event, handler) {
|
|
1662
|
-
if (event === "status") {
|
|
1663
|
-
return connection.onStatus(handler);
|
|
1664
|
-
}
|
|
1665
|
-
return () => {
|
|
1666
|
-
};
|
|
1667
|
-
}
|
|
1668
|
-
};
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
// src/context.ts
|
|
1672
|
-
import { MemoryNodeStorageAdapter, NodeStore } from "@xnetjs/data";
|
|
1673
|
-
import { createMainThreadBridge, MainThreadBridge } from "@xnetjs/data-bridge";
|
|
1674
|
-
import { createUCAN } from "@xnetjs/identity";
|
|
1675
|
-
import { PluginRegistry } from "@xnetjs/plugins";
|
|
1676
|
-
import React, {
|
|
1677
|
-
createContext as createContext4,
|
|
1678
|
-
useCallback,
|
|
1679
|
-
useContext as useContext4,
|
|
1680
|
-
useEffect as useEffect3,
|
|
1681
|
-
useMemo as useMemo2,
|
|
1682
|
-
useRef,
|
|
1683
|
-
useState as useState3
|
|
1684
|
-
} from "react";
|
|
1685
|
-
|
|
1686
|
-
// src/hub/auto-backup.ts
|
|
1687
|
-
import * as Y3 from "yjs";
|
|
1688
|
-
var AutoBackup = class {
|
|
1689
|
-
constructor(upload, options) {
|
|
1690
|
-
this.upload = upload;
|
|
1691
|
-
this.debounceMs = options?.debounceMs ?? 5e3;
|
|
1692
|
-
this.isEnabled = options?.isEnabled ?? (() => true);
|
|
1693
|
-
}
|
|
1694
|
-
timers = /* @__PURE__ */ new Map();
|
|
1695
|
-
debounceMs;
|
|
1696
|
-
isEnabled;
|
|
1697
|
-
handleDocUpdate(docId, doc) {
|
|
1698
|
-
this.scheduleBackup(docId, doc, this.debounceMs);
|
|
1699
|
-
}
|
|
1700
|
-
handleDocEvict(docId, doc) {
|
|
1701
|
-
this.scheduleBackup(docId, doc, 0);
|
|
1702
|
-
}
|
|
1703
|
-
scheduleBackup(docId, doc, delay) {
|
|
1704
|
-
const existing = this.timers.get(docId);
|
|
1705
|
-
if (existing) {
|
|
1706
|
-
clearTimeout(existing);
|
|
1707
|
-
}
|
|
1708
|
-
const run = async () => {
|
|
1709
|
-
this.timers.delete(docId);
|
|
1710
|
-
if (!this.isEnabled()) return;
|
|
1711
|
-
try {
|
|
1712
|
-
const state = Y3.encodeStateAsUpdate(doc);
|
|
1713
|
-
await this.upload(docId, state);
|
|
1714
|
-
} catch (err) {
|
|
1715
|
-
console.warn(`[auto-backup] Failed for ${docId}:`, err);
|
|
1716
|
-
}
|
|
1717
|
-
};
|
|
1718
|
-
if (delay <= 0) {
|
|
1719
|
-
void run();
|
|
1720
|
-
return;
|
|
1721
|
-
}
|
|
1722
|
-
this.timers.set(
|
|
1723
|
-
docId,
|
|
1724
|
-
setTimeout(() => void run(), delay)
|
|
1725
|
-
);
|
|
1726
|
-
}
|
|
1727
|
-
async flush() {
|
|
1728
|
-
for (const timer of this.timers.values()) {
|
|
1729
|
-
clearTimeout(timer);
|
|
1730
|
-
}
|
|
1731
|
-
this.timers.clear();
|
|
1732
|
-
}
|
|
1733
|
-
destroy() {
|
|
1734
|
-
void this.flush();
|
|
1735
|
-
}
|
|
1736
|
-
};
|
|
1737
|
-
|
|
1738
|
-
// src/hub/backup.ts
|
|
1739
|
-
import { concatBytes, decrypt, encrypt, NONCE_SIZE } from "@xnetjs/crypto";
|
|
1740
|
-
var toHttpUrl = (hubUrl) => {
|
|
1741
|
-
try {
|
|
1742
|
-
const url = new URL(hubUrl);
|
|
1743
|
-
if (url.protocol === "ws:") url.protocol = "http:";
|
|
1744
|
-
if (url.protocol === "wss:") url.protocol = "https:";
|
|
1745
|
-
return url.toString().replace(/\/$/, "");
|
|
1746
|
-
} catch {
|
|
1747
|
-
return hubUrl;
|
|
1748
|
-
}
|
|
1749
|
-
};
|
|
1750
|
-
var encodeEncrypted = (encrypted) => concatBytes(encrypted.nonce, encrypted.ciphertext);
|
|
1751
|
-
var decodeEncrypted = (payload) => {
|
|
1752
|
-
const nonce = payload.slice(0, NONCE_SIZE);
|
|
1753
|
-
const ciphertext = payload.slice(NONCE_SIZE);
|
|
1754
|
-
return { nonce, ciphertext };
|
|
1755
|
-
};
|
|
1756
|
-
var resolveFetch = (fetchFn) => {
|
|
1757
|
-
if (fetchFn) return fetchFn;
|
|
1758
|
-
if (typeof fetch === "undefined") {
|
|
1759
|
-
throw new Error("fetch is not available in this environment");
|
|
1760
|
-
}
|
|
1761
|
-
return fetch;
|
|
1762
|
-
};
|
|
1763
|
-
var buildAuthHeader = async (getAuthToken) => {
|
|
1764
|
-
if (!getAuthToken) return null;
|
|
1765
|
-
const token = await getAuthToken();
|
|
1766
|
-
if (!token) return null;
|
|
1767
|
-
return `Bearer ${token}`;
|
|
1768
|
-
};
|
|
1769
|
-
async function uploadBackup(config, docId, plaintext) {
|
|
1770
|
-
const encrypted = encrypt(plaintext, config.encryptionKey);
|
|
1771
|
-
const payload = encodeEncrypted(encrypted);
|
|
1772
|
-
return uploadEncryptedBackup(config, docId, payload);
|
|
1773
|
-
}
|
|
1774
|
-
async function uploadEncryptedBackup(config, docId, payload) {
|
|
1775
|
-
const httpUrl = toHttpUrl(config.hubUrl);
|
|
1776
|
-
const fetcher = resolveFetch(config.fetchFn);
|
|
1777
|
-
const authHeader = await buildAuthHeader(config.getAuthToken);
|
|
1778
|
-
const res = await fetcher(`${httpUrl}/backup/${encodeURIComponent(docId)}`, {
|
|
1779
|
-
method: "PUT",
|
|
1780
|
-
headers: {
|
|
1781
|
-
...authHeader ? { Authorization: authHeader } : {},
|
|
1782
|
-
"Content-Type": "application/octet-stream"
|
|
1783
|
-
},
|
|
1784
|
-
body: payload
|
|
1785
|
-
});
|
|
1786
|
-
if (!res.ok) {
|
|
1787
|
-
throw new Error(`Backup upload failed: ${res.status} ${res.statusText}`);
|
|
1788
|
-
}
|
|
1789
|
-
return res.json();
|
|
1790
|
-
}
|
|
1791
|
-
async function downloadBackup(config, docId) {
|
|
1792
|
-
const payload = await downloadEncryptedBackup(config, docId);
|
|
1793
|
-
if (!payload) return null;
|
|
1794
|
-
const encrypted = decodeEncrypted(payload);
|
|
1795
|
-
return decrypt(encrypted, config.encryptionKey);
|
|
1796
|
-
}
|
|
1797
|
-
async function downloadEncryptedBackup(config, docId) {
|
|
1798
|
-
const httpUrl = toHttpUrl(config.hubUrl);
|
|
1799
|
-
const fetcher = resolveFetch(config.fetchFn);
|
|
1800
|
-
const authHeader = await buildAuthHeader(config.getAuthToken);
|
|
1801
|
-
const res = await fetcher(`${httpUrl}/backup/${encodeURIComponent(docId)}`, {
|
|
1802
|
-
headers: authHeader ? { Authorization: authHeader } : void 0
|
|
1803
|
-
});
|
|
1804
|
-
if (res.status === 404) return null;
|
|
1805
|
-
if (!res.ok) {
|
|
1806
|
-
throw new Error(`Backup download failed: ${res.status} ${res.statusText}`);
|
|
1807
|
-
}
|
|
1808
|
-
return new Uint8Array(await res.arrayBuffer());
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
// src/context.ts
|
|
1812
|
-
function log3(...args) {
|
|
1813
|
-
if (typeof localStorage !== "undefined" && localStorage.getItem("xnet:sync:debug") === "true") {
|
|
1814
|
-
console.log("[XNetProvider]", ...args);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
var HUB_CAPABILITIES = [
|
|
1818
|
-
{ with: "*", can: "hub/*" },
|
|
1819
|
-
{ with: "*", can: "backup/*" },
|
|
1820
|
-
{ with: "*", can: "files/*" },
|
|
1821
|
-
{ with: "*", can: "query/*" },
|
|
1822
|
-
{ with: "*", can: "index/*" }
|
|
1823
|
-
];
|
|
1824
|
-
var HUB_TOKEN_TTL_SECONDS = 60 * 60 * 24;
|
|
1825
|
-
var HUB_INDEX_DEBOUNCE_MS = 2e3;
|
|
1826
|
-
var XNetContext = createContext4(null);
|
|
1827
|
-
var DataBridgeContext = createContext4(null);
|
|
1828
|
-
function useDataBridge() {
|
|
1829
|
-
return useContext4(DataBridgeContext);
|
|
1830
|
-
}
|
|
1831
|
-
function XNetProvider({ config, children }) {
|
|
1832
|
-
const [nodeStore, setNodeStore] = useState3(null);
|
|
1833
|
-
const [nodeStoreReady, setNodeStoreReady] = useState3(false);
|
|
1834
|
-
const [dataBridge, setDataBridge] = useState3(null);
|
|
1835
|
-
const [syncManager, setSyncManager] = useState3(null);
|
|
1836
|
-
const [hubStatus, setHubStatus] = useState3("disconnected");
|
|
1837
|
-
const [pluginRegistry, setPluginRegistry] = useState3(null);
|
|
1838
|
-
const nodeStorageRef = useRef(null);
|
|
1839
|
-
const authorDID = config.authorDID ?? config.identity?.did;
|
|
1840
|
-
const hubUrl = config.hubUrl ?? null;
|
|
1841
|
-
const hubOptions = config.hubOptions;
|
|
1842
|
-
const autoAuth = hubOptions?.autoAuth ?? true;
|
|
1843
|
-
const staticHubAuthToken = hubOptions?.authToken?.trim() ?? "";
|
|
1844
|
-
const autoBackup = hubOptions?.autoBackup ?? false;
|
|
1845
|
-
const backupDebounceMs = hubOptions?.backupDebounceMs ?? 5e3;
|
|
1846
|
-
const enableSearchIndex = hubOptions?.enableSearchIndex ?? false;
|
|
1847
|
-
const nodeSyncRoom = hubOptions?.nodeSyncRoom ?? authorDID ?? "default";
|
|
1848
|
-
const encryptionKey = config.encryptionKey ?? null;
|
|
1849
|
-
const getHubAuthToken = useCallback(async () => {
|
|
1850
|
-
if (staticHubAuthToken) return staticHubAuthToken;
|
|
1851
|
-
if (!hubUrl || !autoAuth) return "";
|
|
1852
|
-
if (!authorDID || !config.signingKey) {
|
|
1853
|
-
throw new Error("Missing authorDID/signingKey for hub auth");
|
|
1854
|
-
}
|
|
1855
|
-
return createUCAN({
|
|
1856
|
-
issuer: authorDID,
|
|
1857
|
-
issuerKey: config.signingKey,
|
|
1858
|
-
audience: hubUrl,
|
|
1859
|
-
capabilities: HUB_CAPABILITIES,
|
|
1860
|
-
expiration: Math.floor(Date.now() / 1e3) + HUB_TOKEN_TTL_SECONDS
|
|
1861
|
-
});
|
|
1862
|
-
}, [authorDID, autoAuth, config.signingKey, hubUrl, staticHubAuthToken]);
|
|
1863
|
-
useEffect3(() => {
|
|
1864
|
-
const nodeStorageAdapter = config.nodeStorage ?? new MemoryNodeStorageAdapter();
|
|
1865
|
-
nodeStorageRef.current = nodeStorageAdapter;
|
|
1866
|
-
const signingKey = config.signingKey;
|
|
1867
|
-
if (!authorDID || !signingKey) {
|
|
1868
|
-
console.warn(
|
|
1869
|
-
"XNetProvider: authorDID and signingKey not provided. NodeStore will not be initialized. Provide these via config.authorDID/config.signingKey or config.identity."
|
|
1870
|
-
);
|
|
1871
|
-
return;
|
|
1872
|
-
}
|
|
1873
|
-
let cancelled = false;
|
|
1874
|
-
const initializeNodeStore = async () => {
|
|
1875
|
-
if ("open" in nodeStorageAdapter && typeof nodeStorageAdapter.open === "function") {
|
|
1876
|
-
await nodeStorageAdapter.open();
|
|
1877
|
-
}
|
|
1878
|
-
if (cancelled) return;
|
|
1879
|
-
const ns = new NodeStore({
|
|
1880
|
-
storage: nodeStorageAdapter,
|
|
1881
|
-
authorDID,
|
|
1882
|
-
signingKey
|
|
1883
|
-
});
|
|
1884
|
-
await ns.initialize();
|
|
1885
|
-
if (cancelled) return;
|
|
1886
|
-
const bridge = config.dataBridge ?? createMainThreadBridge(ns);
|
|
1887
|
-
setNodeStore(ns);
|
|
1888
|
-
setNodeStoreReady(true);
|
|
1889
|
-
setDataBridge(bridge);
|
|
1890
|
-
bridgeRef = config.dataBridge ? null : bridge;
|
|
1891
|
-
if (typeof window !== "undefined") {
|
|
1892
|
-
const win = window;
|
|
1893
|
-
win.__xnetNodeStore = ns;
|
|
1894
|
-
}
|
|
1895
|
-
};
|
|
1896
|
-
let bridgeRef = null;
|
|
1897
|
-
initializeNodeStore();
|
|
1898
|
-
return () => {
|
|
1899
|
-
cancelled = true;
|
|
1900
|
-
if (bridgeRef) {
|
|
1901
|
-
bridgeRef.destroy();
|
|
1902
|
-
}
|
|
1903
|
-
setDataBridge(null);
|
|
1904
|
-
setNodeStore(null);
|
|
1905
|
-
setNodeStoreReady(false);
|
|
1906
|
-
if (typeof window !== "undefined") {
|
|
1907
|
-
delete window.__xnetNodeStore;
|
|
1908
|
-
}
|
|
1909
|
-
if ("close" in nodeStorageAdapter && typeof nodeStorageAdapter.close === "function") {
|
|
1910
|
-
nodeStorageAdapter.close();
|
|
1911
|
-
}
|
|
1912
|
-
};
|
|
1913
|
-
}, [authorDID, config.nodeStorage, config.signingKey, config.dataBridge]);
|
|
1914
|
-
useEffect3(() => {
|
|
1915
|
-
if (config.syncManager) {
|
|
1916
|
-
setSyncManager(config.syncManager);
|
|
1917
|
-
const sm2 = config.syncManager;
|
|
1918
|
-
if (sm2.setIdentity && authorDID && config.signingKey) {
|
|
1919
|
-
sm2.setIdentity(authorDID, config.signingKey);
|
|
1920
|
-
}
|
|
1921
|
-
config.syncManager.start().catch((err) => {
|
|
1922
|
-
console.warn("[XNetProvider] External SyncManager failed to start:", err);
|
|
1923
|
-
});
|
|
1924
|
-
return () => {
|
|
1925
|
-
config.syncManager.stop().catch((err) => {
|
|
1926
|
-
console.warn("[XNetProvider] External SyncManager failed to stop:", err);
|
|
1927
|
-
});
|
|
1928
|
-
setSyncManager(null);
|
|
1929
|
-
};
|
|
1930
|
-
}
|
|
1931
|
-
if (!nodeStore || !nodeStoreReady || config.disableSyncManager) {
|
|
1932
|
-
log3("SyncManager disabled or NodeStore not ready", {
|
|
1933
|
-
nodeStore: !!nodeStore,
|
|
1934
|
-
nodeStoreReady,
|
|
1935
|
-
disableSyncManager: config.disableSyncManager
|
|
1936
|
-
});
|
|
1937
|
-
setSyncManager(null);
|
|
1938
|
-
return;
|
|
1939
|
-
}
|
|
1940
|
-
const storage = nodeStorageRef.current;
|
|
1941
|
-
if (!storage) {
|
|
1942
|
-
log3("No storage adapter available");
|
|
1943
|
-
return;
|
|
1944
|
-
}
|
|
1945
|
-
const signalingUrl = hubUrl ?? config.signalingServers?.[0] ?? "ws://localhost:4444";
|
|
1946
|
-
if (autoAuth && hubUrl && (!authorDID || !config.signingKey)) {
|
|
1947
|
-
console.warn("[XNetProvider] Hub auth enabled but authorDID/signingKey missing");
|
|
1948
|
-
}
|
|
1949
|
-
if (autoBackup && (!hubUrl || !encryptionKey)) {
|
|
1950
|
-
console.warn("[XNetProvider] Auto-backup requires hubUrl and encryptionKey");
|
|
1951
|
-
}
|
|
1952
|
-
console.log("[XNetProvider] Creating SyncManager with signalingUrl:", signalingUrl);
|
|
1953
|
-
log3("Creating SyncManager with signalingUrl:", signalingUrl);
|
|
1954
|
-
let autoBackupManager = null;
|
|
1955
|
-
const enableAutoBackup = Boolean(autoBackup && hubUrl && encryptionKey);
|
|
1956
|
-
const sm = createSyncManager({
|
|
1957
|
-
nodeStore,
|
|
1958
|
-
storage,
|
|
1959
|
-
signalingUrl,
|
|
1960
|
-
authorDID,
|
|
1961
|
-
blobStore: config.blobStore,
|
|
1962
|
-
nodeSyncRoom: hubUrl ? nodeSyncRoom : void 0,
|
|
1963
|
-
getUCANToken: hubUrl ? getHubAuthToken : void 0,
|
|
1964
|
-
onDocUpdate: enableAutoBackup ? (nodeId, doc) => {
|
|
1965
|
-
autoBackupManager?.handleDocUpdate(nodeId, doc);
|
|
1966
|
-
} : void 0,
|
|
1967
|
-
onDocEvict: enableAutoBackup ? (nodeId, doc) => {
|
|
1968
|
-
autoBackupManager?.handleDocEvict(nodeId, doc);
|
|
1969
|
-
} : void 0
|
|
1970
|
-
});
|
|
1971
|
-
if (enableAutoBackup && hubUrl && encryptionKey) {
|
|
1972
|
-
autoBackupManager = new AutoBackup(
|
|
1973
|
-
async (docId, plaintext) => {
|
|
1974
|
-
await uploadBackup(
|
|
1975
|
-
{
|
|
1976
|
-
hubUrl,
|
|
1977
|
-
encryptionKey,
|
|
1978
|
-
getAuthToken: autoAuth ? getHubAuthToken : void 0
|
|
1979
|
-
},
|
|
1980
|
-
docId,
|
|
1981
|
-
plaintext
|
|
1982
|
-
);
|
|
1983
|
-
},
|
|
1984
|
-
{
|
|
1985
|
-
debounceMs: backupDebounceMs,
|
|
1986
|
-
isEnabled: () => sm.connection?.status === "connected"
|
|
1987
|
-
}
|
|
1988
|
-
);
|
|
1989
|
-
}
|
|
1990
|
-
setSyncManager(sm);
|
|
1991
|
-
console.log("[XNetProvider] SyncManager created and set in context");
|
|
1992
|
-
log3("SyncManager created, starting...");
|
|
1993
|
-
sm.start().then(() => {
|
|
1994
|
-
log3("SyncManager started successfully");
|
|
1995
|
-
}).catch((err) => {
|
|
1996
|
-
console.warn("[XNetProvider] SyncManager failed to start:", err);
|
|
1997
|
-
log3("SyncManager start failed:", err);
|
|
1998
|
-
});
|
|
1999
|
-
return () => {
|
|
2000
|
-
sm.stop().catch((err) => {
|
|
2001
|
-
console.warn("[XNetProvider] SyncManager failed to stop:", err);
|
|
2002
|
-
});
|
|
2003
|
-
autoBackupManager?.destroy();
|
|
2004
|
-
setSyncManager(null);
|
|
2005
|
-
};
|
|
2006
|
-
}, [
|
|
2007
|
-
nodeStore,
|
|
2008
|
-
nodeStoreReady,
|
|
2009
|
-
config.disableSyncManager,
|
|
2010
|
-
config.syncManager,
|
|
2011
|
-
config.signalingServers,
|
|
2012
|
-
config.blobStore,
|
|
2013
|
-
authorDID,
|
|
2014
|
-
autoAuth,
|
|
2015
|
-
autoBackup,
|
|
2016
|
-
backupDebounceMs,
|
|
2017
|
-
encryptionKey,
|
|
2018
|
-
getHubAuthToken,
|
|
2019
|
-
hubUrl,
|
|
2020
|
-
nodeSyncRoom
|
|
2021
|
-
]);
|
|
2022
|
-
useEffect3(() => {
|
|
2023
|
-
if (!dataBridge || !syncManager) return;
|
|
2024
|
-
if (dataBridge instanceof MainThreadBridge) {
|
|
2025
|
-
dataBridge.setSyncManager(syncManager);
|
|
2026
|
-
log3("Connected SyncManager to DataBridge");
|
|
2027
|
-
}
|
|
2028
|
-
return () => {
|
|
2029
|
-
if (dataBridge instanceof MainThreadBridge) {
|
|
2030
|
-
dataBridge.setSyncManager(null);
|
|
2031
|
-
}
|
|
2032
|
-
};
|
|
2033
|
-
}, [dataBridge, syncManager]);
|
|
2034
|
-
useEffect3(() => {
|
|
2035
|
-
if (!syncManager) {
|
|
2036
|
-
setHubStatus("disconnected");
|
|
2037
|
-
return;
|
|
2038
|
-
}
|
|
2039
|
-
setHubStatus(syncManager.status);
|
|
2040
|
-
return syncManager.on("status", (status) => {
|
|
2041
|
-
setHubStatus(status);
|
|
2042
|
-
});
|
|
2043
|
-
}, [syncManager]);
|
|
2044
|
-
useEffect3(() => {
|
|
2045
|
-
if (!nodeStore || !syncManager || !hubUrl || !enableSearchIndex) return;
|
|
2046
|
-
const connection = syncManager.connection;
|
|
2047
|
-
if (!connection) return;
|
|
2048
|
-
const timers = /* @__PURE__ */ new Map();
|
|
2049
|
-
const pending = /* @__PURE__ */ new Map();
|
|
2050
|
-
const schedule = (docId, payload) => {
|
|
2051
|
-
pending.set(docId, payload);
|
|
2052
|
-
const existing = timers.get(docId);
|
|
2053
|
-
if (existing) clearTimeout(existing);
|
|
2054
|
-
timers.set(
|
|
2055
|
-
docId,
|
|
2056
|
-
setTimeout(() => {
|
|
2057
|
-
timers.delete(docId);
|
|
2058
|
-
const next = pending.get(docId);
|
|
2059
|
-
pending.delete(docId);
|
|
2060
|
-
if (!next) return;
|
|
2061
|
-
if (connection.status !== "connected") return;
|
|
2062
|
-
if (next.type === "remove") {
|
|
2063
|
-
connection.sendRaw({ type: "index-remove", docId });
|
|
2064
|
-
return;
|
|
2065
|
-
}
|
|
2066
|
-
connection.sendRaw({
|
|
2067
|
-
type: "index-update",
|
|
2068
|
-
docId,
|
|
2069
|
-
meta: next.meta
|
|
2070
|
-
});
|
|
2071
|
-
}, HUB_INDEX_DEBOUNCE_MS)
|
|
2072
|
-
);
|
|
2073
|
-
};
|
|
2074
|
-
const handleChange = (event) => {
|
|
2075
|
-
const node = event.node;
|
|
2076
|
-
if (!node || node.deleted) {
|
|
2077
|
-
schedule(event.change.payload.nodeId, { type: "remove" });
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
if (!node.schemaId) return;
|
|
2081
|
-
const title = typeof node.properties.title === "string" ? node.properties.title : "";
|
|
2082
|
-
schedule(node.id, {
|
|
2083
|
-
type: "update",
|
|
2084
|
-
meta: {
|
|
2085
|
-
schemaIri: node.schemaId,
|
|
2086
|
-
title,
|
|
2087
|
-
properties: node.properties
|
|
2088
|
-
}
|
|
2089
|
-
});
|
|
2090
|
-
};
|
|
2091
|
-
const unsubscribe = nodeStore.subscribe(handleChange);
|
|
2092
|
-
return () => {
|
|
2093
|
-
unsubscribe();
|
|
2094
|
-
for (const timer of timers.values()) {
|
|
2095
|
-
clearTimeout(timer);
|
|
2096
|
-
}
|
|
2097
|
-
timers.clear();
|
|
2098
|
-
pending.clear();
|
|
2099
|
-
};
|
|
2100
|
-
}, [enableSearchIndex, hubUrl, nodeStore, syncManager]);
|
|
2101
|
-
useEffect3(() => {
|
|
2102
|
-
if (!nodeStore || !nodeStoreReady || config.disablePlugins) {
|
|
2103
|
-
log3("PluginRegistry disabled or NodeStore not ready");
|
|
2104
|
-
setPluginRegistry(null);
|
|
2105
|
-
return;
|
|
2106
|
-
}
|
|
2107
|
-
const platform = config.platform ?? "web";
|
|
2108
|
-
log3("Creating PluginRegistry with platform:", platform);
|
|
2109
|
-
const registry = new PluginRegistry(nodeStore, platform);
|
|
2110
|
-
setPluginRegistry(registry);
|
|
2111
|
-
registry.loadFromStore().catch((err) => {
|
|
2112
|
-
console.warn("[XNetProvider] Failed to load plugins from store:", err);
|
|
2113
|
-
});
|
|
2114
|
-
return () => {
|
|
2115
|
-
const plugins = registry.getAll();
|
|
2116
|
-
for (const plugin of plugins) {
|
|
2117
|
-
if (plugin.status === "active") {
|
|
2118
|
-
registry.deactivate(plugin.manifest.id).catch((err) => {
|
|
2119
|
-
console.warn(`[XNetProvider] Failed to deactivate plugin ${plugin.manifest.id}:`, err);
|
|
2120
|
-
});
|
|
2121
|
-
}
|
|
2122
|
-
}
|
|
2123
|
-
setPluginRegistry(null);
|
|
2124
|
-
};
|
|
2125
|
-
}, [nodeStore, nodeStoreReady, config.disablePlugins, config.platform]);
|
|
2126
|
-
const value = useMemo2(
|
|
2127
|
-
() => ({
|
|
2128
|
-
nodeStore,
|
|
2129
|
-
nodeStoreReady,
|
|
2130
|
-
identity: config.identity,
|
|
2131
|
-
authorDID: authorDID ?? null,
|
|
2132
|
-
syncManager,
|
|
2133
|
-
hubUrl,
|
|
2134
|
-
hubStatus,
|
|
2135
|
-
hubConnection: syncManager?.connection ?? null,
|
|
2136
|
-
getHubAuthToken: hubUrl ? getHubAuthToken : void 0,
|
|
2137
|
-
encryptionKey,
|
|
2138
|
-
blobStore: config.blobStore ?? null,
|
|
2139
|
-
pluginRegistry
|
|
2140
|
-
}),
|
|
2141
|
-
[
|
|
2142
|
-
nodeStore,
|
|
2143
|
-
nodeStoreReady,
|
|
2144
|
-
config.identity,
|
|
2145
|
-
authorDID,
|
|
2146
|
-
syncManager,
|
|
2147
|
-
hubUrl,
|
|
2148
|
-
hubStatus,
|
|
2149
|
-
getHubAuthToken,
|
|
2150
|
-
encryptionKey,
|
|
2151
|
-
config.blobStore,
|
|
2152
|
-
pluginRegistry
|
|
2153
|
-
]
|
|
2154
|
-
);
|
|
2155
|
-
let content = pluginRegistry ? React.createElement(PluginRegistryContext.Provider, { value: pluginRegistry }, children) : children;
|
|
2156
|
-
if (dataBridge) {
|
|
2157
|
-
content = React.createElement(DataBridgeContext.Provider, { value: dataBridge }, content);
|
|
2158
|
-
}
|
|
2159
|
-
if (config.telemetry) {
|
|
2160
|
-
content = React.createElement(TelemetryContext.Provider, { value: config.telemetry }, content);
|
|
2161
|
-
}
|
|
2162
|
-
content = React.createElement(SecurityProvider, {
|
|
2163
|
-
level: config.security?.level,
|
|
2164
|
-
minVerificationLevel: config.security?.minVerificationLevel,
|
|
2165
|
-
verificationPolicy: config.security?.verificationPolicy,
|
|
2166
|
-
registry: config.security?.registry,
|
|
2167
|
-
keyBundle: config.keyBundle,
|
|
2168
|
-
children: content
|
|
2169
|
-
});
|
|
2170
|
-
return React.createElement(XNetContext.Provider, { value }, content);
|
|
2171
|
-
}
|
|
2172
|
-
function useXNet() {
|
|
2173
|
-
const context = useContext4(XNetContext);
|
|
2174
|
-
if (!context) {
|
|
2175
|
-
throw new Error("useXNet must be used within an XNetProvider");
|
|
2176
|
-
}
|
|
2177
|
-
return context;
|
|
2178
|
-
}
|
|
2179
|
-
|
|
2180
|
-
// src/instrumentation.ts
|
|
2181
|
-
import { createContext as createContext5, useContext as useContext5 } from "react";
|
|
2182
|
-
var InstrumentationContext = createContext5(null);
|
|
2183
|
-
function useInstrumentation() {
|
|
2184
|
-
return useContext5(InstrumentationContext);
|
|
2185
|
-
}
|
|
2186
|
-
|
|
2187
|
-
// src/hooks/useNodeStore.ts
|
|
2188
|
-
import { useContext as useContext6 } from "react";
|
|
2189
|
-
function useNodeStore() {
|
|
2190
|
-
const context = useContext6(XNetContext);
|
|
2191
|
-
if (context) {
|
|
2192
|
-
return {
|
|
2193
|
-
store: context.nodeStore,
|
|
2194
|
-
isReady: context.nodeStoreReady,
|
|
2195
|
-
error: null
|
|
2196
|
-
};
|
|
2197
|
-
}
|
|
2198
|
-
throw new Error("useNodeStore must be used within an XNetProvider");
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
export {
|
|
2202
|
-
SecurityProvider,
|
|
2203
|
-
useSecurityContext,
|
|
2204
|
-
useSecurityContextOptional,
|
|
2205
|
-
TelemetryContext,
|
|
2206
|
-
useTelemetryReporter,
|
|
2207
|
-
PluginRegistryContext,
|
|
2208
|
-
usePluginRegistry,
|
|
2209
|
-
usePluginRegistryOptional,
|
|
2210
|
-
usePlugins,
|
|
2211
|
-
useContributions,
|
|
2212
|
-
useViews,
|
|
2213
|
-
useCommands,
|
|
2214
|
-
useSlashCommands,
|
|
2215
|
-
useSidebarItems,
|
|
2216
|
-
useEditorExtensions,
|
|
2217
|
-
useEditorExtensionsSafe,
|
|
2218
|
-
useView,
|
|
2219
|
-
useCommand,
|
|
2220
|
-
uploadBackup,
|
|
2221
|
-
downloadBackup,
|
|
2222
|
-
createConnectionManager,
|
|
2223
|
-
METABRIDGE_ORIGIN,
|
|
2224
|
-
METABRIDGE_SEED_ORIGIN,
|
|
2225
|
-
createMetaBridge,
|
|
2226
|
-
createNodePool,
|
|
2227
|
-
NodeStoreSyncProvider,
|
|
2228
|
-
createOfflineQueue,
|
|
2229
|
-
createRegistry,
|
|
2230
|
-
createSyncManager,
|
|
2231
|
-
XNetContext,
|
|
2232
|
-
useDataBridge,
|
|
2233
|
-
XNetProvider,
|
|
2234
|
-
useXNet,
|
|
2235
|
-
InstrumentationContext,
|
|
2236
|
-
useInstrumentation,
|
|
2237
|
-
useNodeStore
|
|
2238
|
-
};
|