@gleanql/client 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.
package/src/proxy.ts ADDED
@@ -0,0 +1,288 @@
1
+ import { argAliasSuffix, canonicalArgs, type ArgMap, type ArgValue, type SchemaModel } from "@gleanql/core";
2
+ import type { GraphRef, FieldValue } from "./cache.js";
3
+ import type { GraphRuntime } from "./runtime.js";
4
+
5
+ /**
6
+ * Runtime graph proxies.
7
+ *
8
+ * The compiler statically infers what fields a route needs; this layer is what
9
+ * makes ordinary reads (`product.title`, `product.featuredImage?.url`,
10
+ * `collection.products({ first: 12 }).nodes`) actually *execute* at runtime.
11
+ *
12
+ * A graph value is a Proxy over a cache `GraphRef`. Property access routes
13
+ * through the Suspense-aware runtime:
14
+ * - scalar field -> `runtime.readField(ref, key)` (sync hit, or throws a promise)
15
+ * - object field -> the stored `GraphRef`, re-wrapped as a child proxy
16
+ * - list field -> an array of child proxies/scalars
17
+ * - callable field -> a function `(args) => value` (field arguments)
18
+ *
19
+ * The proxy is intentionally transparent: parent components pass it as a normal
20
+ * prop, child components read fields off it, and nothing in userland sees a ref,
21
+ * a selection object, or a promise (the promise is thrown to Suspense).
22
+ */
23
+
24
+ /** Escape-hatch / brand keys exposed on every graph proxy. */
25
+ export const GRAPH_REF = Symbol.for("graph.ref");
26
+ export const GRAPH_TYPE = Symbol.for("graph.type");
27
+ export const GRAPH_TRAIL = Symbol.for("graph.trail");
28
+
29
+ /**
30
+ * Read tracking for fine-grained reactivity. Every field read records the record
31
+ * key it touched into the active tracker; the tracking hook's `useSyncExternalStore`
32
+ * snapshot is then a digest of just those records' versions.
33
+ *
34
+ * Attribution is primarily PER BINDING: `useGlean` binds the graph with its render's
35
+ * own `affected` set (see `GraphBinding.tracker`), so reads through that render's
36
+ * proxies record into that set directly — fiber-local, safe under concurrent/
37
+ * interleaved rendering. This ambient global is only a fallback for proxies created
38
+ * without a binding tracker (the server / isomorphic accessor), where no re-render
39
+ * depends on attribution.
40
+ */
41
+ let currentTracker: Set<string> | null = null;
42
+
43
+ /** Install (or clear) the active read tracker. Returns the previous one. */
44
+ export function setReadTracker(tracker: Set<string> | null): Set<string> | null {
45
+ const prev = currentTracker;
46
+ currentTracker = tracker;
47
+ return prev;
48
+ }
49
+
50
+ /** One object-field hop from a Query root: the field name + the args it was called with. */
51
+ export interface PathStep {
52
+ readonly name: string;
53
+ readonly args?: Record<string, unknown>;
54
+ }
55
+
56
+ /** The hidden selection token (brief: `product.selection`). */
57
+ export interface GraphSelection {
58
+ readonly ref: GraphRef;
59
+ readonly type: string;
60
+ }
61
+
62
+ /** A value is a `GraphRef` if it carries entity identity or a path. */
63
+ export function isGraphRef(value: unknown): value is GraphRef {
64
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
65
+ const v = value as Record<string, unknown>;
66
+ return (v.__typename != null && v.id != null) || typeof v.path === "string";
67
+ }
68
+
69
+ function toArgValue(value: unknown): ArgValue {
70
+ if (value === null) return { kind: "literal", value: null };
71
+ if (Array.isArray(value)) return { kind: "list", items: value.map(toArgValue) };
72
+ if (typeof value === "object") {
73
+ return { kind: "object", fields: Object.entries(value as object).map(([k, v]) => [k, toArgValue(v)] as const) };
74
+ }
75
+ return { kind: "literal", value: value as string | number | boolean };
76
+ }
77
+
78
+ /** Plain runtime args (`{ first: 12 }`) -> IR `ArgMap`, reusing core's canonicalization. */
79
+ export function toArgMap(args: Record<string, unknown> | undefined): ArgMap {
80
+ if (!args) return [];
81
+ return Object.entries(args).map(([k, v]) => [k, toArgValue(v)] as const);
82
+ }
83
+
84
+ /**
85
+ * Candidate response keys for a (possibly callable) field, most-specific first.
86
+ * A callable field that coexisted with a differently-argued sibling was aliased
87
+ * by the merger (`url_transformMaxWidth300`); a lone callable field keeps its
88
+ * plain name. The proxy tries the alias key first, then the plain name, which
89
+ * is correct for both shapes without the proxy having to know about conflicts.
90
+ */
91
+ export function responseKeyCandidates(name: string, argMap: ArgMap): readonly string[] {
92
+ if (argMap.length === 0) return [name];
93
+ const suffix = argAliasSuffix(argMap);
94
+ return suffix ? [`${name}_${suffix}`, name] : [name];
95
+ }
96
+
97
+ export interface GraphBinding {
98
+ readonly schema: SchemaModel;
99
+ /** Resolve the active runtime lazily (per request on the server). */
100
+ readonly getRuntime: () => GraphRuntime;
101
+ /**
102
+ * Per-binding read tracker. When a `useGlean` render binds the graph with its own
103
+ * `affected` set, every proxy created from this binding records reads into THAT set
104
+ * — not the ambient global — so concurrent/interleaved renders can't misattribute.
105
+ * Absent (server / isomorphic accessor) → reads fall back to the ambient tracker.
106
+ */
107
+ readonly tracker?: Set<string>;
108
+ }
109
+
110
+ interface ProxyState {
111
+ readonly binding: GraphBinding;
112
+ readonly ref: GraphRef;
113
+ readonly type: string;
114
+ /** Object-field path from a Query root to this value (drives `usePaginated`/`fetchMore`). */
115
+ readonly trail: readonly PathStep[];
116
+ }
117
+
118
+ /** Read a field on a ref, resolving the right response key, and wrap object/list results. */
119
+ function readField(state: ProxyState, fieldName: string, args?: Record<string, unknown>): unknown {
120
+ const { binding, ref, type } = state;
121
+ const runtime = binding.getRuntime();
122
+ const fieldDef = binding.schema.getField(type, fieldName);
123
+ const argMap = toArgMap(args);
124
+ const candidates = responseKeyCandidates(fieldName, argMap);
125
+
126
+ // Prefer an already-cached candidate key; otherwise read the most specific one
127
+ // (which suspends + enqueues a missing-field fetch under that stable key).
128
+ let key = candidates[0]!;
129
+ for (const candidate of candidates) {
130
+ if (runtime.cache.getField(ref, candidate).status === "ready") {
131
+ key = candidate;
132
+ break;
133
+ }
134
+ }
135
+
136
+ // Record this exact field as read (field-level granularity), so the rendering
137
+ // component re-renders only when a field IT read changes — not on any write to the
138
+ // record. Tracked before the read so a currently-missing field still re-renders
139
+ // when it lands. Guard: refs always carry identity or a path here. Prefer the
140
+ // binding's own set (fiber-scoped, set by useGlean) over the ambient global.
141
+ const tracker = binding.tracker ?? currentTracker;
142
+ if (tracker) {
143
+ try {
144
+ tracker.add(runtime.cache.fieldTrackingKey(runtime.cache.recordKey(ref), key));
145
+ } catch {
146
+ /* ref without identity/path — not trackable */
147
+ }
148
+ }
149
+
150
+ const raw = runtime.readField(ref, key);
151
+ const childTrail = [...state.trail, { name: fieldName, ...(args ? { args } : {}) }];
152
+ return wrap(binding, raw, fieldDef?.type, childTrail);
153
+ }
154
+
155
+ /** Wrap a raw cache value as a child proxy (object), array of proxies (list), or scalar. */
156
+ function wrap(binding: GraphBinding, value: FieldValue, declaredType: string | undefined, trail: readonly PathStep[]): unknown {
157
+ if (Array.isArray(value)) {
158
+ return value.map((item) => wrap(binding, item, declaredType, trail));
159
+ }
160
+ if (isGraphRef(value)) {
161
+ // For union/interface fields the concrete type comes from the ref's __typename.
162
+ const type = (value.__typename as string | undefined) ?? declaredType ?? "Unknown";
163
+ return createGraphProxy(binding, value, type, trail);
164
+ }
165
+ return value;
166
+ }
167
+
168
+ const handler: ProxyHandler<{ state: ProxyState }> = {
169
+ get(target, prop) {
170
+ const { state } = target;
171
+ if (prop === GRAPH_REF) return state.ref;
172
+ if (prop === GRAPH_TYPE) return state.type;
173
+ if (prop === GRAPH_TRAIL) return state.trail;
174
+ if (prop === "selection") {
175
+ return { ref: state.ref, type: state.type } satisfies GraphSelection;
176
+ }
177
+ if (typeof prop === "symbol") return undefined;
178
+ if (prop === "__typename") {
179
+ // Identity field: prefer the ref's own typename, fall back to a cache read.
180
+ return state.ref.__typename ?? readField(state, "__typename");
181
+ }
182
+
183
+ const fieldName = prop;
184
+ const fieldDef = state.binding.schema.getField(state.type, fieldName);
185
+
186
+ // Callable field (has declared arguments): return a function `(args) => value`.
187
+ if (fieldDef?.args && fieldDef.args.length > 0) {
188
+ return (args?: Record<string, unknown>) => readField(state, fieldName, args);
189
+ }
190
+ return readField(state, fieldName);
191
+ },
192
+ has(target, prop) {
193
+ if (prop === GRAPH_REF || prop === GRAPH_TYPE || prop === GRAPH_TRAIL || prop === "selection") return true;
194
+ return typeof prop === "string" && !!target.state.binding.schema.getField(target.state.type, prop);
195
+ },
196
+ set() {
197
+ throw new Error("graph values are read-only");
198
+ },
199
+ ownKeys() {
200
+ // Graph values are not enumerable/spreadable (brief: spreading is a diagnostic).
201
+ return [];
202
+ },
203
+ };
204
+
205
+ export function createGraphProxy(binding: GraphBinding, ref: GraphRef, type: string, trail: readonly PathStep[] = []): unknown {
206
+ return new Proxy({ state: { binding, ref, type, trail } }, handler);
207
+ }
208
+
209
+ /** Read the hidden selection token off any graph proxy. */
210
+ export function selectionOf(value: unknown): GraphSelection | undefined {
211
+ if (value && typeof value === "object" && GRAPH_REF in value) {
212
+ const v = value as { [GRAPH_REF]: GraphRef; [GRAPH_TYPE]: string };
213
+ return { ref: v[GRAPH_REF], type: v[GRAPH_TYPE] };
214
+ }
215
+ return undefined;
216
+ }
217
+
218
+ /** Read the root→value path off any graph proxy (for `usePaginated`/`fetchMore`). */
219
+ export function trailOf(value: unknown): readonly PathStep[] | undefined {
220
+ if (value && typeof value === "object" && GRAPH_TRAIL in value) {
221
+ return (value as { [GRAPH_TRAIL]: readonly PathStep[] })[GRAPH_TRAIL];
222
+ }
223
+ return undefined;
224
+ }
225
+
226
+ // --- Bound graph ----------------------------------------------------------
227
+
228
+ /**
229
+ * The runtime `graph` object: one callable per Query root field, each returning
230
+ * a graph proxy. Root values resolve to the ref the operation seeded (via the
231
+ * `roots` map) so reads hit the warm cache; an unseeded root falls back to a
232
+ * path-identity ref and suspends.
233
+ */
234
+ export interface BoundGraph {
235
+ readonly [rootField: string]: (args?: Record<string, unknown>) => unknown;
236
+ }
237
+
238
+ export interface BindGraphOptions {
239
+ readonly schema: SchemaModel;
240
+ readonly getRuntime: () => GraphRuntime;
241
+ /**
242
+ * Root field -> seeded ref, from `runRoute`/`seedResult`. A function form is
243
+ * resolved per call, so the bound graph can follow page-current roots that
244
+ * change across client navigations (the RSC hydrator updates them per nav).
245
+ */
246
+ readonly roots?:
247
+ | Record<string, FieldValue>
248
+ | (() => Record<string, FieldValue> | undefined);
249
+ /**
250
+ * A read tracker scoping this binding's reads to one render (see `GraphBinding`).
251
+ * `useGlean` passes its render's `affected` set so attribution is fiber-local;
252
+ * the server/isomorphic accessor omits it.
253
+ */
254
+ readonly tracker?: Set<string>;
255
+ }
256
+
257
+ export function bindGraph(options: BindGraphOptions): BoundGraph {
258
+ const binding: GraphBinding = {
259
+ schema: options.schema,
260
+ getRuntime: options.getRuntime,
261
+ ...(options.tracker ? { tracker: options.tracker } : {}),
262
+ };
263
+ const queryType = options.schema.queryType;
264
+ const rootFields = options.schema.getType(queryType)?.fields ?? {};
265
+
266
+ const graph: Record<string, (args?: Record<string, unknown>) => unknown> = {};
267
+ for (const [fieldName, fieldDef] of Object.entries(rootFields)) {
268
+ graph[fieldName] = (args?: Record<string, unknown>) => {
269
+ const rootsNow =
270
+ typeof options.roots === "function" ? options.roots() : options.roots;
271
+ const seeded = rootsNow?.[fieldName];
272
+ const trail: PathStep[] = [{ name: fieldName, ...(args ? { args } : {}) }];
273
+ // A list root (`type Query { todos: [Todo!] }`) seeds an array of refs — wrap
274
+ // each as a child proxy so `glean.todos().map(...)` works without an object
275
+ // wrapper. Unseeded (pre-hydration / not yet fetched) -> empty array; the
276
+ // page-pointer re-render fills it once the operation resolves.
277
+ if (fieldDef.list) {
278
+ const items = Array.isArray(seeded) ? seeded : [];
279
+ return items.map((item) => wrap(binding, item, fieldDef.type, trail));
280
+ }
281
+ const ref: GraphRef = isGraphRef(seeded)
282
+ ? seeded
283
+ : { path: `${queryType}.${fieldName}(${canonicalArgs(toArgMap(args))})` };
284
+ return createGraphProxy(binding, ref, fieldDef.type, trail);
285
+ };
286
+ }
287
+ return graph as BoundGraph;
288
+ }
@@ -0,0 +1,149 @@
1
+ import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
2
+ import type { GraphCache } from "./cache.js";
3
+ import type { GraphRuntime } from "./runtime.js";
4
+
5
+ /**
6
+ * Fine-grained re-render substrate (valtio-style).
7
+ *
8
+ * A component tracks the records it read this render; its `useSyncExternalStore`
9
+ * snapshot is gated by a digest of just those records' versions, so a global cache
10
+ * notify only re-renders the components whose records actually changed — no per-key
11
+ * subscription fan-out. The hooks in `glue-client.ts` drive this: `useGlean` installs
12
+ * an ambient read tracker, `usePaginated` seeds the set with its connection's record.
13
+ */
14
+
15
+ /** A stable digest of the tracked keys' versions; changes iff a tracked key changed.
16
+ * Each key is resolved at its own granularity — a field (`record\0field`, from
17
+ * `useGlean`) or a whole record (from `usePaginated`) — via {@link GraphCache.trackedVersion}. */
18
+ export function affectedDigest(cache: GraphCache, keys: ReadonlySet<string>): string {
19
+ if (keys.size === 0) return "";
20
+ let out = "";
21
+ for (const key of keys) out += `${key}:${cache.trackedVersion(key)}|`;
22
+ return out;
23
+ }
24
+
25
+ /**
26
+ * Re-render the caller only when one of the records in `affected.current` changes.
27
+ * The external snapshot is a monotonic counter (stable during render, decoupled from
28
+ * the live set) so it never spuriously diverges from the render-time value; the
29
+ * subscriber bumps it only when the digest actually changed, and an effect rebases the
30
+ * baseline to this render's reads. SSR is a no-op (`getServerSnapshot` → 0).
31
+ */
32
+ export function useFineGrainedRerender(
33
+ runtime: GraphRuntime | undefined,
34
+ affected: { current: Set<string> },
35
+ ): void {
36
+ const tick = useRef(0);
37
+ const baseline = useRef("");
38
+ const subscribe = useCallback(
39
+ (onChange: () => void) => {
40
+ if (!runtime) return () => {};
41
+ const cache = runtime.cache;
42
+ baseline.current = affectedDigest(cache, affected.current); // rebase at (re)subscribe
43
+ return cache.subscribe(() => {
44
+ const next = affectedDigest(cache, affected.current);
45
+ if (next !== baseline.current) {
46
+ baseline.current = next;
47
+ tick.current++;
48
+ onChange();
49
+ }
50
+ });
51
+ },
52
+ [runtime, affected],
53
+ );
54
+ useSyncExternalStore(subscribe, () => tick.current, () => 0);
55
+ // Post-commit: rebase to THIS render's reads + current versions, so the next notify
56
+ // compares against the records actually read this pass (they can differ per render).
57
+ useEffect(() => {
58
+ if (runtime) baseline.current = affectedDigest(runtime.cache, affected.current);
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Diff this render's tracked keys against the currently-held retentions:
64
+ * retain newly-read records, release ones no longer read. `held` maps record
65
+ * key → its release. Pure cache+map logic (the React hook is a thin effect
66
+ * shell over it), so it's directly unit-testable.
67
+ */
68
+ export function syncRetention(cache: GraphCache, held: Map<string, () => void>, tracked: ReadonlySet<string>): void {
69
+ const current = new Set<string>();
70
+ for (const key of tracked) current.add(cache.trackedRecordKey(key));
71
+ for (const key of current) {
72
+ if (!held.has(key)) held.set(key, cache.retain(key));
73
+ }
74
+ for (const [key, release] of held) {
75
+ if (!current.has(key)) {
76
+ release();
77
+ held.delete(key);
78
+ }
79
+ }
80
+ }
81
+
82
+ /** Release every held retention (unmount). */
83
+ export function releaseRetention(held: Map<string, () => void>): void {
84
+ for (const release of held.values()) release();
85
+ held.clear();
86
+ }
87
+
88
+ /**
89
+ * Retain this render's records while mounted (reference-counted, Relay-style):
90
+ * post-commit, the records the component read are pinned — `cache.gc()` and LRU
91
+ * eviction skip them — and released when the component stops reading them or
92
+ * unmounts.
93
+ */
94
+ function useRetained(runtime: GraphRuntime | undefined, affected: { current: Set<string> }): void {
95
+ const held = useRef<Map<string, () => void>>(new Map());
96
+ useEffect(() => {
97
+ if (runtime) syncRetention(runtime.cache, held.current, affected.current);
98
+ });
99
+ useEffect(() => {
100
+ const map = held.current;
101
+ return () => releaseRetention(map);
102
+ }, []);
103
+ }
104
+
105
+ /**
106
+ * Translate a render's tracked keys into masked-read violations: field-level
107
+ * reads on IDENTIFIED records whose `Type.field` pair is outside the
108
+ * component's compiled read-map. Record-level trackers and path-identity
109
+ * records carry no typename — skipped, never guessed.
110
+ */
111
+ export function maskViolations(
112
+ cache: GraphCache,
113
+ allowed: ReadonlySet<string>,
114
+ tracked: ReadonlySet<string>,
115
+ ): string[] {
116
+ const out: string[] = [];
117
+ for (const key of tracked) {
118
+ const record = cache.trackedRecordKey(key);
119
+ if (record === key) continue; // record-level tracker — not a field read
120
+ if (record.startsWith("path:")) continue; // id-less record — typename unknown
121
+ const pair = `${record.slice(0, record.indexOf(":"))}.${key.slice(record.length + 1)}`;
122
+ if (!allowed.has(pair)) out.push(pair);
123
+ }
124
+ return out;
125
+ }
126
+
127
+ /**
128
+ * The shared shape of the tracking hooks: hold a per-render "affected records" set,
129
+ * let the caller `populate` it (install an ambient tracker, or seed a known record),
130
+ * and wire fine-grained re-rendering over it. `populate` runs synchronously in the
131
+ * hook body, so reads that follow in the component attribute to this render's set.
132
+ * While mounted, the records read are retained (see {@link useRetained});
133
+ * `onCommit` (if given) sees the final set post-commit — the masking check.
134
+ */
135
+ export function useTracked(
136
+ runtime: GraphRuntime | undefined,
137
+ populate: (affected: Set<string>) => void,
138
+ onCommit?: (tracked: ReadonlySet<string>) => void,
139
+ ): void {
140
+ const affected = useRef<Set<string>>(new Set());
141
+ const tracking = new Set<string>();
142
+ populate(tracking);
143
+ affected.current = tracking;
144
+ useFineGrainedRerender(runtime, affected);
145
+ useRetained(runtime, affected);
146
+ useEffect(() => {
147
+ onCommit?.(affected.current);
148
+ });
149
+ }
package/src/route.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type { SelectionSet } from "@gleanql/core";
2
+ import type { GraphClientAdapter, GraphRequestContext } from "./adapter.js";
3
+ import type { GraphRuntime } from "./runtime.js";
4
+ import type { FieldValue } from "./cache.js";
5
+ import { persistRootLinks, resolveFromCache } from "./cache-resolve.js";
6
+
7
+ /**
8
+ * Framework-integration seam. A compiled operation (the artifact the compiler
9
+ * emits) plus a client adapter and a request context is enough to drive a
10
+ * route: compute variables, execute, seed the cache. A framework adapter
11
+ * (RWSDK first) answers "which operation for this entrypoint?" and "how do I
12
+ * build the request context?".
13
+ */
14
+ export interface CompiledOperation<RouteContext = unknown, TVariables = Record<string, unknown>> {
15
+ readonly name: string;
16
+ readonly kind: "query" | "mutation" | "subscription";
17
+ readonly document: string;
18
+ readonly hash?: string;
19
+ readonly variables: (ctx: RouteContext) => TVariables;
20
+ readonly readMap?: Record<string, readonly string[]>;
21
+ /** Merged selection tree; enables cache-first resolution when present. */
22
+ readonly selection?: SelectionSet;
23
+ }
24
+
25
+ export interface RunRouteOptions {
26
+ /**
27
+ * Cache-first: if the cache already satisfies the operation's full selection,
28
+ * skip the network. Defaults to true; pass false to always fetch (e.g. an
29
+ * explicit refresh that must hit the server).
30
+ */
31
+ readonly cacheFirst?: boolean;
32
+ }
33
+
34
+ export interface RunRouteResult<TVariables> {
35
+ readonly variables: TVariables;
36
+ readonly roots: Record<string, FieldValue>;
37
+ readonly errors?: ReadonlyArray<{ message: string }>;
38
+ }
39
+
40
+ /** Execute a compiled operation and seed the runtime cache (steps 2–5 of the route flow). */
41
+ export async function runRoute<RouteContext, TVariables extends Record<string, unknown>>(args: {
42
+ operation: CompiledOperation<RouteContext, TVariables>;
43
+ routeContext: RouteContext;
44
+ adapter: GraphClientAdapter;
45
+ context: GraphRequestContext;
46
+ runtime: GraphRuntime;
47
+ options?: RunRouteOptions;
48
+ }): Promise<RunRouteResult<TVariables>> {
49
+ const variables = args.operation.variables(args.routeContext);
50
+ const selection = args.operation.selection;
51
+ const cacheFirst = args.options?.cacheFirst ?? true;
52
+
53
+ // Cache-first: serve from the normalized cache when it already covers the op.
54
+ if (cacheFirst && selection) {
55
+ const hit = resolveFromCache(args.runtime.cache, selection, variables);
56
+ if (hit.covered) return { variables, roots: hit.roots };
57
+ }
58
+
59
+ const result = await args.adapter.execute(
60
+ { name: args.operation.name, kind: args.operation.kind, document: args.operation.document },
61
+ variables,
62
+ args.context,
63
+ );
64
+ const roots = result.data ? args.runtime.seedResult(result.data as Record<string, unknown>) : {};
65
+ // Persist root links so a later run can resolve this operation from cache.
66
+ if (selection && result.data) persistRootLinks(args.runtime.cache, selection, variables, roots);
67
+ return { variables, roots, errors: result.errors };
68
+ }
69
+
70
+ /**
71
+ * Re-run an operation against the network, bypassing cache-first, and re-seed.
72
+ * The re-seed writes through the cache, bumping its version and notifying
73
+ * subscribers — so a `useSyncExternalStore` (`cache.subscribe`) re-renders the
74
+ * UI with the fresh data. Use for an explicit "Refresh" / post-mutation refetch.
75
+ */
76
+ export function refetch<RouteContext, TVariables extends Record<string, unknown>>(args: {
77
+ operation: CompiledOperation<RouteContext, TVariables>;
78
+ routeContext: RouteContext;
79
+ adapter: GraphClientAdapter;
80
+ context: GraphRequestContext;
81
+ runtime: GraphRuntime;
82
+ }): Promise<RunRouteResult<TVariables>> {
83
+ return runRoute({ ...args, options: { cacheFirst: false } });
84
+ }