@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
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
import {
|
|
2
|
+
flattenNode,
|
|
3
|
+
useQuery,
|
|
4
|
+
useSyncManager
|
|
5
|
+
} from "./chunk-6VOICQZ3.js";
|
|
6
|
+
import {
|
|
7
|
+
useInstrumentation
|
|
8
|
+
} from "./chunk-JCOFKBOB.js";
|
|
9
|
+
import {
|
|
10
|
+
useDataBridge,
|
|
11
|
+
useNodeStore,
|
|
12
|
+
useXNet,
|
|
13
|
+
useXNetInternal
|
|
14
|
+
} from "./chunk-IHTMVTTE.js";
|
|
15
|
+
|
|
16
|
+
// src/hooks/useInfiniteQuery.ts
|
|
17
|
+
import { createQueryDescriptor, serializeQueryDescriptor } from "@xnetjs/data-bridge";
|
|
18
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
19
|
+
var DEFAULT_PAGE_SIZE = 50;
|
|
20
|
+
function getBaseFilter(filter) {
|
|
21
|
+
const baseFilter = { ...filter };
|
|
22
|
+
delete baseFilter.pageSize;
|
|
23
|
+
delete baseFilter.maxLoaded;
|
|
24
|
+
delete baseFilter.page;
|
|
25
|
+
return {
|
|
26
|
+
...baseFilter,
|
|
27
|
+
orderBy: baseFilter.orderBy ?? { updatedAt: "desc" }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function chunkIntoPages(data, pageSize, pageInfo) {
|
|
31
|
+
if (data.length === 0) return [];
|
|
32
|
+
const pages = [];
|
|
33
|
+
for (let i = 0; i < data.length; i += pageSize) {
|
|
34
|
+
pages.push({ cursor: null, data: data.slice(i, i + pageSize), pageInfo });
|
|
35
|
+
}
|
|
36
|
+
return pages;
|
|
37
|
+
}
|
|
38
|
+
function useInfiniteQuery(schema, filter = {}) {
|
|
39
|
+
const pageSize = filter.page?.first ?? filter.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
40
|
+
const count = filter.page?.count;
|
|
41
|
+
const maxLoaded = filter.maxLoaded;
|
|
42
|
+
const baseFilter = useMemo(() => getBaseFilter(filter), [filter]);
|
|
43
|
+
const baseKey = useMemo(
|
|
44
|
+
() => serializeQueryDescriptor(createQueryDescriptor(schema._schemaId, baseFilter)),
|
|
45
|
+
[schema._schemaId, baseFilter]
|
|
46
|
+
);
|
|
47
|
+
const [loadedCount, setLoadedCount] = useState(
|
|
48
|
+
() => maxLoaded === void 0 ? pageSize : Math.min(pageSize, maxLoaded)
|
|
49
|
+
);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
setLoadedCount(maxLoaded === void 0 ? pageSize : Math.min(pageSize, maxLoaded));
|
|
52
|
+
}, [baseKey, pageSize, maxLoaded]);
|
|
53
|
+
const windowFilter = useMemo(
|
|
54
|
+
() => count ? { ...baseFilter, page: { first: loadedCount, count } } : { ...baseFilter, limit: loadedCount },
|
|
55
|
+
[baseFilter, loadedCount, count]
|
|
56
|
+
);
|
|
57
|
+
const current = useQuery(schema, windowFilter);
|
|
58
|
+
const lastDataRef = useRef([]);
|
|
59
|
+
if (!current.loading) {
|
|
60
|
+
lastDataRef.current = current.data;
|
|
61
|
+
}
|
|
62
|
+
const data = current.loading && current.data.length === 0 ? lastDataRef.current : current.data;
|
|
63
|
+
const atCeiling = maxLoaded !== void 0 && loadedCount >= maxLoaded;
|
|
64
|
+
const moreAvailable = current.pageInfo.hasMore || !current.loading && current.data.length >= loadedCount;
|
|
65
|
+
const hasMore = moreAvailable && !atCeiling;
|
|
66
|
+
const isFetchingNextPage = current.loading && data.length > 0;
|
|
67
|
+
const fetchNextPage = useCallback(async () => {
|
|
68
|
+
if (current.loading || !hasMore) return;
|
|
69
|
+
setLoadedCount((n) => {
|
|
70
|
+
if (maxLoaded !== void 0 && n >= maxLoaded) return n;
|
|
71
|
+
const next = n + pageSize;
|
|
72
|
+
return maxLoaded === void 0 ? next : Math.min(next, maxLoaded);
|
|
73
|
+
});
|
|
74
|
+
}, [current.loading, hasMore, maxLoaded, pageSize]);
|
|
75
|
+
const reset = useCallback(() => {
|
|
76
|
+
lastDataRef.current = [];
|
|
77
|
+
setLoadedCount(maxLoaded === void 0 ? pageSize : Math.min(pageSize, maxLoaded));
|
|
78
|
+
}, [maxLoaded, pageSize]);
|
|
79
|
+
const reload = useCallback(() => {
|
|
80
|
+
current.reload();
|
|
81
|
+
}, [current]);
|
|
82
|
+
const pages = useMemo(
|
|
83
|
+
() => chunkIntoPages(data, pageSize, current.pageInfo),
|
|
84
|
+
[data, pageSize, current.pageInfo]
|
|
85
|
+
);
|
|
86
|
+
const pageInfo = useMemo(
|
|
87
|
+
() => ({ ...current.pageInfo, hasMore, hasNextPage: hasMore, loadedCount: data.length }),
|
|
88
|
+
[current.pageInfo, hasMore, data.length]
|
|
89
|
+
);
|
|
90
|
+
const loading = current.loading && data.length === 0;
|
|
91
|
+
return {
|
|
92
|
+
data,
|
|
93
|
+
pages,
|
|
94
|
+
status: current.status,
|
|
95
|
+
loading,
|
|
96
|
+
isLoading: loading,
|
|
97
|
+
isFetching: current.isFetching || isFetchingNextPage,
|
|
98
|
+
isFetchingNextPage,
|
|
99
|
+
isLive: current.isLive,
|
|
100
|
+
source: current.source,
|
|
101
|
+
error: current.error,
|
|
102
|
+
reload,
|
|
103
|
+
reset,
|
|
104
|
+
fetchNextPage,
|
|
105
|
+
migrationWarnings: current.migrationWarnings,
|
|
106
|
+
pageInfo,
|
|
107
|
+
totalCount: pageInfo.totalCount,
|
|
108
|
+
hasMore,
|
|
109
|
+
plan: current.plan,
|
|
110
|
+
materialized: current.materialized,
|
|
111
|
+
completeness: current.completeness,
|
|
112
|
+
staleness: current.staleness,
|
|
113
|
+
verification: current.verification,
|
|
114
|
+
stream: current.stream
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/hooks/useNode.ts
|
|
119
|
+
import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2, useRef as useRef2, useMemo as useMemo2 } from "react";
|
|
120
|
+
import * as Y from "yjs";
|
|
121
|
+
import { METABRIDGE_ORIGIN, METABRIDGE_SEED_ORIGIN } from "@xnetjs/runtime";
|
|
122
|
+
import { WebSocketSyncProvider } from "@xnetjs/runtime";
|
|
123
|
+
function log(...args) {
|
|
124
|
+
if (typeof localStorage !== "undefined" && localStorage.getItem("xnet:sync:debug") === "true") {
|
|
125
|
+
console.log("[useNode]", ...args);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
var DEFAULT_SIGNALING_SERVERS = ["ws://localhost:4444"];
|
|
129
|
+
var DEFAULT_PERSIST_DEBOUNCE = 1e3;
|
|
130
|
+
function generateColor(seed) {
|
|
131
|
+
let hash = 0;
|
|
132
|
+
for (let i = 0; i < seed.length; i++) {
|
|
133
|
+
hash = seed.charCodeAt(i) + ((hash << 5) - hash);
|
|
134
|
+
}
|
|
135
|
+
const hue = Math.abs(hash % 360);
|
|
136
|
+
const s = 0.7;
|
|
137
|
+
const l = 0.5;
|
|
138
|
+
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
139
|
+
const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
|
|
140
|
+
const m = l - c / 2;
|
|
141
|
+
let r = 0, g = 0, b = 0;
|
|
142
|
+
if (hue < 60) {
|
|
143
|
+
r = c;
|
|
144
|
+
g = x;
|
|
145
|
+
b = 0;
|
|
146
|
+
} else if (hue < 120) {
|
|
147
|
+
r = x;
|
|
148
|
+
g = c;
|
|
149
|
+
b = 0;
|
|
150
|
+
} else if (hue < 180) {
|
|
151
|
+
r = 0;
|
|
152
|
+
g = c;
|
|
153
|
+
b = x;
|
|
154
|
+
} else if (hue < 240) {
|
|
155
|
+
r = 0;
|
|
156
|
+
g = x;
|
|
157
|
+
b = c;
|
|
158
|
+
} else if (hue < 300) {
|
|
159
|
+
r = x;
|
|
160
|
+
g = 0;
|
|
161
|
+
b = c;
|
|
162
|
+
} else {
|
|
163
|
+
r = c;
|
|
164
|
+
g = 0;
|
|
165
|
+
b = x;
|
|
166
|
+
}
|
|
167
|
+
const toHex = (v) => Math.round((v + m) * 255).toString(16).padStart(2, "0");
|
|
168
|
+
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
|
|
169
|
+
}
|
|
170
|
+
function hasSyncManagerSetter(bridge) {
|
|
171
|
+
if (!bridge || typeof bridge !== "object") {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const candidate = bridge;
|
|
175
|
+
return typeof candidate.setSyncManager === "function";
|
|
176
|
+
}
|
|
177
|
+
var pendingFlushes = /* @__PURE__ */ new Map();
|
|
178
|
+
function nowMs() {
|
|
179
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
180
|
+
}
|
|
181
|
+
function useNode(schema, id, options = {}) {
|
|
182
|
+
const {
|
|
183
|
+
signalingServers = DEFAULT_SIGNALING_SERVERS,
|
|
184
|
+
disableSync = false,
|
|
185
|
+
persistDebounce = DEFAULT_PERSIST_DEBOUNCE,
|
|
186
|
+
createIfMissing,
|
|
187
|
+
did
|
|
188
|
+
} = options;
|
|
189
|
+
const createIfMissingRef = useRef2(createIfMissing);
|
|
190
|
+
createIfMissingRef.current = createIfMissing;
|
|
191
|
+
const { store, isReady } = useNodeStore();
|
|
192
|
+
const syncManager = useSyncManager();
|
|
193
|
+
const bridge = useDataBridge();
|
|
194
|
+
const {
|
|
195
|
+
authorDID: syncAuthorDID,
|
|
196
|
+
signingKey: syncSigningKey,
|
|
197
|
+
sync: syncConfig
|
|
198
|
+
} = useXNetInternal();
|
|
199
|
+
const instrumentation = useInstrumentation();
|
|
200
|
+
const schemaId = schema._schemaId;
|
|
201
|
+
const hasDocument = schema.schema.document === "yjs";
|
|
202
|
+
const usingBridgeRef = useRef2(false);
|
|
203
|
+
const [data, setData] = useState2(null);
|
|
204
|
+
const [doc, setDoc] = useState2(null);
|
|
205
|
+
const [loading, setLoading] = useState2(true);
|
|
206
|
+
const [error, setError] = useState2(null);
|
|
207
|
+
const [isDirty, setIsDirty] = useState2(false);
|
|
208
|
+
const [lastSavedAt, setLastSavedAt] = useState2(null);
|
|
209
|
+
const [syncStatus, setSyncStatus] = useState2("offline");
|
|
210
|
+
const [syncError, setSyncError] = useState2(null);
|
|
211
|
+
const [peerCount, setPeerCount] = useState2(0);
|
|
212
|
+
const [wasCreated, setWasCreated] = useState2(false);
|
|
213
|
+
const [livePresence, setLivePresence] = useState2([]);
|
|
214
|
+
const [snapshotPresence, setSnapshotPresence] = useState2([]);
|
|
215
|
+
const [awareness, setAwareness] = useState2(null);
|
|
216
|
+
const presence = useMemo2(() => {
|
|
217
|
+
if (livePresence.length === 0 && snapshotPresence.length === 0) return [];
|
|
218
|
+
const liveByDid = new Map(livePresence.map((user) => [user.did, user]));
|
|
219
|
+
const snapshotByDid = new Map(snapshotPresence.map((user) => [user.did, user]));
|
|
220
|
+
const mergedLive = livePresence.map((user) => ({
|
|
221
|
+
...snapshotByDid.get(user.did),
|
|
222
|
+
...user,
|
|
223
|
+
isStale: false
|
|
224
|
+
}));
|
|
225
|
+
const mergedSnapshot = snapshotPresence.filter((user) => !liveByDid.has(user.did));
|
|
226
|
+
return [...mergedLive, ...mergedSnapshot];
|
|
227
|
+
}, [livePresence, snapshotPresence]);
|
|
228
|
+
const providerRef = useRef2(null);
|
|
229
|
+
const saveTimeoutRef = useRef2(null);
|
|
230
|
+
const docRef = useRef2(null);
|
|
231
|
+
const creatingRef = useRef2(false);
|
|
232
|
+
const storeRef = useRef2(store);
|
|
233
|
+
storeRef.current = store;
|
|
234
|
+
const dataRef = useRef2(null);
|
|
235
|
+
dataRef.current = data;
|
|
236
|
+
const loadDurationRef = useRef2(0);
|
|
237
|
+
const queryIdRef = useRef2(
|
|
238
|
+
`useNode-${schemaId}-${id || "null"}-${Math.random().toString(36).slice(2, 8)}`
|
|
239
|
+
);
|
|
240
|
+
useEffect2(() => {
|
|
241
|
+
if (!instrumentation?.queryTracker || !id) return;
|
|
242
|
+
instrumentation.queryTracker.register(queryIdRef.current, {
|
|
243
|
+
type: "useNode",
|
|
244
|
+
schemaId,
|
|
245
|
+
mode: "document",
|
|
246
|
+
descriptorKey: `node:${schemaId}:${id}`,
|
|
247
|
+
nodeId: id
|
|
248
|
+
});
|
|
249
|
+
return () => {
|
|
250
|
+
instrumentation.queryTracker.unregister(queryIdRef.current);
|
|
251
|
+
};
|
|
252
|
+
}, [instrumentation, schemaId, id]);
|
|
253
|
+
const load = useCallback2(async () => {
|
|
254
|
+
if (!store || !isReady || !id) {
|
|
255
|
+
setData(null);
|
|
256
|
+
setDoc(null);
|
|
257
|
+
setLoading(false);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (hasDocument && !disableSync && !syncManager) {
|
|
261
|
+
setLoading(true);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
setLoading(true);
|
|
265
|
+
setError(null);
|
|
266
|
+
setWasCreated(false);
|
|
267
|
+
setSyncError(null);
|
|
268
|
+
const loadStartedAt = nowMs();
|
|
269
|
+
try {
|
|
270
|
+
const pendingFlush = pendingFlushes.get(id);
|
|
271
|
+
if (pendingFlush) {
|
|
272
|
+
await pendingFlush;
|
|
273
|
+
}
|
|
274
|
+
let node = await store.get(id);
|
|
275
|
+
let justCreated = false;
|
|
276
|
+
if (!node && createIfMissingRef.current && !creatingRef.current) {
|
|
277
|
+
creatingRef.current = true;
|
|
278
|
+
try {
|
|
279
|
+
node = await store.create({
|
|
280
|
+
id,
|
|
281
|
+
schemaId,
|
|
282
|
+
properties: createIfMissingRef.current
|
|
283
|
+
});
|
|
284
|
+
justCreated = true;
|
|
285
|
+
setWasCreated(true);
|
|
286
|
+
log("Node auto-created:", id);
|
|
287
|
+
} finally {
|
|
288
|
+
creatingRef.current = false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (node?.deleted && createIfMissingRef.current) {
|
|
292
|
+
node = await store.restore(id);
|
|
293
|
+
}
|
|
294
|
+
if (!node || node.schemaId !== schemaId) {
|
|
295
|
+
setData(null);
|
|
296
|
+
setDoc(null);
|
|
297
|
+
setLoading(false);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
setData(flattenNode(node));
|
|
301
|
+
if (hasDocument) {
|
|
302
|
+
if (docRef.current && docRef.current.guid === id) {
|
|
303
|
+
setDoc(docRef.current);
|
|
304
|
+
setLastSavedAt(node.updatedAt);
|
|
305
|
+
} else {
|
|
306
|
+
if (docRef.current) {
|
|
307
|
+
instrumentation?.yDocRegistry.unregister(docRef.current.guid);
|
|
308
|
+
if (!usingBridgeRef.current) {
|
|
309
|
+
if (providerRef.current) {
|
|
310
|
+
providerRef.current.destroy();
|
|
311
|
+
providerRef.current = null;
|
|
312
|
+
}
|
|
313
|
+
docRef.current.destroy();
|
|
314
|
+
} else if (docRef.current.guid) {
|
|
315
|
+
syncManager?.release(docRef.current.guid);
|
|
316
|
+
}
|
|
317
|
+
docRef.current = null;
|
|
318
|
+
setDoc(null);
|
|
319
|
+
usingBridgeRef.current = false;
|
|
320
|
+
}
|
|
321
|
+
let ydoc;
|
|
322
|
+
let acquiredAwareness = null;
|
|
323
|
+
if (bridge?.acquireDoc && !disableSync) {
|
|
324
|
+
if (syncManager && hasSyncManagerSetter(bridge)) {
|
|
325
|
+
bridge.setSyncManager(syncManager);
|
|
326
|
+
}
|
|
327
|
+
const acquired = await bridge.acquireDoc(id);
|
|
328
|
+
ydoc = acquired.doc;
|
|
329
|
+
acquiredAwareness = acquired.awareness;
|
|
330
|
+
usingBridgeRef.current = true;
|
|
331
|
+
const storedContent = await store.getDocumentContent(id);
|
|
332
|
+
if (storedContent && storedContent.length > 0) {
|
|
333
|
+
Y.applyUpdate(ydoc, storedContent, "storage");
|
|
334
|
+
}
|
|
335
|
+
if (syncManager) {
|
|
336
|
+
syncManager.track(id, schemaId);
|
|
337
|
+
}
|
|
338
|
+
} else if (syncManager && !disableSync) {
|
|
339
|
+
ydoc = await syncManager.acquire(id);
|
|
340
|
+
usingBridgeRef.current = true;
|
|
341
|
+
const storedContent = await store.getDocumentContent(id);
|
|
342
|
+
if (storedContent && storedContent.length > 0) {
|
|
343
|
+
Y.applyUpdate(ydoc, storedContent, "storage");
|
|
344
|
+
}
|
|
345
|
+
syncManager.track(id, schemaId);
|
|
346
|
+
} else {
|
|
347
|
+
log("Using fallback WebSocketSyncProvider path for:", id);
|
|
348
|
+
ydoc = new Y.Doc({ guid: id, gc: false });
|
|
349
|
+
usingBridgeRef.current = false;
|
|
350
|
+
const storedContent = await store.getDocumentContent(id);
|
|
351
|
+
if (storedContent && storedContent.length > 0) {
|
|
352
|
+
Y.applyUpdate(ydoc, storedContent);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (acquiredAwareness) {
|
|
356
|
+
setAwareness(acquiredAwareness);
|
|
357
|
+
}
|
|
358
|
+
docRef.current = ydoc;
|
|
359
|
+
const metaMap = ydoc.getMap("meta");
|
|
360
|
+
if (metaMap.size === 0 && node.properties) {
|
|
361
|
+
const entries = Object.entries(node.properties);
|
|
362
|
+
const hasContent = entries.some(([, v]) => v !== "" && v !== null && v !== void 0);
|
|
363
|
+
if (hasContent || !justCreated) {
|
|
364
|
+
ydoc.transact(() => {
|
|
365
|
+
metaMap.set("_schemaId", schemaId);
|
|
366
|
+
for (const [key, value] of entries) {
|
|
367
|
+
if (!justCreated || value !== "" && value !== null && value !== void 0) {
|
|
368
|
+
metaMap.set(key, value);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}, "local");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
instrumentation?.yDocRegistry.register(id, ydoc);
|
|
375
|
+
setDoc(ydoc);
|
|
376
|
+
setLastSavedAt(node.updatedAt);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
} catch (err) {
|
|
380
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
381
|
+
} finally {
|
|
382
|
+
loadDurationRef.current = Math.max(0, nowMs() - loadStartedAt);
|
|
383
|
+
setLoading(false);
|
|
384
|
+
}
|
|
385
|
+
}, [store, isReady, id, schemaId, hasDocument, syncManager, disableSync]);
|
|
386
|
+
const save = useCallback2(async () => {
|
|
387
|
+
if (!store || !id || !docRef.current) return;
|
|
388
|
+
saveTimeoutRef.current = null;
|
|
389
|
+
try {
|
|
390
|
+
const content = Y.encodeStateAsUpdate(docRef.current);
|
|
391
|
+
await store.setDocumentContent(id, content);
|
|
392
|
+
setIsDirty(false);
|
|
393
|
+
setLastSavedAt(Date.now());
|
|
394
|
+
} catch (err) {
|
|
395
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
396
|
+
}
|
|
397
|
+
}, [store, id]);
|
|
398
|
+
const saveRef = useRef2(save);
|
|
399
|
+
saveRef.current = save;
|
|
400
|
+
const scheduleSave = useCallback2(() => {
|
|
401
|
+
setIsDirty(true);
|
|
402
|
+
if (saveTimeoutRef.current) {
|
|
403
|
+
clearTimeout(saveTimeoutRef.current);
|
|
404
|
+
}
|
|
405
|
+
saveTimeoutRef.current = setTimeout(() => {
|
|
406
|
+
saveRef.current();
|
|
407
|
+
}, persistDebounce);
|
|
408
|
+
}, [persistDebounce]);
|
|
409
|
+
const update = useCallback2(
|
|
410
|
+
async (properties) => {
|
|
411
|
+
if (!store || !isReady || !id) return;
|
|
412
|
+
try {
|
|
413
|
+
const node = await store.update(id, {
|
|
414
|
+
properties
|
|
415
|
+
});
|
|
416
|
+
setData(flattenNode(node));
|
|
417
|
+
if (docRef.current) {
|
|
418
|
+
const metaMap = docRef.current.getMap("meta");
|
|
419
|
+
docRef.current.transact(() => {
|
|
420
|
+
metaMap.set("_schemaId", schemaId);
|
|
421
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
422
|
+
metaMap.set(key, value);
|
|
423
|
+
}
|
|
424
|
+
}, "local");
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
[store, isReady, id]
|
|
431
|
+
);
|
|
432
|
+
const remove = useCallback2(async () => {
|
|
433
|
+
if (!store || !isReady || !id) return;
|
|
434
|
+
try {
|
|
435
|
+
await store.delete(id);
|
|
436
|
+
setData(null);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
439
|
+
}
|
|
440
|
+
}, [store, isReady, id]);
|
|
441
|
+
useEffect2(() => {
|
|
442
|
+
load();
|
|
443
|
+
}, [load]);
|
|
444
|
+
useEffect2(() => {
|
|
445
|
+
if (!doc || !id || !hasDocument) return;
|
|
446
|
+
if (doc.guid !== id) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (usingBridgeRef.current && syncManager) {
|
|
450
|
+
let receivedSyncData = false;
|
|
451
|
+
let syncTimeoutId = null;
|
|
452
|
+
const updateHandler2 = (_update, origin) => {
|
|
453
|
+
if (origin === "remote") {
|
|
454
|
+
receivedSyncData = true;
|
|
455
|
+
if (syncTimeoutId) {
|
|
456
|
+
clearTimeout(syncTimeoutId);
|
|
457
|
+
syncTimeoutId = null;
|
|
458
|
+
}
|
|
459
|
+
setSyncError(null);
|
|
460
|
+
}
|
|
461
|
+
scheduleSave();
|
|
462
|
+
};
|
|
463
|
+
doc.on("update", updateHandler2);
|
|
464
|
+
const metaMap = doc.getMap("meta");
|
|
465
|
+
const applyMetaToNodeStore = () => {
|
|
466
|
+
if (metaMap.size === 0 || !storeRef.current || !id) return;
|
|
467
|
+
const remoteProps = {};
|
|
468
|
+
metaMap.forEach((value, key) => {
|
|
469
|
+
if (key.startsWith("_")) return;
|
|
470
|
+
remoteProps[key] = value;
|
|
471
|
+
});
|
|
472
|
+
if (Object.keys(remoteProps).length === 0) return;
|
|
473
|
+
const currentData = dataRef.current;
|
|
474
|
+
if (currentData) {
|
|
475
|
+
let hasChanges = false;
|
|
476
|
+
for (const [key, value] of Object.entries(remoteProps)) {
|
|
477
|
+
if (currentData[key] !== value) {
|
|
478
|
+
hasChanges = true;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!hasChanges) return;
|
|
483
|
+
}
|
|
484
|
+
storeRef.current.update(id, { properties: remoteProps }).then((node) => {
|
|
485
|
+
if (node) setData(flattenNode(node));
|
|
486
|
+
}).catch((err) => {
|
|
487
|
+
console.warn("[useNode] Failed to apply remote meta to NodeStore:", err);
|
|
488
|
+
});
|
|
489
|
+
};
|
|
490
|
+
const metaObserver = (event) => {
|
|
491
|
+
const origin = event.transaction.origin;
|
|
492
|
+
if (origin === null || origin === "local" || origin === "storage" || origin === METABRIDGE_ORIGIN || origin === METABRIDGE_SEED_ORIGIN) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
applyMetaToNodeStore();
|
|
496
|
+
};
|
|
497
|
+
metaMap.observe(metaObserver);
|
|
498
|
+
if (metaMap.size > 0) {
|
|
499
|
+
applyMetaToNodeStore();
|
|
500
|
+
}
|
|
501
|
+
const statusUnsub = syncManager.on("status", (status) => {
|
|
502
|
+
setSyncStatus(
|
|
503
|
+
status === "connected" ? "connected" : status === "connecting" ? "connecting" : "offline"
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
setSyncStatus(
|
|
507
|
+
syncManager.status === "connected" ? "connected" : syncManager.status === "connecting" ? "connecting" : "offline"
|
|
508
|
+
);
|
|
509
|
+
const awareness2 = syncManager.getAwareness(id);
|
|
510
|
+
setAwareness(awareness2);
|
|
511
|
+
if (awareness2 && did) {
|
|
512
|
+
awareness2.setLocalStateField("user", {
|
|
513
|
+
did,
|
|
514
|
+
name: `${did.slice(8, 16)}...`,
|
|
515
|
+
color: generateColor(did)
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
let awarenessCleanup = null;
|
|
519
|
+
if (awareness2) {
|
|
520
|
+
const awarenessHandler = () => {
|
|
521
|
+
const states = awareness2.getStates();
|
|
522
|
+
const nextPresence = [];
|
|
523
|
+
states.forEach((state, clientId) => {
|
|
524
|
+
if (clientId === awareness2.clientID) return;
|
|
525
|
+
const user = state.user;
|
|
526
|
+
if (user?.did) {
|
|
527
|
+
nextPresence.push({
|
|
528
|
+
did: user.did,
|
|
529
|
+
name: user.name,
|
|
530
|
+
color: user.color || generateColor(user.did),
|
|
531
|
+
isStale: false
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
setLivePresence(nextPresence);
|
|
536
|
+
};
|
|
537
|
+
awareness2.on("change", awarenessHandler);
|
|
538
|
+
awarenessHandler();
|
|
539
|
+
awarenessCleanup = () => awareness2.off("change", awarenessHandler);
|
|
540
|
+
}
|
|
541
|
+
const snapshotUnsub = syncManager.onAwarenessSnapshot(id, (users) => {
|
|
542
|
+
const mapped = users.map((user) => ({
|
|
543
|
+
did: user.did,
|
|
544
|
+
name: user.state.user?.name,
|
|
545
|
+
color: user.state.user?.color ?? generateColor(user.did),
|
|
546
|
+
lastSeen: user.lastSeen,
|
|
547
|
+
isStale: user.isStale
|
|
548
|
+
}));
|
|
549
|
+
setSnapshotPresence(mapped);
|
|
550
|
+
});
|
|
551
|
+
if (wasCreated) {
|
|
552
|
+
syncTimeoutId = setTimeout(() => {
|
|
553
|
+
const fragment = doc.getXmlFragment("default");
|
|
554
|
+
const metaMap2 = doc.getMap("meta");
|
|
555
|
+
const hasContent = (fragment?.length ?? 0) > 0 || metaMap2.size > 1;
|
|
556
|
+
if (!receivedSyncData && !hasContent) {
|
|
557
|
+
const errorMsg = "Sync timeout: No content received from peers. The shared document may not exist or peers may be offline.";
|
|
558
|
+
log("Sync timeout - no content received for:", id);
|
|
559
|
+
setSyncStatus("error");
|
|
560
|
+
setSyncError(errorMsg);
|
|
561
|
+
}
|
|
562
|
+
}, 1e4);
|
|
563
|
+
}
|
|
564
|
+
return () => {
|
|
565
|
+
if (syncTimeoutId) {
|
|
566
|
+
clearTimeout(syncTimeoutId);
|
|
567
|
+
}
|
|
568
|
+
doc.off("update", updateHandler2);
|
|
569
|
+
metaMap.unobserve(metaObserver);
|
|
570
|
+
statusUnsub();
|
|
571
|
+
if (awarenessCleanup) awarenessCleanup();
|
|
572
|
+
snapshotUnsub();
|
|
573
|
+
setAwareness(null);
|
|
574
|
+
setSyncStatus("offline");
|
|
575
|
+
setSyncError(null);
|
|
576
|
+
setPeerCount(0);
|
|
577
|
+
setLivePresence([]);
|
|
578
|
+
setSnapshotPresence([]);
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
const updateHandler = (_update, _origin) => {
|
|
582
|
+
scheduleSave();
|
|
583
|
+
};
|
|
584
|
+
doc.on("update", updateHandler);
|
|
585
|
+
if (!disableSync && signalingServers.length > 0) {
|
|
586
|
+
setSyncStatus("connecting");
|
|
587
|
+
const provider = new WebSocketSyncProvider(doc, {
|
|
588
|
+
url: signalingServers[0],
|
|
589
|
+
room: `xnet-doc-${id}`,
|
|
590
|
+
authorDID: syncAuthorDID ?? did ?? void 0,
|
|
591
|
+
signingKey: syncSigningKey ?? void 0,
|
|
592
|
+
replication: syncConfig
|
|
593
|
+
});
|
|
594
|
+
providerRef.current = provider;
|
|
595
|
+
let receivedSyncData = false;
|
|
596
|
+
let syncTimeoutId = null;
|
|
597
|
+
const statusHandler = (event) => {
|
|
598
|
+
const { connected } = event;
|
|
599
|
+
setSyncStatus(connected ? "connected" : "connecting");
|
|
600
|
+
};
|
|
601
|
+
provider.on("status", statusHandler);
|
|
602
|
+
const metaMap = doc.getMap("meta");
|
|
603
|
+
const applyMetaToNodeStore = () => {
|
|
604
|
+
if (metaMap.size === 0) return;
|
|
605
|
+
const props = {};
|
|
606
|
+
metaMap.forEach((value, key) => {
|
|
607
|
+
if (key.startsWith("_")) return;
|
|
608
|
+
props[key] = value;
|
|
609
|
+
});
|
|
610
|
+
if (Object.keys(props).length > 0 && storeRef.current && id) {
|
|
611
|
+
storeRef.current.update(id, { properties: props }).then((node) => {
|
|
612
|
+
if (node) setData(flattenNode(node));
|
|
613
|
+
}).catch((err) => {
|
|
614
|
+
console.warn("[useDocument] Failed to apply remote meta to NodeStore:", err);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
const metaObserver = (event) => {
|
|
619
|
+
const origin = event.transaction.origin;
|
|
620
|
+
if (origin === null || origin === "local" || origin === "storage" || origin === METABRIDGE_ORIGIN || origin === METABRIDGE_SEED_ORIGIN) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
applyMetaToNodeStore();
|
|
624
|
+
};
|
|
625
|
+
metaMap.observe(metaObserver);
|
|
626
|
+
const syncedHandler = (event) => {
|
|
627
|
+
const { synced } = event;
|
|
628
|
+
if (synced) {
|
|
629
|
+
receivedSyncData = true;
|
|
630
|
+
if (syncTimeoutId) {
|
|
631
|
+
clearTimeout(syncTimeoutId);
|
|
632
|
+
syncTimeoutId = null;
|
|
633
|
+
}
|
|
634
|
+
setSyncError(null);
|
|
635
|
+
applyMetaToNodeStore();
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
provider.on("synced", syncedHandler);
|
|
639
|
+
if (wasCreated) {
|
|
640
|
+
syncTimeoutId = setTimeout(() => {
|
|
641
|
+
const fragment = doc.getXmlFragment("default");
|
|
642
|
+
const metaMap2 = doc.getMap("meta");
|
|
643
|
+
const hasContent = (fragment?.length ?? 0) > 0 || metaMap2.size > 1;
|
|
644
|
+
if (!receivedSyncData && !hasContent) {
|
|
645
|
+
const errorMsg = "Sync timeout: No content received from peers. The shared document may not exist or peers may be offline.";
|
|
646
|
+
log("Sync timeout - no content received for:", id);
|
|
647
|
+
setSyncStatus("error");
|
|
648
|
+
setSyncError(errorMsg);
|
|
649
|
+
}
|
|
650
|
+
}, 1e4);
|
|
651
|
+
}
|
|
652
|
+
const peersHandler = (event) => {
|
|
653
|
+
const { count } = event;
|
|
654
|
+
setPeerCount(count);
|
|
655
|
+
};
|
|
656
|
+
provider.on("peers", peersHandler);
|
|
657
|
+
const { awareness: providerAwareness } = provider;
|
|
658
|
+
setAwareness(providerAwareness);
|
|
659
|
+
if (did) {
|
|
660
|
+
providerAwareness.setLocalStateField("user", {
|
|
661
|
+
did,
|
|
662
|
+
name: `${did.slice(8, 16)}...`,
|
|
663
|
+
color: generateColor(did)
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
const awarenessHandler = () => {
|
|
667
|
+
const states = providerAwareness.getStates();
|
|
668
|
+
const nextPresence = [];
|
|
669
|
+
states.forEach((state, clientId) => {
|
|
670
|
+
if (clientId === providerAwareness.clientID) return;
|
|
671
|
+
const user = state.user;
|
|
672
|
+
if (user?.did) {
|
|
673
|
+
nextPresence.push({
|
|
674
|
+
did: user.did,
|
|
675
|
+
name: user.name,
|
|
676
|
+
color: user.color || generateColor(user.did),
|
|
677
|
+
isStale: false
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
setLivePresence(nextPresence);
|
|
682
|
+
};
|
|
683
|
+
providerAwareness.on("change", awarenessHandler);
|
|
684
|
+
const snapshotHandler = (users) => {
|
|
685
|
+
if (!Array.isArray(users)) return;
|
|
686
|
+
const mapped = users.filter(
|
|
687
|
+
(user) => Boolean(
|
|
688
|
+
user && typeof user === "object" && typeof user.did === "string"
|
|
689
|
+
)
|
|
690
|
+
).map(
|
|
691
|
+
(user) => ({
|
|
692
|
+
did: user.did,
|
|
693
|
+
name: user.state.user?.name,
|
|
694
|
+
color: user.state.user?.color ?? generateColor(user.did),
|
|
695
|
+
lastSeen: user.lastSeen,
|
|
696
|
+
isStale: user.isStale
|
|
697
|
+
})
|
|
698
|
+
);
|
|
699
|
+
setSnapshotPresence(mapped);
|
|
700
|
+
};
|
|
701
|
+
provider.on("awareness-snapshot", snapshotHandler);
|
|
702
|
+
return () => {
|
|
703
|
+
if (syncTimeoutId) {
|
|
704
|
+
clearTimeout(syncTimeoutId);
|
|
705
|
+
}
|
|
706
|
+
doc.off("update", updateHandler);
|
|
707
|
+
metaMap.unobserve(metaObserver);
|
|
708
|
+
providerAwareness.off("change", awarenessHandler);
|
|
709
|
+
provider.off("awareness-snapshot", snapshotHandler);
|
|
710
|
+
provider.off("synced", syncedHandler);
|
|
711
|
+
provider.off("status", statusHandler);
|
|
712
|
+
provider.off("peers", peersHandler);
|
|
713
|
+
provider.destroy();
|
|
714
|
+
providerRef.current = null;
|
|
715
|
+
setAwareness(null);
|
|
716
|
+
setSyncStatus("offline");
|
|
717
|
+
setSyncError(null);
|
|
718
|
+
setPeerCount(0);
|
|
719
|
+
setLivePresence([]);
|
|
720
|
+
setSnapshotPresence([]);
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
return () => {
|
|
724
|
+
doc.off("update", updateHandler);
|
|
725
|
+
};
|
|
726
|
+
}, [
|
|
727
|
+
doc,
|
|
728
|
+
id,
|
|
729
|
+
hasDocument,
|
|
730
|
+
disableSync,
|
|
731
|
+
signalingServers,
|
|
732
|
+
scheduleSave,
|
|
733
|
+
did,
|
|
734
|
+
syncManager,
|
|
735
|
+
wasCreated
|
|
736
|
+
]);
|
|
737
|
+
useEffect2(() => {
|
|
738
|
+
return () => {
|
|
739
|
+
if (saveTimeoutRef.current) {
|
|
740
|
+
clearTimeout(saveTimeoutRef.current);
|
|
741
|
+
saveTimeoutRef.current = null;
|
|
742
|
+
}
|
|
743
|
+
if (docRef.current && store && id) {
|
|
744
|
+
const content = Y.encodeStateAsUpdate(docRef.current);
|
|
745
|
+
const flushPromise = store.setDocumentContent(id, content).catch(() => {
|
|
746
|
+
}).finally(() => {
|
|
747
|
+
pendingFlushes.delete(id);
|
|
748
|
+
});
|
|
749
|
+
pendingFlushes.set(id, flushPromise);
|
|
750
|
+
}
|
|
751
|
+
if (usingBridgeRef.current && id) {
|
|
752
|
+
if (bridge?.releaseDoc) {
|
|
753
|
+
bridge.releaseDoc(id);
|
|
754
|
+
} else if (syncManager) {
|
|
755
|
+
syncManager.release(id);
|
|
756
|
+
}
|
|
757
|
+
usingBridgeRef.current = false;
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
}, [store, id, syncManager, bridge]);
|
|
761
|
+
useEffect2(() => {
|
|
762
|
+
if (!store || !id || !hasDocument) return;
|
|
763
|
+
const handleBeforeUnload = () => {
|
|
764
|
+
if (saveTimeoutRef.current) {
|
|
765
|
+
clearTimeout(saveTimeoutRef.current);
|
|
766
|
+
saveTimeoutRef.current = null;
|
|
767
|
+
}
|
|
768
|
+
if (docRef.current) {
|
|
769
|
+
try {
|
|
770
|
+
const content = Y.encodeStateAsUpdate(docRef.current);
|
|
771
|
+
store.setDocumentContent(id, content).catch(() => {
|
|
772
|
+
});
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
778
|
+
return () => {
|
|
779
|
+
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
780
|
+
};
|
|
781
|
+
}, [store, id, hasDocument]);
|
|
782
|
+
useEffect2(() => {
|
|
783
|
+
if (!store || !id) return;
|
|
784
|
+
const unsubscribe = store.subscribeToNode(id, (event) => {
|
|
785
|
+
if (event.node && event.node.schemaId === schemaId) {
|
|
786
|
+
setData(flattenNode(event.node));
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
return unsubscribe;
|
|
790
|
+
}, [store, id, schemaId]);
|
|
791
|
+
useEffect2(() => {
|
|
792
|
+
if (!instrumentation?.queryTracker || !id || loading) return;
|
|
793
|
+
instrumentation.queryTracker.recordUpdate(
|
|
794
|
+
queryIdRef.current,
|
|
795
|
+
data ? 1 : 0,
|
|
796
|
+
loadDurationRef.current,
|
|
797
|
+
{ source: "local" }
|
|
798
|
+
);
|
|
799
|
+
loadDurationRef.current = 0;
|
|
800
|
+
}, [data, instrumentation, id, loading]);
|
|
801
|
+
return {
|
|
802
|
+
// Data
|
|
803
|
+
data,
|
|
804
|
+
doc,
|
|
805
|
+
// Mutations
|
|
806
|
+
update,
|
|
807
|
+
remove,
|
|
808
|
+
// State
|
|
809
|
+
loading,
|
|
810
|
+
error,
|
|
811
|
+
isDirty,
|
|
812
|
+
lastSavedAt,
|
|
813
|
+
wasCreated,
|
|
814
|
+
// Sync
|
|
815
|
+
syncStatus,
|
|
816
|
+
syncError,
|
|
817
|
+
peerCount,
|
|
818
|
+
// Presence
|
|
819
|
+
presence,
|
|
820
|
+
awareness,
|
|
821
|
+
// Actions
|
|
822
|
+
save,
|
|
823
|
+
reload: load
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// src/hooks/useIdentity.ts
|
|
828
|
+
function useIdentity() {
|
|
829
|
+
const { identity, authorDID } = useXNet();
|
|
830
|
+
return {
|
|
831
|
+
identity: identity ?? null,
|
|
832
|
+
isAuthenticated: !!identity || !!authorDID,
|
|
833
|
+
did: identity?.did ?? authorDID ?? null
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/components/ErrorBoundary.tsx
|
|
838
|
+
import { Component } from "react";
|
|
839
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
840
|
+
var ErrorBoundary = class extends Component {
|
|
841
|
+
state = { hasError: false, error: null };
|
|
842
|
+
static getDerivedStateFromError(error) {
|
|
843
|
+
return { hasError: true, error };
|
|
844
|
+
}
|
|
845
|
+
componentDidCatch(error, errorInfo) {
|
|
846
|
+
console.error("[ErrorBoundary]", error, errorInfo);
|
|
847
|
+
this.props.onError?.(error, errorInfo);
|
|
848
|
+
}
|
|
849
|
+
handleReset = () => {
|
|
850
|
+
this.setState({ hasError: false, error: null });
|
|
851
|
+
};
|
|
852
|
+
componentDidUpdate(prevProps) {
|
|
853
|
+
if (this.state.hasError && this.props.resetKey !== void 0 && prevProps.resetKey !== this.props.resetKey) {
|
|
854
|
+
this.handleReset();
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
render() {
|
|
858
|
+
if (!this.state.hasError) {
|
|
859
|
+
return this.props.children;
|
|
860
|
+
}
|
|
861
|
+
if (this.props.fallback) {
|
|
862
|
+
if (typeof this.props.fallback === "function") {
|
|
863
|
+
return this.props.fallback({
|
|
864
|
+
error: this.state.error ?? new Error("Unknown error"),
|
|
865
|
+
reset: this.handleReset
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
return this.props.fallback;
|
|
869
|
+
}
|
|
870
|
+
return /* @__PURE__ */ jsxs(
|
|
871
|
+
"div",
|
|
872
|
+
{
|
|
873
|
+
role: "alert",
|
|
874
|
+
style: {
|
|
875
|
+
padding: "2rem",
|
|
876
|
+
textAlign: "center",
|
|
877
|
+
maxWidth: "480px",
|
|
878
|
+
margin: "4rem auto"
|
|
879
|
+
},
|
|
880
|
+
children: [
|
|
881
|
+
/* @__PURE__ */ jsx("h2", { style: { fontSize: "1.25rem", fontWeight: 600, marginBottom: "0.5rem" }, children: "Something went wrong" }),
|
|
882
|
+
/* @__PURE__ */ jsx("p", { style: { color: "#666", marginBottom: "1rem", fontSize: "0.875rem" }, children: this.state.error?.message ?? "An unexpected error occurred." }),
|
|
883
|
+
/* @__PURE__ */ jsx(
|
|
884
|
+
"button",
|
|
885
|
+
{
|
|
886
|
+
onClick: this.handleReset,
|
|
887
|
+
style: {
|
|
888
|
+
padding: "0.5rem 1rem",
|
|
889
|
+
borderRadius: "6px",
|
|
890
|
+
border: "1px solid #ccc",
|
|
891
|
+
background: "#fff",
|
|
892
|
+
cursor: "pointer",
|
|
893
|
+
fontSize: "0.875rem"
|
|
894
|
+
},
|
|
895
|
+
children: "Try again"
|
|
896
|
+
}
|
|
897
|
+
)
|
|
898
|
+
]
|
|
899
|
+
}
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
// src/components/OfflineIndicator.tsx
|
|
905
|
+
import { useState as useState3, useEffect as useEffect3 } from "react";
|
|
906
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
907
|
+
function useIsOffline() {
|
|
908
|
+
const [offline, setOffline] = useState3(
|
|
909
|
+
typeof navigator !== "undefined" ? !navigator.onLine : false
|
|
910
|
+
);
|
|
911
|
+
useEffect3(() => {
|
|
912
|
+
const goOffline = () => setOffline(true);
|
|
913
|
+
const goOnline = () => setOffline(false);
|
|
914
|
+
window.addEventListener("offline", goOffline);
|
|
915
|
+
window.addEventListener("online", goOnline);
|
|
916
|
+
return () => {
|
|
917
|
+
window.removeEventListener("offline", goOffline);
|
|
918
|
+
window.removeEventListener("online", goOnline);
|
|
919
|
+
};
|
|
920
|
+
}, []);
|
|
921
|
+
return offline;
|
|
922
|
+
}
|
|
923
|
+
function OfflineIndicator({
|
|
924
|
+
message = "You are offline. Changes will sync when reconnected.",
|
|
925
|
+
className,
|
|
926
|
+
position = "bottom"
|
|
927
|
+
}) {
|
|
928
|
+
const offline = useIsOffline();
|
|
929
|
+
if (!offline) return null;
|
|
930
|
+
const positionStyle = position === "top" ? { top: 0 } : { bottom: 0 };
|
|
931
|
+
return /* @__PURE__ */ jsxs2(
|
|
932
|
+
"div",
|
|
933
|
+
{
|
|
934
|
+
role: "status",
|
|
935
|
+
"aria-live": "polite",
|
|
936
|
+
className,
|
|
937
|
+
style: {
|
|
938
|
+
position: "fixed",
|
|
939
|
+
left: 0,
|
|
940
|
+
right: 0,
|
|
941
|
+
zIndex: 9999,
|
|
942
|
+
display: "flex",
|
|
943
|
+
alignItems: "center",
|
|
944
|
+
justifyContent: "center",
|
|
945
|
+
gap: "0.5rem",
|
|
946
|
+
padding: "0.5rem 1rem",
|
|
947
|
+
background: "#fbbf24",
|
|
948
|
+
color: "#78350f",
|
|
949
|
+
fontSize: "0.875rem",
|
|
950
|
+
fontWeight: 500,
|
|
951
|
+
...positionStyle
|
|
952
|
+
},
|
|
953
|
+
children: [
|
|
954
|
+
/* @__PURE__ */ jsxs2(
|
|
955
|
+
"svg",
|
|
956
|
+
{
|
|
957
|
+
width: "16",
|
|
958
|
+
height: "16",
|
|
959
|
+
viewBox: "0 0 24 24",
|
|
960
|
+
fill: "none",
|
|
961
|
+
stroke: "currentColor",
|
|
962
|
+
strokeWidth: "2",
|
|
963
|
+
strokeLinecap: "round",
|
|
964
|
+
strokeLinejoin: "round",
|
|
965
|
+
children: [
|
|
966
|
+
/* @__PURE__ */ jsx2("line", { x1: "1", y1: "1", x2: "23", y2: "23" }),
|
|
967
|
+
/* @__PURE__ */ jsx2("path", { d: "M16.72 11.06A10.94 10.94 0 0 1 19 12.55" }),
|
|
968
|
+
/* @__PURE__ */ jsx2("path", { d: "M5 12.55a10.94 10.94 0 0 1 5.17-2.39" }),
|
|
969
|
+
/* @__PURE__ */ jsx2("path", { d: "M10.71 5.05A16 16 0 0 1 22.56 9" }),
|
|
970
|
+
/* @__PURE__ */ jsx2("path", { d: "M1.42 9a15.91 15.91 0 0 1 4.7-2.88" }),
|
|
971
|
+
/* @__PURE__ */ jsx2("path", { d: "M8.53 16.11a6 6 0 0 1 6.95 0" }),
|
|
972
|
+
/* @__PURE__ */ jsx2("line", { x1: "12", y1: "20", x2: "12.01", y2: "20" })
|
|
973
|
+
]
|
|
974
|
+
}
|
|
975
|
+
),
|
|
976
|
+
/* @__PURE__ */ jsx2("span", { children: message })
|
|
977
|
+
]
|
|
978
|
+
}
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
export {
|
|
983
|
+
useInfiniteQuery,
|
|
984
|
+
useNode,
|
|
985
|
+
useIdentity,
|
|
986
|
+
ErrorBoundary,
|
|
987
|
+
useIsOffline,
|
|
988
|
+
OfflineIndicator
|
|
989
|
+
};
|