@xnetjs/react 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ };