@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,760 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useInstrumentation
|
|
3
|
+
} from "./chunk-JCOFKBOB.js";
|
|
4
|
+
import {
|
|
5
|
+
TRACE_STAGES,
|
|
6
|
+
XNetContext,
|
|
7
|
+
useDataBridge,
|
|
8
|
+
useTelemetryReporter,
|
|
9
|
+
useTracingReporter
|
|
10
|
+
} from "./chunk-IHTMVTTE.js";
|
|
11
|
+
|
|
12
|
+
// src/utils/flattenNode.ts
|
|
13
|
+
function flattenNode(node, options) {
|
|
14
|
+
const { properties, timestamps, deletedAt, documentContent, _unknown, _schemaVersion, ...base } = node;
|
|
15
|
+
const migrationInfo = options?.migrationInfo ?? node._migrationInfo;
|
|
16
|
+
return {
|
|
17
|
+
...base,
|
|
18
|
+
...properties,
|
|
19
|
+
// Include version compatibility fields if present
|
|
20
|
+
...options?.unknownSchema && { _unknownSchema: true },
|
|
21
|
+
..._unknown && Object.keys(_unknown).length > 0 && { _unknown },
|
|
22
|
+
..._schemaVersion && { _schemaVersion },
|
|
23
|
+
// Include migration fields if present
|
|
24
|
+
...migrationInfo && {
|
|
25
|
+
_migratedFrom: migrationInfo.from,
|
|
26
|
+
_migrationInfo: migrationInfo
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function flattenNodes(nodes, options) {
|
|
31
|
+
return nodes.map((node) => flattenNode(node, options));
|
|
32
|
+
}
|
|
33
|
+
function flattenUnknownSchemaNode(node) {
|
|
34
|
+
return flattenNode(node, { unknownSchema: true });
|
|
35
|
+
}
|
|
36
|
+
function flattenNodesWithSchemaCheck(nodes, isSchemaKnown) {
|
|
37
|
+
return nodes.map((node) => flattenNode(node, { unknownSchema: !isSchemaKnown(node.schemaId) }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/hooks/useQuery.ts
|
|
41
|
+
import {
|
|
42
|
+
createQueryDescriptor,
|
|
43
|
+
queryDescriptorToOptions,
|
|
44
|
+
serializeQueryDescriptor
|
|
45
|
+
} from "@xnetjs/data-bridge";
|
|
46
|
+
import { useSyncExternalStore, useMemo, useCallback, useEffect, useRef } from "react";
|
|
47
|
+
|
|
48
|
+
// src/utils/queryResultMeta.ts
|
|
49
|
+
var EMPTY_PAGE_INFO = {
|
|
50
|
+
totalCount: null,
|
|
51
|
+
countMode: "none",
|
|
52
|
+
hasMore: false,
|
|
53
|
+
hasNextPage: false,
|
|
54
|
+
hasPreviousPage: false,
|
|
55
|
+
loadedCount: 0
|
|
56
|
+
};
|
|
57
|
+
function computeFallbackPageInfo(input) {
|
|
58
|
+
if (input.metadata?.pageInfo) {
|
|
59
|
+
return input.metadata.pageInfo;
|
|
60
|
+
}
|
|
61
|
+
if (input.loading || input.loadedCount === null) {
|
|
62
|
+
return EMPTY_PAGE_INFO;
|
|
63
|
+
}
|
|
64
|
+
const { loadedCount, offset, limit } = input;
|
|
65
|
+
const totalCount = limit === void 0 && offset === 0 ? loadedCount : null;
|
|
66
|
+
const countMode = totalCount === null ? "none" : "exact";
|
|
67
|
+
const hasMore = limit !== void 0 ? totalCount === null ? loadedCount >= limit : offset + loadedCount < totalCount : false;
|
|
68
|
+
return {
|
|
69
|
+
totalCount,
|
|
70
|
+
countMode,
|
|
71
|
+
hasMore,
|
|
72
|
+
hasNextPage: hasMore,
|
|
73
|
+
hasPreviousPage: offset > 0,
|
|
74
|
+
loadedCount
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function summarizePlan(metadata) {
|
|
78
|
+
const plan = metadata?.plan;
|
|
79
|
+
if (!plan) return null;
|
|
80
|
+
return {
|
|
81
|
+
strategy: plan.strategy,
|
|
82
|
+
candidateNodeCount: plan.candidateNodeCount,
|
|
83
|
+
hydratedNodeCount: plan.hydratedNodeCount,
|
|
84
|
+
returnedNodeCount: plan.returnedNodeCount,
|
|
85
|
+
durationMs: plan.durationMs,
|
|
86
|
+
descriptorHash: plan.descriptorHash,
|
|
87
|
+
candidateAccelerators: plan.candidateAccelerators,
|
|
88
|
+
materializedViewId: plan.materializedViewId,
|
|
89
|
+
materializedCacheHit: plan.materializedCacheHit,
|
|
90
|
+
materializedRefreshReason: plan.materializedRefreshReason
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/hooks/useQuery.ts
|
|
95
|
+
var EMPTY_QUERY_FILTER = Object.freeze({});
|
|
96
|
+
var DISABLED_QUERY_SNAPSHOT = [];
|
|
97
|
+
var nextQueryInstanceId = 0;
|
|
98
|
+
var flatNodeCache = /* @__PURE__ */ new WeakMap();
|
|
99
|
+
function flattenNodeCached(node) {
|
|
100
|
+
const cached = flatNodeCache.get(node);
|
|
101
|
+
if (cached) return cached;
|
|
102
|
+
const flat = flattenNode(node);
|
|
103
|
+
flatNodeCache.set(node, flat);
|
|
104
|
+
return flat;
|
|
105
|
+
}
|
|
106
|
+
var SYSTEM_ORDER_FIELDS = /* @__PURE__ */ new Set(["createdAt", "updatedAt"]);
|
|
107
|
+
var warnedUnboundedSorts = /* @__PURE__ */ new Set();
|
|
108
|
+
function isProductionEnv() {
|
|
109
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
110
|
+
}
|
|
111
|
+
function isUnboundedListQuery(descriptor) {
|
|
112
|
+
if (descriptor.nodeId) return false;
|
|
113
|
+
if (descriptor.limit !== void 0) return false;
|
|
114
|
+
if (descriptor.after !== void 0) return false;
|
|
115
|
+
return !descriptor.offset;
|
|
116
|
+
}
|
|
117
|
+
function propertyOrderKeys(descriptor) {
|
|
118
|
+
return Object.keys(descriptor.orderBy ?? {}).filter((key) => !SYSTEM_ORDER_FIELDS.has(key));
|
|
119
|
+
}
|
|
120
|
+
function unboundedPropertySortKey(schemaId, descriptor) {
|
|
121
|
+
if (isProductionEnv()) return null;
|
|
122
|
+
if (!isUnboundedListQuery(descriptor)) return null;
|
|
123
|
+
const propertyKeys = propertyOrderKeys(descriptor);
|
|
124
|
+
return propertyKeys.length > 0 ? `${schemaId}:${propertyKeys.join(",")}` : null;
|
|
125
|
+
}
|
|
126
|
+
function warnIfUnboundedPropertySort(schemaId, descriptor) {
|
|
127
|
+
const warnKey = unboundedPropertySortKey(schemaId, descriptor);
|
|
128
|
+
if (warnKey === null) return;
|
|
129
|
+
if (warnedUnboundedSorts.has(warnKey)) return;
|
|
130
|
+
warnedUnboundedSorts.add(warnKey);
|
|
131
|
+
console.warn(
|
|
132
|
+
`[useQuery] Unbounded query on "${schemaId}" ordered by property ${JSON.stringify(propertyOrderKeys(descriptor))} cannot use an index \u2014 it full-scans and JS-sorts the whole schema. Add a \`limit\` and order by a system field (createdAt/updatedAt), then sort in JS if needed (exploration 0184).`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
function getFallbackPageInfo(input) {
|
|
136
|
+
return computeFallbackPageInfo({
|
|
137
|
+
metadata: input.metadata,
|
|
138
|
+
loading: input.loading,
|
|
139
|
+
loadedCount: !input.isSingleQuery && Array.isArray(input.data) ? input.data.length : null,
|
|
140
|
+
offset: input.descriptor.offset ?? 0,
|
|
141
|
+
limit: input.descriptor.limit
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function flattenListSnapshot(rawData) {
|
|
145
|
+
const data = rawData.map((node) => flattenNodeCached(node));
|
|
146
|
+
return { data, migrationWarnings: collectMigrationWarnings(data) };
|
|
147
|
+
}
|
|
148
|
+
function deriveMetadataSurfaces(metadata) {
|
|
149
|
+
return {
|
|
150
|
+
source: metadata?.source ?? "local",
|
|
151
|
+
materialized: metadata?.materialized ?? null,
|
|
152
|
+
completeness: metadata?.completeness ?? null,
|
|
153
|
+
staleness: metadata?.staleness ?? null,
|
|
154
|
+
verification: metadata?.verification ?? null,
|
|
155
|
+
stream: metadata?.stream ?? null
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function deriveTrackedFilter(descriptor) {
|
|
159
|
+
const next = {
|
|
160
|
+
...descriptor.where ? { where: descriptor.where } : {},
|
|
161
|
+
...descriptor.spatial ? { spatial: descriptor.spatial } : {},
|
|
162
|
+
...descriptor.search ? { search: descriptor.search } : {},
|
|
163
|
+
...descriptor.materializedView ? { materializedView: descriptor.materializedView } : {}
|
|
164
|
+
};
|
|
165
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
166
|
+
}
|
|
167
|
+
function deriveTrackedMode(isSingleQuery, descriptor) {
|
|
168
|
+
if (isSingleQuery) return "single";
|
|
169
|
+
return descriptor.where || descriptor.spatial || descriptor.search ? "filtered" : "list";
|
|
170
|
+
}
|
|
171
|
+
function collectMigrationWarnings(flattened) {
|
|
172
|
+
const warnings = [];
|
|
173
|
+
for (const flat of flattened) {
|
|
174
|
+
if (flat._migrationInfo && !flat._migrationInfo.lossless) {
|
|
175
|
+
warnings.push({
|
|
176
|
+
nodeId: flat.id,
|
|
177
|
+
from: flat._migrationInfo.from,
|
|
178
|
+
to: flat._migrationInfo.to,
|
|
179
|
+
warnings: flat._migrationInfo.warnings
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return warnings;
|
|
184
|
+
}
|
|
185
|
+
function useQuery(schema, idOrFilter) {
|
|
186
|
+
const bridge = useDataBridge();
|
|
187
|
+
const instrumentation = useInstrumentation();
|
|
188
|
+
const telemetry = useTelemetryReporter();
|
|
189
|
+
const tracing = useTracingReporter();
|
|
190
|
+
const schemaId = schema._schemaId;
|
|
191
|
+
const queryStartRef = useRef(null);
|
|
192
|
+
if (queryStartRef.current === null) queryStartRef.current = Date.now();
|
|
193
|
+
const isSingleQuery = typeof idOrFilter === "string";
|
|
194
|
+
const filter = typeof idOrFilter === "object" ? idOrFilter : EMPTY_QUERY_FILTER;
|
|
195
|
+
const nodeId = isSingleQuery ? idOrFilter : null;
|
|
196
|
+
const enabled = filter.enabled ?? true;
|
|
197
|
+
const descriptorRef = useRef(null);
|
|
198
|
+
if (!descriptorRef.current || descriptorRef.current.input !== idOrFilter || descriptorRef.current.schemaId !== schemaId) {
|
|
199
|
+
const options = isSingleQuery && nodeId ? { nodeId } : filter;
|
|
200
|
+
const candidate = createQueryDescriptor(schemaId, options);
|
|
201
|
+
const candidateKey = serializeQueryDescriptor(candidate);
|
|
202
|
+
const descriptor2 = descriptorRef.current && descriptorRef.current.key === candidateKey ? descriptorRef.current.descriptor : candidate;
|
|
203
|
+
descriptorRef.current = { input: idOrFilter, schemaId, key: candidateKey, descriptor: descriptor2 };
|
|
204
|
+
warnIfUnboundedPropertySort(schemaId, descriptor2);
|
|
205
|
+
}
|
|
206
|
+
const descriptor = descriptorRef.current.descriptor;
|
|
207
|
+
const queryKey = descriptorRef.current.key;
|
|
208
|
+
const subscription = useMemo(() => {
|
|
209
|
+
if (!enabled) {
|
|
210
|
+
return {
|
|
211
|
+
getSnapshot: () => DISABLED_QUERY_SNAPSHOT,
|
|
212
|
+
getMetadata: () => null,
|
|
213
|
+
subscribe: () => () => {
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (!bridge) {
|
|
218
|
+
return {
|
|
219
|
+
getSnapshot: () => null,
|
|
220
|
+
getMetadata: () => null,
|
|
221
|
+
subscribe: () => () => {
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return bridge.query(schema, queryDescriptorToOptions(descriptor));
|
|
226
|
+
}, [bridge, enabled, schema, queryKey]);
|
|
227
|
+
const reload = useCallback(() => {
|
|
228
|
+
if (!enabled || !bridge?.reloadQuery) return;
|
|
229
|
+
void bridge.reloadQuery(descriptor);
|
|
230
|
+
}, [bridge, descriptor, enabled]);
|
|
231
|
+
const combinedSnapshotRef = useRef(null);
|
|
232
|
+
const getCombinedSnapshot = useCallback(() => {
|
|
233
|
+
const data2 = subscription.getSnapshot();
|
|
234
|
+
const metadata2 = subscription.getMetadata?.() ?? null;
|
|
235
|
+
const previous = combinedSnapshotRef.current;
|
|
236
|
+
if (previous && previous.data === data2 && previous.metadata === metadata2) {
|
|
237
|
+
return previous;
|
|
238
|
+
}
|
|
239
|
+
const next = { data: data2, metadata: metadata2 };
|
|
240
|
+
combinedSnapshotRef.current = next;
|
|
241
|
+
return next;
|
|
242
|
+
}, [subscription]);
|
|
243
|
+
const combinedSnapshot = useSyncExternalStore(
|
|
244
|
+
subscription.subscribe,
|
|
245
|
+
getCombinedSnapshot,
|
|
246
|
+
getCombinedSnapshot
|
|
247
|
+
// Server snapshot (same as client for now)
|
|
248
|
+
);
|
|
249
|
+
const rawData = combinedSnapshot.data;
|
|
250
|
+
const metadata = combinedSnapshot.metadata;
|
|
251
|
+
const previousListRef = useRef(null);
|
|
252
|
+
const { data, migrationWarnings } = useMemo(() => {
|
|
253
|
+
if (rawData === null) {
|
|
254
|
+
return {
|
|
255
|
+
data: isSingleQuery ? null : [],
|
|
256
|
+
migrationWarnings: []
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
if (isSingleQuery) {
|
|
260
|
+
const node = rawData[0] ?? null;
|
|
261
|
+
const flat = node ? flattenNodeCached(node) : null;
|
|
262
|
+
return {
|
|
263
|
+
data: flat,
|
|
264
|
+
migrationWarnings: flat ? collectMigrationWarnings([flat]) : []
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const next = flattenListSnapshot(rawData);
|
|
268
|
+
const previous = previousListRef.current;
|
|
269
|
+
if (previous && previous.data.length === next.data.length && next.data.every((flat, index) => flat === previous.data[index])) {
|
|
270
|
+
return previous;
|
|
271
|
+
}
|
|
272
|
+
previousListRef.current = next;
|
|
273
|
+
return next;
|
|
274
|
+
}, [rawData, isSingleQuery]);
|
|
275
|
+
const loading = rawData === null;
|
|
276
|
+
const pageInfo = useMemo(
|
|
277
|
+
() => getFallbackPageInfo({ metadata, loading, isSingleQuery, data, descriptor }),
|
|
278
|
+
[metadata, loading, isSingleQuery, data, descriptor]
|
|
279
|
+
);
|
|
280
|
+
const error = useMemo(
|
|
281
|
+
() => metadata?.error ? new Error(metadata.error) : null,
|
|
282
|
+
[metadata?.error]
|
|
283
|
+
);
|
|
284
|
+
const status = error ? "error" : loading ? "loading" : "success";
|
|
285
|
+
const plan = useMemo(() => summarizePlan(metadata), [metadata]);
|
|
286
|
+
const { source, materialized, completeness, staleness, verification, stream } = deriveMetadataSurfaces(metadata);
|
|
287
|
+
const trackedFilter = useMemo(
|
|
288
|
+
() => deriveTrackedFilter(descriptor),
|
|
289
|
+
// queryKey is the canonical descriptor identity.
|
|
290
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
291
|
+
[queryKey]
|
|
292
|
+
);
|
|
293
|
+
const trackedMode = useMemo(
|
|
294
|
+
() => deriveTrackedMode(isSingleQuery, descriptor),
|
|
295
|
+
// queryKey is the canonical descriptor identity.
|
|
296
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
297
|
+
[isSingleQuery, queryKey]
|
|
298
|
+
);
|
|
299
|
+
const queryIdRef = useRef(null);
|
|
300
|
+
if (queryIdRef.current === null) {
|
|
301
|
+
queryIdRef.current = `useQuery-${schemaId}-${nodeId || "list"}-${(nextQueryInstanceId++).toString(36)}`;
|
|
302
|
+
}
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
if (!instrumentation?.queryTracker) return;
|
|
305
|
+
const queryId = queryIdRef.current;
|
|
306
|
+
instrumentation.queryTracker.register(queryId, {
|
|
307
|
+
type: "useQuery",
|
|
308
|
+
schemaId,
|
|
309
|
+
mode: trackedMode,
|
|
310
|
+
filter: trackedFilter,
|
|
311
|
+
descriptorKey: queryKey,
|
|
312
|
+
nodeId: nodeId || void 0
|
|
313
|
+
});
|
|
314
|
+
return () => {
|
|
315
|
+
instrumentation.queryTracker.unregister(queryId);
|
|
316
|
+
};
|
|
317
|
+
}, [instrumentation, schemaId, nodeId, trackedMode, trackedFilter, queryKey]);
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
if (!instrumentation?.queryTracker || loading) return;
|
|
320
|
+
const count = isSingleQuery ? data ? 1 : 0 : Array.isArray(data) ? data.length : 0;
|
|
321
|
+
instrumentation.queryTracker.recordUpdate(queryIdRef.current, count, 0, {
|
|
322
|
+
source,
|
|
323
|
+
plan,
|
|
324
|
+
materialized,
|
|
325
|
+
completeness,
|
|
326
|
+
staleness,
|
|
327
|
+
verification,
|
|
328
|
+
stream
|
|
329
|
+
});
|
|
330
|
+
}, [
|
|
331
|
+
data,
|
|
332
|
+
instrumentation,
|
|
333
|
+
isSingleQuery,
|
|
334
|
+
loading,
|
|
335
|
+
source,
|
|
336
|
+
plan,
|
|
337
|
+
materialized,
|
|
338
|
+
completeness,
|
|
339
|
+
staleness,
|
|
340
|
+
verification,
|
|
341
|
+
stream
|
|
342
|
+
]);
|
|
343
|
+
const lastRecordedStreamEventAtRef = useRef(0);
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
if (!instrumentation?.queryTracker.recordStreamEvent || !stream) return;
|
|
346
|
+
if (lastRecordedStreamEventAtRef.current === stream.lastEventAt) return;
|
|
347
|
+
lastRecordedStreamEventAtRef.current = stream.lastEventAt;
|
|
348
|
+
const count = isSingleQuery ? data ? 1 : 0 : Array.isArray(data) ? data.length : 0;
|
|
349
|
+
instrumentation.queryTracker.recordStreamEvent(queryIdRef.current, stream, count, {
|
|
350
|
+
source
|
|
351
|
+
});
|
|
352
|
+
}, [data, instrumentation, isSingleQuery, source, stream]);
|
|
353
|
+
useEffect(() => {
|
|
354
|
+
if (!telemetry) return;
|
|
355
|
+
telemetry.reportUsage("react.useQuery", 1);
|
|
356
|
+
return () => {
|
|
357
|
+
telemetry.reportUsage("react.useQuery.unmount", 1);
|
|
358
|
+
};
|
|
359
|
+
}, [telemetry]);
|
|
360
|
+
const hasReportedTimingRef = useRef(false);
|
|
361
|
+
const queryTraceRef = useRef(null);
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
queryStartRef.current = Date.now();
|
|
364
|
+
hasReportedTimingRef.current = false;
|
|
365
|
+
queryTraceRef.current?.end();
|
|
366
|
+
queryTraceRef.current = tracing?.startTrace("query", `query:${schemaId}`) ?? null;
|
|
367
|
+
return () => {
|
|
368
|
+
queryTraceRef.current?.end();
|
|
369
|
+
queryTraceRef.current = null;
|
|
370
|
+
};
|
|
371
|
+
}, [queryKey, tracing, schemaId]);
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
if (loading || hasReportedTimingRef.current) return;
|
|
374
|
+
hasReportedTimingRef.current = true;
|
|
375
|
+
const elapsed = Date.now() - (queryStartRef.current ?? Date.now());
|
|
376
|
+
if (telemetry) {
|
|
377
|
+
telemetry.reportPerformance("react.useQuery", elapsed);
|
|
378
|
+
if (elapsed < 5) {
|
|
379
|
+
telemetry.reportUsage("react.useQuery.cache_hit", 1);
|
|
380
|
+
} else {
|
|
381
|
+
telemetry.reportUsage("react.useQuery.cache_miss", 1);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const trace = queryTraceRef.current;
|
|
385
|
+
if (trace) {
|
|
386
|
+
const rows = isSingleQuery ? data ? 1 : 0 : Array.isArray(data) ? data.length : 0;
|
|
387
|
+
trace.addSpan({
|
|
388
|
+
name: TRACE_STAGES.queryCommit,
|
|
389
|
+
startOffsetMs: 0,
|
|
390
|
+
durationMs: elapsed,
|
|
391
|
+
attributes: { returnedRows: rows }
|
|
392
|
+
});
|
|
393
|
+
trace.end();
|
|
394
|
+
queryTraceRef.current = null;
|
|
395
|
+
}
|
|
396
|
+
}, [loading, telemetry, data, isSingleQuery]);
|
|
397
|
+
return {
|
|
398
|
+
data,
|
|
399
|
+
status,
|
|
400
|
+
loading,
|
|
401
|
+
isLoading: loading,
|
|
402
|
+
isFetching: loading,
|
|
403
|
+
isLive: bridge !== null,
|
|
404
|
+
source,
|
|
405
|
+
error,
|
|
406
|
+
reload,
|
|
407
|
+
migrationWarnings,
|
|
408
|
+
pageInfo,
|
|
409
|
+
totalCount: pageInfo.totalCount,
|
|
410
|
+
hasMore: pageInfo.hasMore,
|
|
411
|
+
plan,
|
|
412
|
+
materialized,
|
|
413
|
+
completeness,
|
|
414
|
+
staleness,
|
|
415
|
+
verification,
|
|
416
|
+
stream
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/hooks/useMutate.ts
|
|
421
|
+
import { isTempId } from "@xnetjs/data";
|
|
422
|
+
import { useCallback as useCallback2, useMemo as useMemo2, useRef as useRef2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
423
|
+
function hasTempIdInValue(value) {
|
|
424
|
+
if (isTempId(value)) return true;
|
|
425
|
+
if (Array.isArray(value)) {
|
|
426
|
+
return value.some((item) => hasTempIdInValue(item));
|
|
427
|
+
}
|
|
428
|
+
if (value && typeof value === "object") {
|
|
429
|
+
return Object.values(value).some((item) => hasTempIdInValue(item));
|
|
430
|
+
}
|
|
431
|
+
return false;
|
|
432
|
+
}
|
|
433
|
+
function hasTempIdsInOps(ops) {
|
|
434
|
+
return ops.some((op) => {
|
|
435
|
+
switch (op.type) {
|
|
436
|
+
case "create":
|
|
437
|
+
return Boolean(op.id && isTempId(op.id) || hasTempIdInValue(op.data));
|
|
438
|
+
case "update":
|
|
439
|
+
return isTempId(op.id) || hasTempIdInValue(op.data);
|
|
440
|
+
case "delete":
|
|
441
|
+
case "restore":
|
|
442
|
+
return isTempId(op.id);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
function toTransactionOperations(ops) {
|
|
447
|
+
return ops.map((op) => {
|
|
448
|
+
switch (op.type) {
|
|
449
|
+
case "create":
|
|
450
|
+
return {
|
|
451
|
+
type: "create",
|
|
452
|
+
options: {
|
|
453
|
+
id: op.id,
|
|
454
|
+
schemaId: op.schema._schemaId,
|
|
455
|
+
properties: op.data
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
case "update":
|
|
459
|
+
return {
|
|
460
|
+
type: "update",
|
|
461
|
+
nodeId: op.id,
|
|
462
|
+
options: {
|
|
463
|
+
properties: op.data
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
case "delete":
|
|
467
|
+
return {
|
|
468
|
+
type: "delete",
|
|
469
|
+
nodeId: op.id
|
|
470
|
+
};
|
|
471
|
+
case "restore":
|
|
472
|
+
return {
|
|
473
|
+
type: "restore",
|
|
474
|
+
nodeId: op.id
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function useMutate() {
|
|
480
|
+
const bridge = useDataBridge();
|
|
481
|
+
const telemetry = useTelemetryReporter();
|
|
482
|
+
const tracing = useTracingReporter();
|
|
483
|
+
const pendingRef = useRef2(0);
|
|
484
|
+
const pendingListenersRef = useRef2(null);
|
|
485
|
+
pendingListenersRef.current ??= /* @__PURE__ */ new Set();
|
|
486
|
+
const pendingReadLevelRef = useRef2(0);
|
|
487
|
+
const subscribePending = useCallback2((listener) => {
|
|
488
|
+
const listeners = pendingListenersRef.current;
|
|
489
|
+
listeners.add(listener);
|
|
490
|
+
return () => {
|
|
491
|
+
listeners.delete(listener);
|
|
492
|
+
};
|
|
493
|
+
}, []);
|
|
494
|
+
const getPendingSnapshot = useCallback2(() => {
|
|
495
|
+
if (pendingReadLevelRef.current === 0) return 0;
|
|
496
|
+
if (pendingReadLevelRef.current === 1) return pendingRef.current > 0 ? 1 : 0;
|
|
497
|
+
return pendingRef.current;
|
|
498
|
+
}, []);
|
|
499
|
+
useSyncExternalStore2(subscribePending, getPendingSnapshot, getPendingSnapshot);
|
|
500
|
+
const withPending = useCallback2(async (fn) => {
|
|
501
|
+
const notify = () => {
|
|
502
|
+
for (const listener of pendingListenersRef.current) {
|
|
503
|
+
listener();
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
pendingRef.current++;
|
|
507
|
+
notify();
|
|
508
|
+
try {
|
|
509
|
+
return await fn();
|
|
510
|
+
} finally {
|
|
511
|
+
pendingRef.current--;
|
|
512
|
+
notify();
|
|
513
|
+
}
|
|
514
|
+
}, []);
|
|
515
|
+
const create = useCallback2(
|
|
516
|
+
async (schema, data, id) => {
|
|
517
|
+
if (!bridge) return null;
|
|
518
|
+
const start = telemetry ? Date.now() : 0;
|
|
519
|
+
const trace = tracing?.startTrace("mutate", "mutate:create");
|
|
520
|
+
try {
|
|
521
|
+
const result = await withPending(async () => {
|
|
522
|
+
const endBridge = trace?.mark(TRACE_STAGES.mutateBridge);
|
|
523
|
+
const node = await bridge.create(schema, data, id);
|
|
524
|
+
endBridge?.();
|
|
525
|
+
return flattenNode(node);
|
|
526
|
+
});
|
|
527
|
+
telemetry?.reportPerformance("react.useMutate.create", Date.now() - start);
|
|
528
|
+
telemetry?.reportUsage("react.useMutate.create.success", 1);
|
|
529
|
+
trace?.end();
|
|
530
|
+
return result;
|
|
531
|
+
} catch (err) {
|
|
532
|
+
trace?.end();
|
|
533
|
+
telemetry?.reportUsage("react.useMutate.create.failure", 1);
|
|
534
|
+
telemetry?.reportCrash(err instanceof Error ? err : new Error(String(err)), {
|
|
535
|
+
codeNamespace: "react.useMutate.create"
|
|
536
|
+
});
|
|
537
|
+
throw err;
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
[bridge, telemetry, tracing, withPending]
|
|
541
|
+
);
|
|
542
|
+
const update = useCallback2(
|
|
543
|
+
async (_schema, id, data) => {
|
|
544
|
+
if (!bridge) return null;
|
|
545
|
+
const start = telemetry ? Date.now() : 0;
|
|
546
|
+
const trace = tracing?.startTrace("mutate", "mutate:update");
|
|
547
|
+
try {
|
|
548
|
+
const result = await withPending(async () => {
|
|
549
|
+
const endBridge = trace?.mark(TRACE_STAGES.mutateBridge);
|
|
550
|
+
const node = await bridge.update(id, data);
|
|
551
|
+
endBridge?.();
|
|
552
|
+
return flattenNode(node);
|
|
553
|
+
});
|
|
554
|
+
telemetry?.reportPerformance("react.useMutate.update", Date.now() - start);
|
|
555
|
+
telemetry?.reportUsage("react.useMutate.update.success", 1);
|
|
556
|
+
trace?.end();
|
|
557
|
+
return result;
|
|
558
|
+
} catch (err) {
|
|
559
|
+
trace?.end();
|
|
560
|
+
telemetry?.reportUsage("react.useMutate.update.failure", 1);
|
|
561
|
+
telemetry?.reportCrash(err instanceof Error ? err : new Error(String(err)), {
|
|
562
|
+
codeNamespace: "react.useMutate.update",
|
|
563
|
+
nodeId: id
|
|
564
|
+
});
|
|
565
|
+
throw err;
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
[bridge, telemetry, tracing, withPending]
|
|
569
|
+
);
|
|
570
|
+
const remove = useCallback2(
|
|
571
|
+
async (id) => {
|
|
572
|
+
if (!bridge) return;
|
|
573
|
+
const start = telemetry ? Date.now() : 0;
|
|
574
|
+
const trace = tracing?.startTrace("mutate", "mutate:delete");
|
|
575
|
+
try {
|
|
576
|
+
await withPending(async () => {
|
|
577
|
+
const endBridge = trace?.mark(TRACE_STAGES.mutateBridge);
|
|
578
|
+
await bridge.delete(id);
|
|
579
|
+
endBridge?.();
|
|
580
|
+
});
|
|
581
|
+
telemetry?.reportPerformance("react.useMutate.delete", Date.now() - start);
|
|
582
|
+
telemetry?.reportUsage("react.useMutate.delete.success", 1);
|
|
583
|
+
trace?.end();
|
|
584
|
+
} catch (err) {
|
|
585
|
+
trace?.end();
|
|
586
|
+
telemetry?.reportUsage("react.useMutate.delete.failure", 1);
|
|
587
|
+
telemetry?.reportCrash(err instanceof Error ? err : new Error(String(err)), {
|
|
588
|
+
codeNamespace: "react.useMutate.delete",
|
|
589
|
+
nodeId: id
|
|
590
|
+
});
|
|
591
|
+
throw err;
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
[bridge, telemetry, tracing, withPending]
|
|
595
|
+
);
|
|
596
|
+
const restore = useCallback2(
|
|
597
|
+
async (id) => {
|
|
598
|
+
if (!bridge) return null;
|
|
599
|
+
const start = telemetry ? Date.now() : 0;
|
|
600
|
+
const trace = tracing?.startTrace("mutate", "mutate:restore");
|
|
601
|
+
try {
|
|
602
|
+
const result = await withPending(async () => {
|
|
603
|
+
const endBridge = trace?.mark(TRACE_STAGES.mutateBridge);
|
|
604
|
+
const node = await bridge.restore(id);
|
|
605
|
+
endBridge?.();
|
|
606
|
+
return flattenNode(node);
|
|
607
|
+
});
|
|
608
|
+
telemetry?.reportPerformance("react.useMutate.restore", Date.now() - start);
|
|
609
|
+
telemetry?.reportUsage("react.useMutate.restore.success", 1);
|
|
610
|
+
trace?.end();
|
|
611
|
+
return result;
|
|
612
|
+
} catch (err) {
|
|
613
|
+
trace?.end();
|
|
614
|
+
telemetry?.reportUsage("react.useMutate.restore.failure", 1);
|
|
615
|
+
telemetry?.reportCrash(err instanceof Error ? err : new Error(String(err)), {
|
|
616
|
+
codeNamespace: "react.useMutate.restore",
|
|
617
|
+
nodeId: id
|
|
618
|
+
});
|
|
619
|
+
throw err;
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
[bridge, telemetry, tracing, withPending]
|
|
623
|
+
);
|
|
624
|
+
const mutate = useCallback2(
|
|
625
|
+
async (ops) => {
|
|
626
|
+
if (!bridge || ops.length === 0) return null;
|
|
627
|
+
const start = telemetry ? Date.now() : 0;
|
|
628
|
+
const trace = tracing?.startTrace("mutate", "mutate:transaction");
|
|
629
|
+
try {
|
|
630
|
+
const result = await withPending(async () => {
|
|
631
|
+
const endBridge = trace?.mark(TRACE_STAGES.mutateBridge);
|
|
632
|
+
try {
|
|
633
|
+
const canUseTransactions = typeof bridge.transaction === "function";
|
|
634
|
+
const usesTempIds = hasTempIdsInOps(ops);
|
|
635
|
+
if (usesTempIds && !canUseTransactions) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
"Temp IDs in useMutate.mutate() require a transaction-capable bridge (DataBridge.transaction). Current bridge executes operations sequentially and cannot resolve temp IDs."
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
if (canUseTransactions) {
|
|
641
|
+
const tx = await bridge.transaction(toTransactionOperations(ops));
|
|
642
|
+
return {
|
|
643
|
+
results: tx.results,
|
|
644
|
+
batchId: tx.batchId,
|
|
645
|
+
tempIds: tx.tempIds
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const results = [];
|
|
649
|
+
for (const op of ops) {
|
|
650
|
+
switch (op.type) {
|
|
651
|
+
case "create": {
|
|
652
|
+
const node = await bridge.create(
|
|
653
|
+
op.schema,
|
|
654
|
+
op.data,
|
|
655
|
+
op.id
|
|
656
|
+
);
|
|
657
|
+
results.push(node);
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case "update": {
|
|
661
|
+
const node = await bridge.update(op.id, op.data);
|
|
662
|
+
results.push(node);
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case "delete": {
|
|
666
|
+
await bridge.delete(op.id);
|
|
667
|
+
results.push(null);
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
case "restore": {
|
|
671
|
+
const node = await bridge.restore(op.id);
|
|
672
|
+
results.push(node);
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return { results };
|
|
678
|
+
} finally {
|
|
679
|
+
endBridge?.();
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
telemetry?.reportPerformance("react.useMutate.transaction", Date.now() - start);
|
|
683
|
+
telemetry?.reportUsage("react.useMutate.transaction.success", 1);
|
|
684
|
+
trace?.end();
|
|
685
|
+
return result;
|
|
686
|
+
} catch (err) {
|
|
687
|
+
trace?.end();
|
|
688
|
+
telemetry?.reportUsage("react.useMutate.transaction.failure", 1);
|
|
689
|
+
telemetry?.reportCrash(err instanceof Error ? err : new Error(String(err)), {
|
|
690
|
+
codeNamespace: "react.useMutate.transaction",
|
|
691
|
+
opCount: ops.length
|
|
692
|
+
});
|
|
693
|
+
throw err;
|
|
694
|
+
}
|
|
695
|
+
},
|
|
696
|
+
[bridge, telemetry, tracing, withPending]
|
|
697
|
+
);
|
|
698
|
+
const bulk = useCallback2(
|
|
699
|
+
async (input) => {
|
|
700
|
+
if (!bridge) return null;
|
|
701
|
+
const start = telemetry ? Date.now() : 0;
|
|
702
|
+
try {
|
|
703
|
+
const result = await withPending(async () => bridge.bulkWrite(input));
|
|
704
|
+
telemetry?.reportPerformance("react.useMutate.bulk", Date.now() - start);
|
|
705
|
+
telemetry?.reportUsage("react.useMutate.bulk.success", 1);
|
|
706
|
+
return result;
|
|
707
|
+
} catch (err) {
|
|
708
|
+
telemetry?.reportUsage("react.useMutate.bulk.failure", 1);
|
|
709
|
+
telemetry?.reportCrash(err instanceof Error ? err : new Error(String(err)), {
|
|
710
|
+
codeNamespace: "react.useMutate.bulk",
|
|
711
|
+
kind: input.kind
|
|
712
|
+
});
|
|
713
|
+
throw err;
|
|
714
|
+
}
|
|
715
|
+
},
|
|
716
|
+
[bridge, telemetry, withPending]
|
|
717
|
+
);
|
|
718
|
+
return useMemo2(
|
|
719
|
+
() => ({
|
|
720
|
+
create,
|
|
721
|
+
update,
|
|
722
|
+
remove,
|
|
723
|
+
restore,
|
|
724
|
+
mutate,
|
|
725
|
+
bulk,
|
|
726
|
+
// Lazy getters record how much pending detail this component reads, so
|
|
727
|
+
// the external-store snapshot above can avoid re-rendering components
|
|
728
|
+
// that never look at pending state.
|
|
729
|
+
get isPending() {
|
|
730
|
+
pendingReadLevelRef.current = Math.max(pendingReadLevelRef.current, 1);
|
|
731
|
+
return pendingRef.current > 0;
|
|
732
|
+
},
|
|
733
|
+
get pendingCount() {
|
|
734
|
+
pendingReadLevelRef.current = 2;
|
|
735
|
+
return pendingRef.current;
|
|
736
|
+
}
|
|
737
|
+
}),
|
|
738
|
+
[create, update, remove, restore, mutate, bulk]
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/hooks/useSyncManager.ts
|
|
743
|
+
import { useContext } from "react";
|
|
744
|
+
function useSyncManager() {
|
|
745
|
+
const context = useContext(XNetContext);
|
|
746
|
+
return context?.syncManager ?? null;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export {
|
|
750
|
+
flattenNode,
|
|
751
|
+
flattenNodes,
|
|
752
|
+
flattenUnknownSchemaNode,
|
|
753
|
+
flattenNodesWithSchemaCheck,
|
|
754
|
+
EMPTY_PAGE_INFO,
|
|
755
|
+
computeFallbackPageInfo,
|
|
756
|
+
summarizePlan,
|
|
757
|
+
useQuery,
|
|
758
|
+
useMutate,
|
|
759
|
+
useSyncManager
|
|
760
|
+
};
|