@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/dist/index.mjs ADDED
@@ -0,0 +1,1412 @@
1
+ import { argAliasSuffix, canonicalArgs, responseKey } from "@gleanql/core";
2
+ //#region src/adapter-shared.ts
3
+ /** Build a `GraphResult`, omitting empty `data`/`errors` keys. */
4
+ function result(data, errors) {
5
+ return {
6
+ ...data != null ? { data } : {},
7
+ ...errors && errors.length > 0 ? { errors } : {}
8
+ };
9
+ }
10
+ /**
11
+ * Bridge a push-based transport (SSE `onmessage`, a graphql-ws sink) into the
12
+ * pull-based `AsyncIterator` the runtime consumes. The transport wires its callbacks
13
+ * to `push`/`finish`; the consumer's `return()` (cleanup) calls `onReturn` to tear the
14
+ * transport down. Values pushed with no pending pull queue up and drain in order.
15
+ */
16
+ function pushPullIterator(onReturn) {
17
+ const queue = [];
18
+ let waiting = null;
19
+ let done = false;
20
+ const ended = () => ({
21
+ value: void 0,
22
+ done: true
23
+ });
24
+ const finish = () => {
25
+ if (done) return;
26
+ done = true;
27
+ if (waiting) {
28
+ waiting(ended());
29
+ waiting = null;
30
+ }
31
+ };
32
+ return {
33
+ push(value) {
34
+ if (done) return;
35
+ if (waiting) {
36
+ waiting({
37
+ value,
38
+ done: false
39
+ });
40
+ waiting = null;
41
+ } else queue.push(value);
42
+ },
43
+ finish,
44
+ next() {
45
+ if (queue.length > 0) return Promise.resolve({
46
+ value: queue.shift(),
47
+ done: false
48
+ });
49
+ if (done) return Promise.resolve(ended());
50
+ return new Promise((resolve) => {
51
+ waiting = resolve;
52
+ });
53
+ },
54
+ return() {
55
+ onReturn?.();
56
+ finish();
57
+ return Promise.resolve(ended());
58
+ }
59
+ };
60
+ }
61
+ //#endregion
62
+ //#region src/adapter.ts
63
+ /**
64
+ * Client adapter interface. The graph runtime owns cache identity, Suspense,
65
+ * batching and normalization; an adapter owns transport (HTTP, auth, retries).
66
+ * Ships a plain fetch adapter; any transport (graphql-ws for subscriptions, or a
67
+ * urql/Apollo client if an app already runs one) slots in behind this interface.
68
+ */
69
+ /** Minimal fetch transport. Context is used only to build headers; it is never serialized into the body. */
70
+ function createFetchAdapter(options) {
71
+ const doFetch = options.fetch ?? fetch;
72
+ return {
73
+ async execute(operation, variables, context) {
74
+ const post = async (body) => {
75
+ const res = await doFetch(options.endpoint, {
76
+ method: "POST",
77
+ headers: {
78
+ "content-type": "application/json",
79
+ ...options.headers ? options.headers(context) : {}
80
+ },
81
+ body: JSON.stringify(body)
82
+ });
83
+ const parsed = await res.json().catch(() => void 0);
84
+ if (parsed === void 0) throw new Error(`graph fetch: non-JSON response (${res.status} ${res.statusText}) from ${options.endpoint}`);
85
+ return parsed;
86
+ };
87
+ if (options.persisted && operation.hash) {
88
+ const extensions = { persistedQuery: {
89
+ version: 1,
90
+ sha256Hash: operation.hash
91
+ } };
92
+ const first = await post({
93
+ operationName: operation.name,
94
+ variables,
95
+ extensions
96
+ });
97
+ if (first.errors?.some((e) => e.message === "PersistedQueryNotFound")) {
98
+ options.onPersistedRetry?.(operation.name);
99
+ return post({
100
+ query: operation.document,
101
+ operationName: operation.name,
102
+ variables,
103
+ extensions
104
+ });
105
+ }
106
+ return first;
107
+ }
108
+ return post({
109
+ query: operation.document,
110
+ variables,
111
+ operationName: operation.name
112
+ });
113
+ },
114
+ subscribe(operation, variables) {
115
+ return sseIterable(options.subscriptionEndpoint ?? `${options.endpoint}/stream`, operation.document, variables, operation.name);
116
+ }
117
+ };
118
+ }
119
+ /** Bridge a Server-Sent Events stream into an `AsyncIterable<GraphResult>`. */
120
+ function sseIterable(url, document, variables, operationName) {
121
+ return { [Symbol.asyncIterator]() {
122
+ if (typeof EventSource === "undefined") return { next: () => Promise.resolve({
123
+ value: void 0,
124
+ done: true
125
+ }) };
126
+ const qs = `query=${encodeURIComponent(document)}&operationName=${encodeURIComponent(operationName)}&variables=${encodeURIComponent(JSON.stringify(variables ?? {}))}`;
127
+ const es = new EventSource(`${url}?${qs}`);
128
+ const it = pushPullIterator(() => es.close());
129
+ es.onmessage = (e) => {
130
+ try {
131
+ it.push(JSON.parse(e.data));
132
+ } catch {}
133
+ };
134
+ es.onerror = () => it.push({ errors: [{ message: "subscription stream error" }] });
135
+ return it;
136
+ } };
137
+ }
138
+ //#endregion
139
+ //#region src/cache.ts
140
+ /** Separator joining a record key + field key into one field-tracking key (NUL — never in a key). */
141
+ const FIELD_SEP = "\0";
142
+ function fieldTrackingKey(recordKey, fieldKey) {
143
+ return recordKey + FIELD_SEP + fieldKey;
144
+ }
145
+ var GraphCache = class GraphCache {
146
+ maxRecords;
147
+ records = /* @__PURE__ */ new Map();
148
+ /**
149
+ * Optional LRU cap. The client cache accumulates entities across navigations; a
150
+ * long session would otherwise grow without bound. When set, the least-recently
151
+ * used records are evicted past the cap. Unset (default) = unbounded, so the
152
+ * server's per-request cache and existing callers are unchanged.
153
+ */
154
+ constructor(maxRecords) {
155
+ this.maxRecords = maxRecords;
156
+ }
157
+ /**
158
+ * Reactivity substrate. Every write bumps `version` and notifies listeners, so
159
+ * UI can re-render after a mutation, refetch, or peer-tab/subscription update.
160
+ * `version` + `subscribe` are exactly the `useSyncExternalStore` contract.
161
+ */
162
+ _version = 0;
163
+ listeners = /* @__PURE__ */ new Set();
164
+ /**
165
+ * Version counters for fine-grained reactivity, at two granularities. A component
166
+ * tracks the keys it read during render; its `useSyncExternalStore` snapshot is a
167
+ * digest of those keys' versions, so a global notify only re-renders the components
168
+ * whose keys actually changed (valtio's approach — no per-key subscription fan-out).
169
+ *
170
+ * - `recordVersions` bumps on ANY write to a record (record-level trackers, e.g.
171
+ * `usePaginated` watching a connection).
172
+ * - `fieldVersions` bumps only the written field (field-level trackers, e.g.
173
+ * `useGlean`, so reading `product.title` ignores a write to `product.views`).
174
+ *
175
+ * The global `version`/`subscribe` stay the notify channel; both granularities are
176
+ * resolved through {@link trackedVersion}.
177
+ */
178
+ recordVersions = /* @__PURE__ */ new Map();
179
+ fieldVersions = /* @__PURE__ */ new Map();
180
+ /** Current version of a record (0 if never written). */
181
+ recordVersion(key) {
182
+ return this.recordVersions.get(key) ?? 0;
183
+ }
184
+ /** Current version of a single field on a record (0 if never written). */
185
+ fieldVersion(recordKey, fieldKey) {
186
+ return this.fieldVersions.get(fieldTrackingKey(recordKey, fieldKey)) ?? 0;
187
+ }
188
+ /** The opaque tracking key a field read records; resolve it with {@link trackedVersion}. */
189
+ fieldTrackingKey(recordKey, fieldKey) {
190
+ return fieldTrackingKey(recordKey, fieldKey);
191
+ }
192
+ /** Version of a tracked key: a bare record key, or `record\0field` for a single field. */
193
+ trackedVersion(trackingKey) {
194
+ return trackingKey.includes(FIELD_SEP) ? this.fieldVersions.get(trackingKey) ?? 0 : this.recordVersion(trackingKey);
195
+ }
196
+ /** Bump a record's version + (optionally) one of its fields' versions. */
197
+ bumpRecord(key, fieldKey) {
198
+ this.recordVersions.set(key, (this.recordVersions.get(key) ?? 0) + 1);
199
+ if (fieldKey !== void 0) this.bumpField(key, fieldKey);
200
+ }
201
+ bumpField(recordKey, fieldKey) {
202
+ const k = fieldTrackingKey(recordKey, fieldKey);
203
+ this.fieldVersions.set(k, (this.fieldVersions.get(k) ?? 0) + 1);
204
+ }
205
+ get version() {
206
+ return this._version;
207
+ }
208
+ subscribe(listener) {
209
+ this.listeners.add(listener);
210
+ return () => this.listeners.delete(listener);
211
+ }
212
+ bump() {
213
+ this._version++;
214
+ for (const listener of this.listeners) listener();
215
+ }
216
+ /** Public notify: bump the version + run listeners (e.g. after `absorbRecords`). */
217
+ notify() {
218
+ this.bump();
219
+ }
220
+ /** Stable storage key for a ref: entity identity wins over path identity. */
221
+ recordKey(ref) {
222
+ if (ref.__typename != null && ref.id != null) return `${ref.__typename}:${ref.id}`;
223
+ if (ref.path != null) return `path:${ref.path}`;
224
+ throw new Error("GraphRef requires either (__typename + id) or path");
225
+ }
226
+ /**
227
+ * Reference-counted retention (Relay-style). A mounted reader retains the
228
+ * records it displays; retained records are never LRU-evicted and survive
229
+ * {@link gc}. The tracking hooks do this automatically — each component
230
+ * retains what it read while mounted — so `gc()` is safe to call any time
231
+ * (e.g. on navigation): it can only drop records nothing on screen reads.
232
+ */
233
+ retainCounts = /* @__PURE__ */ new Map();
234
+ /** Pin a record. Returns the matching release; calling it twice is a no-op. */
235
+ retain(key) {
236
+ this.stamp(key);
237
+ this.retainCounts.set(key, (this.retainCounts.get(key) ?? 0) + 1);
238
+ let released = false;
239
+ return () => {
240
+ if (released) return;
241
+ released = true;
242
+ const n = this.retainCounts.get(key) ?? 0;
243
+ if (n <= 1) this.retainCounts.delete(key);
244
+ else this.retainCounts.set(key, n - 1);
245
+ };
246
+ }
247
+ isRetained(key) {
248
+ return this.retainCounts.has(key);
249
+ }
250
+ /** The record key a tracked key belongs to (strips the `\0field` part, if any). */
251
+ trackedRecordKey(trackingKey) {
252
+ const i = trackingKey.indexOf(FIELD_SEP);
253
+ return i === -1 ? trackingKey : trackingKey.slice(0, i);
254
+ }
255
+ /**
256
+ * Generation clock for staleness-aware GC. The glue advances it on each page
257
+ * navigation; every read/write/retain stamps the record with the current
258
+ * epoch. "Unretained" alone is NOT a reason to drop data (a back-navigation
259
+ * should hit a warm cache) — `gc({ keepEpochs })` drops only records that are
260
+ * unretained AND haven't been touched for that many generations.
261
+ */
262
+ epoch = 0;
263
+ lastActive = /* @__PURE__ */ new Map();
264
+ /** Advance the generation clock (call on navigation). Returns the new epoch. */
265
+ advanceEpoch() {
266
+ return ++this.epoch;
267
+ }
268
+ stamp(key) {
269
+ this.lastActive.set(key, this.epoch);
270
+ }
271
+ /**
272
+ * Drop unretained records; returns how many were dropped. Version counters
273
+ * survive, so if a dropped record is refetched its trackers still see
274
+ * monotonic versions.
275
+ *
276
+ * - `gc()` — drop EVERY unretained record (a full reset, e.g. logout).
277
+ * - `gc({ keepEpochs: N })` — drop only records also untouched for ≥ N
278
+ * generations (see {@link advanceEpoch}); recently-used data stays warm
279
+ * for back-navigation even though nothing on screen retains it.
280
+ */
281
+ gc(options = {}) {
282
+ const { keepEpochs } = options;
283
+ let dropped = 0;
284
+ for (const key of [...this.records.keys()]) {
285
+ if (this.retainCounts.has(key)) continue;
286
+ if (keepEpochs != null && this.epoch - (this.lastActive.get(key) ?? 0) < keepEpochs) continue;
287
+ this.records.delete(key);
288
+ this.lastActive.delete(key);
289
+ dropped++;
290
+ }
291
+ if (dropped > 0) this.bump();
292
+ return dropped;
293
+ }
294
+ hasRecord(ref) {
295
+ return this.records.has(this.recordKey(ref));
296
+ }
297
+ /** Mark a key most-recently-used (Map keeps insertion order; re-insert to bump). No-op when unbounded. */
298
+ touch(key) {
299
+ if (!this.maxRecords) return;
300
+ const rec = this.records.get(key);
301
+ if (rec) {
302
+ this.records.delete(key);
303
+ this.records.set(key, rec);
304
+ }
305
+ }
306
+ /**
307
+ * Evict least-recently-used records past the cap (Map's first key is the
308
+ * oldest), skipping retained records — a record someone on screen reads is
309
+ * never the eviction victim, even if it's the coldest. If retained records
310
+ * alone exceed the cap, the cache temporarily runs over it.
311
+ */
312
+ evict() {
313
+ if (!this.maxRecords) return;
314
+ let over = this.records.size - this.maxRecords;
315
+ if (over <= 0) return;
316
+ for (const key of [...this.records.keys()]) {
317
+ if (over <= 0) break;
318
+ if (this.retainCounts.has(key)) continue;
319
+ this.records.delete(key);
320
+ over--;
321
+ }
322
+ }
323
+ /** Get-or-create the record map for a storage key. */
324
+ ensureRecord(key) {
325
+ this.stamp(key);
326
+ let rec = this.records.get(key);
327
+ if (!rec) {
328
+ rec = /* @__PURE__ */ new Map();
329
+ this.records.set(key, rec);
330
+ } else this.touch(key);
331
+ return rec;
332
+ }
333
+ getField(ref, fieldKey) {
334
+ const key = this.recordKey(ref);
335
+ const rec = this.records.get(key);
336
+ if (rec && rec.has(fieldKey)) {
337
+ this.touch(key);
338
+ this.stamp(key);
339
+ return {
340
+ status: "ready",
341
+ value: rec.get(fieldKey)
342
+ };
343
+ }
344
+ return { status: "missing" };
345
+ }
346
+ setField(ref, fieldKey, value) {
347
+ const key = this.recordKey(ref);
348
+ this.ensureRecord(key).set(fieldKey, value);
349
+ this.bumpRecord(key, fieldKey);
350
+ this.evict();
351
+ this.bump();
352
+ }
353
+ /** Merge a flat record of fields into the entity/path record. */
354
+ merge(ref, fields) {
355
+ const key = this.recordKey(ref);
356
+ const rec = this.ensureRecord(key);
357
+ for (const [k, v] of Object.entries(fields)) {
358
+ rec.set(k, v);
359
+ this.bumpField(key, k);
360
+ }
361
+ this.bumpRecord(key);
362
+ this.evict();
363
+ this.bump();
364
+ }
365
+ /** Drop a whole record (mutation invalidation). */
366
+ invalidate(ref) {
367
+ const key = this.recordKey(ref);
368
+ for (const fieldKey of this.records.get(key)?.keys() ?? []) this.bumpField(key, fieldKey);
369
+ this.records.delete(key);
370
+ this.bumpRecord(key);
371
+ this.bump();
372
+ }
373
+ /** Drop a single field so the next read re-fetches it. */
374
+ invalidateField(ref, fieldKey) {
375
+ const key = this.recordKey(ref);
376
+ this.records.get(key)?.delete(fieldKey);
377
+ this.bumpRecord(key, fieldKey);
378
+ this.bump();
379
+ }
380
+ /**
381
+ * Fold a serialized snapshot into THIS cache, field-by-field, WITHOUT replacing
382
+ * existing records and WITHOUT notifying. Returns whether anything was
383
+ * added/changed. The caller decides when to `notify()` — so a render-phase merge
384
+ * can write records (visible to synchronous reads) yet defer the subscriber bump
385
+ * to a commit-phase effect. Idempotent: re-absorbing the same snapshot is a no-op.
386
+ */
387
+ absorbRecords(snapshot) {
388
+ let changed = false;
389
+ for (const [key, fields] of Object.entries(snapshot)) {
390
+ const rec = this.ensureRecord(key);
391
+ let recordChanged = false;
392
+ for (const [k, v] of Object.entries(fields)) if (!rec.has(k) || rec.get(k) !== v) {
393
+ rec.set(k, v);
394
+ this.bumpField(key, k);
395
+ recordChanged = true;
396
+ }
397
+ if (recordChanged) {
398
+ this.bumpRecord(key);
399
+ changed = true;
400
+ }
401
+ }
402
+ this.evict();
403
+ return changed;
404
+ }
405
+ /** Serialize the whole cache (for hydration). */
406
+ snapshot() {
407
+ const out = {};
408
+ for (const [key, rec] of this.records) out[key] = Object.fromEntries(rec);
409
+ return out;
410
+ }
411
+ static fromSnapshot(snapshot, maxRecords) {
412
+ const cache = new GraphCache(maxRecords);
413
+ for (const [key, rec] of Object.entries(snapshot)) cache.records.set(key, new Map(Object.entries(rec)));
414
+ cache.evict();
415
+ return cache;
416
+ }
417
+ };
418
+ //#endregion
419
+ //#region src/normalize.ts
420
+ const defaultKeyOf = (_t, obj) => obj.id != null ? String(obj.id) : void 0;
421
+ function normalizeValue(cache, value, anchor, field, keyOf = defaultKeyOf, seen = /* @__PURE__ */ new WeakSet()) {
422
+ if (value === null || typeof value !== "object") return value;
423
+ if (seen.has(value)) throw new Error(`normalizeValue: circular reference at ${anchor}.${field} — cannot normalize cyclic data`);
424
+ seen.add(value);
425
+ try {
426
+ return normalizeNonCyclic(cache, value, anchor, field, keyOf, seen);
427
+ } finally {
428
+ seen.delete(value);
429
+ }
430
+ }
431
+ function normalizeNonCyclic(cache, value, anchor, field, keyOf, seen) {
432
+ if (Array.isArray(value)) return value.map((item, i) => normalizeValue(cache, item, anchor, `${field}.${i}`, keyOf, seen));
433
+ const obj = value;
434
+ const typename = typeof obj.__typename === "string" ? obj.__typename : void 0;
435
+ const identity = typename != null ? keyOf(typename, obj) : void 0;
436
+ if (typename != null && identity != null) {
437
+ const ref = {
438
+ __typename: typename,
439
+ id: identity
440
+ };
441
+ const entityAnchor = cache.recordKey(ref);
442
+ for (const [key, v] of Object.entries(obj)) cache.setField(ref, key, normalizeValue(cache, v, entityAnchor, key, keyOf, seen));
443
+ return ref;
444
+ }
445
+ const ref = { path: `${anchor}.${field}` };
446
+ for (const [key, v] of Object.entries(obj)) cache.setField(ref, key, normalizeValue(cache, v, anchor, `${field}.${key}`, keyOf, seen));
447
+ return ref;
448
+ }
449
+ /** Seed an operation result; returns each root field's ref for reading. */
450
+ function seedResult(cache, data, options = {}) {
451
+ const rootPath = options.rootPath ?? "Query";
452
+ const roots = {};
453
+ for (const [field, value] of Object.entries(data)) roots[field] = normalizeValue(cache, value, rootPath, field, options.keyOf);
454
+ return roots;
455
+ }
456
+ //#endregion
457
+ //#region src/runtime.ts
458
+ var GraphRuntime = class GraphRuntime {
459
+ options;
460
+ cache;
461
+ pending = /* @__PURE__ */ new Map();
462
+ queue = [];
463
+ flushScheduled = false;
464
+ constructor(options) {
465
+ this.options = options;
466
+ this.cache = options.cache ?? new GraphCache(options.maxCacheRecords);
467
+ }
468
+ /** Synchronous on hit; throws a (cached) promise on miss. */
469
+ readField(ref, fieldKey, debug) {
470
+ const got = this.cache.getField(ref, fieldKey);
471
+ if (got.status === "ready") return got.value;
472
+ this.reportMiss(ref, fieldKey, debug);
473
+ const pkey = this.pendingKey(ref, fieldKey);
474
+ const existing = this.pending.get(pkey);
475
+ if (existing) throw existing.promise;
476
+ const entry = this.makeDeferred();
477
+ this.pending.set(pkey, entry);
478
+ this.queue.push({
479
+ ref,
480
+ fieldKey
481
+ });
482
+ this.scheduleFlush();
483
+ throw entry.promise;
484
+ }
485
+ /** Seed a record's fields (e.g. from the compiled operation result). */
486
+ seed(ref, fields) {
487
+ this.cache.merge(ref, fields);
488
+ }
489
+ /** Normalize a full operation result into the cache; returns root refs. */
490
+ seedResult(data, options) {
491
+ return seedResult(this.cache, data, {
492
+ keyOf: this.options.keyOf,
493
+ ...options
494
+ });
495
+ }
496
+ /**
497
+ * Low-level pagination primitive: append a freshly-fetched page onto a cached
498
+ * connection. Normalizes the page's `nodes` and concats them after the existing
499
+ * ones, and (if present) replaces `pageInfo`. Every reader of the connection
500
+ * re-renders with the longer `nodes` array. This makes no assumptions about HOW
501
+ * the page was fetched or which cursor convention the schema uses — the app fetches
502
+ * the next page however it likes (the connection's ref is available via
503
+ * `selectionOf(value)`), then hands the page object here to merge it in.
504
+ */
505
+ appendConnection(connectionRef, page, mergeRefs) {
506
+ const keyOf = this.options.keyOf;
507
+ const anchor = this.cache.recordKey(connectionRef);
508
+ const existing = this.cache.getField(connectionRef, "nodes");
509
+ const prior = existing.status === "ready" && Array.isArray(existing.value) ? existing.value : [];
510
+ if (Array.isArray(page.nodes)) {
511
+ const fresh = page.nodes.map((n, i) => normalizeValue(this.cache, n, anchor, `nodes.${prior.length + i}`, keyOf));
512
+ const merged = mergeRefs ? mergeRefs(prior, fresh) : [...prior, ...fresh];
513
+ this.cache.setField(connectionRef, "nodes", [...merged]);
514
+ }
515
+ if (page.pageInfo != null) this.cache.setField(connectionRef, "pageInfo", normalizeValue(this.cache, page.pageInfo, anchor, "pageInfo", keyOf));
516
+ }
517
+ /** Invalidate a record (e.g. after a mutation) and clear its pending reads. */
518
+ invalidate(ref) {
519
+ const prefix = `${this.cache.recordKey(ref)}.`;
520
+ this.cache.invalidate(ref);
521
+ for (const key of [...this.pending.keys()]) if (key.startsWith(prefix)) this.pending.delete(key);
522
+ }
523
+ /** Serialize the cache for hydration across the server/client boundary. */
524
+ snapshot() {
525
+ return this.cache.snapshot();
526
+ }
527
+ /**
528
+ * Fold a snapshot into the live cache (write only, no notify); returns whether
529
+ * anything changed. Use for a per-navigation merge where the notify is deferred
530
+ * to a commit-phase effect (see `serialize.ts#absorbHydrationPayload`).
531
+ */
532
+ absorbRecords(snapshot) {
533
+ return this.cache.absorbRecords(snapshot);
534
+ }
535
+ /** Notify subscribers after one or more `absorbRecords` calls. */
536
+ notify() {
537
+ this.cache.notify();
538
+ }
539
+ /** Convenience: absorb a snapshot and notify if it changed (non-React callers). */
540
+ absorb(snapshot) {
541
+ const changed = this.cache.absorbRecords(snapshot);
542
+ if (changed) this.cache.notify();
543
+ return changed;
544
+ }
545
+ static hydrate(snapshot, options) {
546
+ return new GraphRuntime({
547
+ ...options,
548
+ cache: GraphCache.fromSnapshot(snapshot, options.maxCacheRecords)
549
+ });
550
+ }
551
+ reportMiss(ref, fieldKey, debug) {
552
+ const mode = this.options.unexpectedMissingField ?? "allow";
553
+ if (mode === "allow") return;
554
+ const where = debug?.component ? ` (read by ${debug.component})` : "";
555
+ const message = `Runtime graph field miss: ${this.cache.recordKey(ref)}.${fieldKey}${where} was not in the compiled operation.`;
556
+ if (mode === "error") throw new Error(message);
557
+ (this.options.onWarn ?? ((m) => console.warn(m)))(message);
558
+ }
559
+ scheduleFlush() {
560
+ if (this.flushScheduled) return;
561
+ this.flushScheduled = true;
562
+ (this.options.schedule ?? queueMicrotask)(() => void this.flush());
563
+ }
564
+ async flush() {
565
+ this.flushScheduled = false;
566
+ const misses = this.queue;
567
+ this.queue = [];
568
+ if (misses.length === 0) return;
569
+ try {
570
+ const results = await this.options.fetchMissing(misses);
571
+ for (const r of results) this.cache.setField(r.ref, r.fieldKey, r.value);
572
+ for (const miss of misses) {
573
+ const pkey = this.pendingKey(miss.ref, miss.fieldKey);
574
+ this.pending.get(pkey)?.resolve();
575
+ this.pending.delete(pkey);
576
+ }
577
+ } catch (error) {
578
+ for (const miss of misses) {
579
+ const pkey = this.pendingKey(miss.ref, miss.fieldKey);
580
+ this.pending.get(pkey)?.reject(error);
581
+ this.pending.delete(pkey);
582
+ }
583
+ }
584
+ }
585
+ /** Stable key for a pending (ref, field) read — also the `invalidate` prefix base. */
586
+ pendingKey(ref, fieldKey) {
587
+ return `${this.cache.recordKey(ref)}.${fieldKey}`;
588
+ }
589
+ makeDeferred() {
590
+ let resolve;
591
+ let reject;
592
+ return {
593
+ promise: new Promise((res, rej) => {
594
+ resolve = res;
595
+ reject = rej;
596
+ }),
597
+ resolve,
598
+ reject
599
+ };
600
+ }
601
+ };
602
+ //#endregion
603
+ //#region src/proxy.ts
604
+ /**
605
+ * Runtime graph proxies.
606
+ *
607
+ * The compiler statically infers what fields a route needs; this layer is what
608
+ * makes ordinary reads (`product.title`, `product.featuredImage?.url`,
609
+ * `collection.products({ first: 12 }).nodes`) actually *execute* at runtime.
610
+ *
611
+ * A graph value is a Proxy over a cache `GraphRef`. Property access routes
612
+ * through the Suspense-aware runtime:
613
+ * - scalar field -> `runtime.readField(ref, key)` (sync hit, or throws a promise)
614
+ * - object field -> the stored `GraphRef`, re-wrapped as a child proxy
615
+ * - list field -> an array of child proxies/scalars
616
+ * - callable field -> a function `(args) => value` (field arguments)
617
+ *
618
+ * The proxy is intentionally transparent: parent components pass it as a normal
619
+ * prop, child components read fields off it, and nothing in userland sees a ref,
620
+ * a selection object, or a promise (the promise is thrown to Suspense).
621
+ */
622
+ /** Escape-hatch / brand keys exposed on every graph proxy. */
623
+ const GRAPH_REF = Symbol.for("graph.ref");
624
+ const GRAPH_TYPE = Symbol.for("graph.type");
625
+ const GRAPH_TRAIL = Symbol.for("graph.trail");
626
+ /**
627
+ * Read tracking for fine-grained reactivity. Every field read records the record
628
+ * key it touched into the active tracker; the tracking hook's `useSyncExternalStore`
629
+ * snapshot is then a digest of just those records' versions.
630
+ *
631
+ * Attribution is primarily PER BINDING: `useGlean` binds the graph with its render's
632
+ * own `affected` set (see `GraphBinding.tracker`), so reads through that render's
633
+ * proxies record into that set directly — fiber-local, safe under concurrent/
634
+ * interleaved rendering. This ambient global is only a fallback for proxies created
635
+ * without a binding tracker (the server / isomorphic accessor), where no re-render
636
+ * depends on attribution.
637
+ */
638
+ let currentTracker = null;
639
+ /** Install (or clear) the active read tracker. Returns the previous one. */
640
+ function setReadTracker(tracker) {
641
+ const prev = currentTracker;
642
+ currentTracker = tracker;
643
+ return prev;
644
+ }
645
+ /** A value is a `GraphRef` if it carries entity identity or a path. */
646
+ function isGraphRef(value) {
647
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
648
+ const v = value;
649
+ return v.__typename != null && v.id != null || typeof v.path === "string";
650
+ }
651
+ function toArgValue(value) {
652
+ if (value === null) return {
653
+ kind: "literal",
654
+ value: null
655
+ };
656
+ if (Array.isArray(value)) return {
657
+ kind: "list",
658
+ items: value.map(toArgValue)
659
+ };
660
+ if (typeof value === "object") return {
661
+ kind: "object",
662
+ fields: Object.entries(value).map(([k, v]) => [k, toArgValue(v)])
663
+ };
664
+ return {
665
+ kind: "literal",
666
+ value
667
+ };
668
+ }
669
+ /** Plain runtime args (`{ first: 12 }`) -> IR `ArgMap`, reusing core's canonicalization. */
670
+ function toArgMap(args) {
671
+ if (!args) return [];
672
+ return Object.entries(args).map(([k, v]) => [k, toArgValue(v)]);
673
+ }
674
+ /**
675
+ * Candidate response keys for a (possibly callable) field, most-specific first.
676
+ * A callable field that coexisted with a differently-argued sibling was aliased
677
+ * by the merger (`url_transformMaxWidth300`); a lone callable field keeps its
678
+ * plain name. The proxy tries the alias key first, then the plain name, which
679
+ * is correct for both shapes without the proxy having to know about conflicts.
680
+ */
681
+ function responseKeyCandidates(name, argMap) {
682
+ if (argMap.length === 0) return [name];
683
+ const suffix = argAliasSuffix(argMap);
684
+ return suffix ? [`${name}_${suffix}`, name] : [name];
685
+ }
686
+ /** Read a field on a ref, resolving the right response key, and wrap object/list results. */
687
+ function readField(state, fieldName, args) {
688
+ const { binding, ref, type } = state;
689
+ const runtime = binding.getRuntime();
690
+ const fieldDef = binding.schema.getField(type, fieldName);
691
+ const candidates = responseKeyCandidates(fieldName, toArgMap(args));
692
+ let key = candidates[0];
693
+ for (const candidate of candidates) if (runtime.cache.getField(ref, candidate).status === "ready") {
694
+ key = candidate;
695
+ break;
696
+ }
697
+ const tracker = binding.tracker ?? currentTracker;
698
+ if (tracker) try {
699
+ tracker.add(runtime.cache.fieldTrackingKey(runtime.cache.recordKey(ref), key));
700
+ } catch {}
701
+ const raw = runtime.readField(ref, key);
702
+ const childTrail = [...state.trail, {
703
+ name: fieldName,
704
+ ...args ? { args } : {}
705
+ }];
706
+ return wrap(binding, raw, fieldDef?.type, childTrail);
707
+ }
708
+ /** Wrap a raw cache value as a child proxy (object), array of proxies (list), or scalar. */
709
+ function wrap(binding, value, declaredType, trail) {
710
+ if (Array.isArray(value)) return value.map((item) => wrap(binding, item, declaredType, trail));
711
+ if (isGraphRef(value)) return createGraphProxy(binding, value, value.__typename ?? declaredType ?? "Unknown", trail);
712
+ return value;
713
+ }
714
+ const handler = {
715
+ get(target, prop) {
716
+ const { state } = target;
717
+ if (prop === GRAPH_REF) return state.ref;
718
+ if (prop === GRAPH_TYPE) return state.type;
719
+ if (prop === GRAPH_TRAIL) return state.trail;
720
+ if (prop === "selection") return {
721
+ ref: state.ref,
722
+ type: state.type
723
+ };
724
+ if (typeof prop === "symbol") return void 0;
725
+ if (prop === "__typename") return state.ref.__typename ?? readField(state, "__typename");
726
+ const fieldName = prop;
727
+ const fieldDef = state.binding.schema.getField(state.type, fieldName);
728
+ if (fieldDef?.args && fieldDef.args.length > 0) return (args) => readField(state, fieldName, args);
729
+ return readField(state, fieldName);
730
+ },
731
+ has(target, prop) {
732
+ if (prop === GRAPH_REF || prop === GRAPH_TYPE || prop === GRAPH_TRAIL || prop === "selection") return true;
733
+ return typeof prop === "string" && !!target.state.binding.schema.getField(target.state.type, prop);
734
+ },
735
+ set() {
736
+ throw new Error("graph values are read-only");
737
+ },
738
+ ownKeys() {
739
+ return [];
740
+ }
741
+ };
742
+ function createGraphProxy(binding, ref, type, trail = []) {
743
+ return new Proxy({ state: {
744
+ binding,
745
+ ref,
746
+ type,
747
+ trail
748
+ } }, handler);
749
+ }
750
+ /** Read the hidden selection token off any graph proxy. */
751
+ function selectionOf(value) {
752
+ if (value && typeof value === "object" && GRAPH_REF in value) {
753
+ const v = value;
754
+ return {
755
+ ref: v[GRAPH_REF],
756
+ type: v[GRAPH_TYPE]
757
+ };
758
+ }
759
+ }
760
+ /** Read the root→value path off any graph proxy (for `usePaginated`/`fetchMore`). */
761
+ function trailOf(value) {
762
+ if (value && typeof value === "object" && GRAPH_TRAIL in value) return value[GRAPH_TRAIL];
763
+ }
764
+ function bindGraph(options) {
765
+ const binding = {
766
+ schema: options.schema,
767
+ getRuntime: options.getRuntime,
768
+ ...options.tracker ? { tracker: options.tracker } : {}
769
+ };
770
+ const queryType = options.schema.queryType;
771
+ const rootFields = options.schema.getType(queryType)?.fields ?? {};
772
+ const graph = {};
773
+ for (const [fieldName, fieldDef] of Object.entries(rootFields)) graph[fieldName] = (args) => {
774
+ const seeded = (typeof options.roots === "function" ? options.roots() : options.roots)?.[fieldName];
775
+ const trail = [{
776
+ name: fieldName,
777
+ ...args ? { args } : {}
778
+ }];
779
+ if (fieldDef.list) return (Array.isArray(seeded) ? seeded : []).map((item) => wrap(binding, item, fieldDef.type, trail));
780
+ return createGraphProxy(binding, isGraphRef(seeded) ? seeded : { path: `${queryType}.${fieldName}(${canonicalArgs(toArgMap(args))})` }, fieldDef.type, trail);
781
+ };
782
+ return graph;
783
+ }
784
+ //#endregion
785
+ //#region src/cache-resolve.ts
786
+ /**
787
+ * Cache-first resolution.
788
+ *
789
+ * Before executing an operation over the network, check whether the normalized
790
+ * cache already satisfies its entire selection. The link from a root call to its
791
+ * entity (`product(handle:"x") -> Product:123`) is persisted under the root
792
+ * record, so a re-run, a back-navigation, or data another writer already filled
793
+ * (mutation, subscription, sibling query) resolves with zero requests. Coverage
794
+ * is all-or-nothing here; partial gaps fall back to a full fetch (a future
795
+ * `node(id:)` patch could fetch only the missing fields).
796
+ */
797
+ const ROOT = "Query";
798
+ function resolveArg(v, vars) {
799
+ if (v.kind === "var") return {
800
+ kind: "literal",
801
+ value: vars[v.name] ?? null
802
+ };
803
+ if (v.kind === "list") return {
804
+ kind: "list",
805
+ items: v.items.map((i) => resolveArg(i, vars))
806
+ };
807
+ if (v.kind === "object") return {
808
+ kind: "object",
809
+ fields: v.fields.map(([k, fv]) => [k, resolveArg(fv, vars)])
810
+ };
811
+ return v;
812
+ }
813
+ /** Stable per-root-call key (`product(handle:"x")`), with operation variables substituted. */
814
+ function rootLinkKey(field, vars) {
815
+ const resolved = (field.args ?? []).map(([k, v]) => [k, resolveArg(v, vars)]);
816
+ return `${field.name}(${canonicalArgs(resolved)})`;
817
+ }
818
+ /** Record each root field's resolved ref so a later run finds the entity without a fetch. */
819
+ function persistRootLinks(cache, selection, vars, roots, rootPath = ROOT) {
820
+ const rec = { path: rootPath };
821
+ for (const field of selection.fields) {
822
+ const key = responseKey(field);
823
+ if (key in roots) cache.setField(rec, rootLinkKey(field, vars), roots[key]);
824
+ }
825
+ }
826
+ /** Does `ref`'s record cover every field in `selection` (recursively)? */
827
+ function covers(cache, ref, selection) {
828
+ for (const field of selection.fields) {
829
+ const got = cache.getField(ref, responseKey(field));
830
+ if (got.status !== "ready") return false;
831
+ if (field.selection && !coversValue(cache, got.value, field.selection)) return false;
832
+ }
833
+ for (const frag of selection.inlineFragments ?? []) if (ref.__typename === frag.onType && !covers(cache, ref, frag.selection)) return false;
834
+ return true;
835
+ }
836
+ function coversValue(cache, value, selection) {
837
+ if (value == null) return true;
838
+ if (Array.isArray(value)) return value.every((v) => coversValue(cache, v, selection));
839
+ if (isGraphRef(value)) return covers(cache, value, selection);
840
+ return true;
841
+ }
842
+ /**
843
+ * Try to satisfy an operation entirely from cache. `covered` is true only when
844
+ * every root link exists and every selected field beneath it is present.
845
+ */
846
+ function resolveFromCache(cache, selection, vars, rootPath = ROOT) {
847
+ const rec = { path: rootPath };
848
+ const roots = {};
849
+ for (const field of selection.fields) {
850
+ const link = cache.getField(rec, rootLinkKey(field, vars));
851
+ if (link.status !== "ready") return {
852
+ covered: false,
853
+ roots: {}
854
+ };
855
+ const ref = link.value;
856
+ if (ref == null) {
857
+ roots[responseKey(field)] = null;
858
+ continue;
859
+ }
860
+ if (field.selection && (!isGraphRef(ref) || !covers(cache, ref, field.selection))) return {
861
+ covered: false,
862
+ roots: {}
863
+ };
864
+ roots[responseKey(field)] = ref;
865
+ }
866
+ return {
867
+ covered: true,
868
+ roots
869
+ };
870
+ }
871
+ //#endregion
872
+ //#region src/route.ts
873
+ /** Execute a compiled operation and seed the runtime cache (steps 2–5 of the route flow). */
874
+ async function runRoute(args) {
875
+ const variables = args.operation.variables(args.routeContext);
876
+ const selection = args.operation.selection;
877
+ if ((args.options?.cacheFirst ?? true) && selection) {
878
+ const hit = resolveFromCache(args.runtime.cache, selection, variables);
879
+ if (hit.covered) return {
880
+ variables,
881
+ roots: hit.roots
882
+ };
883
+ }
884
+ const result = await args.adapter.execute({
885
+ name: args.operation.name,
886
+ kind: args.operation.kind,
887
+ document: args.operation.document
888
+ }, variables, args.context);
889
+ const roots = result.data ? args.runtime.seedResult(result.data) : {};
890
+ if (selection && result.data) persistRootLinks(args.runtime.cache, selection, variables, roots);
891
+ return {
892
+ variables,
893
+ roots,
894
+ errors: result.errors
895
+ };
896
+ }
897
+ /**
898
+ * Re-run an operation against the network, bypassing cache-first, and re-seed.
899
+ * The re-seed writes through the cache, bumping its version and notifying
900
+ * subscribers — so a `useSyncExternalStore` (`cache.subscribe`) re-renders the
901
+ * UI with the fresh data. Use for an explicit "Refresh" / post-mutation refetch.
902
+ */
903
+ function refetch(args) {
904
+ return runRoute({
905
+ ...args,
906
+ options: { cacheFirst: false }
907
+ });
908
+ }
909
+ //#endregion
910
+ //#region src/scope.ts
911
+ var GraphScope = class {
912
+ singleton;
913
+ als;
914
+ constructor(als) {
915
+ this.als = als;
916
+ }
917
+ /**
918
+ * Attach an AsyncLocalStorage after construction — for isomorphic frameworks
919
+ * (e.g. React Router) where the *same* `graph` accessor module loads in both
920
+ * bundles: construct `new GraphScope()` in a universal, client-safe module (no
921
+ * `node:async_hooks`), then a server-only module calls `attachAls(...)` to
922
+ * upgrade it to per-request ALS isolation. `run`/`current` read `als`
923
+ * dynamically, so the upgrade takes effect immediately; the client keeps using
924
+ * the singleton set by `set()`.
925
+ */
926
+ attachAls(als) {
927
+ this.als = als;
928
+ }
929
+ /** The active graph, or throw a clear error if read outside any scope. */
930
+ current() {
931
+ const active = this.als?.getStore() ?? this.singleton;
932
+ if (!active) throw new Error("No active graph runtime. On the server wrap rendering in scope.run(active, fn); on the client call scope.set(active) after hydration.");
933
+ return active;
934
+ }
935
+ /** Run `fn` with `active` as the request-scoped runtime (server). */
936
+ run(active, fn) {
937
+ if (this.als) return this.als.run(active, fn);
938
+ const prev = this.singleton;
939
+ this.singleton = active;
940
+ try {
941
+ return fn();
942
+ } finally {
943
+ this.singleton = prev;
944
+ }
945
+ }
946
+ /** Install the active graph as a singleton (client, post-hydration). */
947
+ set(active) {
948
+ this.singleton = active;
949
+ }
950
+ };
951
+ /**
952
+ * Pair a {@link GraphScope} with a zero-arg resolver — the framework-agnostic
953
+ * binding for the generated accessor. An app exports `activeGraph` and points
954
+ * `@gleanql/vite`'s `requestScope: { import: "activeGraph", from: "..." }` at it,
955
+ * then wraps server rendering in `scope.run(active, fn)` (or
956
+ * `integration.runInScope`). Pass an `AsyncLocalStorage` to isolate concurrent
957
+ * server requests; omit it for the client singleton.
958
+ *
959
+ * ```ts
960
+ * import { AsyncLocalStorage } from "node:async_hooks";
961
+ * export const { scope, activeGraph } = bindScope(new AsyncLocalStorage());
962
+ * ```
963
+ */
964
+ function bindScope(als) {
965
+ const scope = new GraphScope(als);
966
+ return {
967
+ scope,
968
+ activeGraph: () => scope.current()
969
+ };
970
+ }
971
+ //#endregion
972
+ //#region src/mutation.ts
973
+ /**
974
+ * A reversible batch of cache writes. Optimistic updates record the prior value
975
+ * of every field they touch so the whole batch can be rolled back if the
976
+ * mutation fails (transport error or `userErrors`).
977
+ */
978
+ var MutationTransaction = class {
979
+ cache;
980
+ undo = [];
981
+ constructor(cache) {
982
+ this.cache = cache;
983
+ }
984
+ /** Optimistically write a field, remembering how to undo it. */
985
+ set(ref, fieldKey, value) {
986
+ const before = this.cache.getField(ref, fieldKey);
987
+ if (before.status === "ready") {
988
+ const prev = before.value;
989
+ this.undo.push(() => this.cache.setField(ref, fieldKey, prev));
990
+ } else this.undo.push(() => this.cache.invalidateField(ref, fieldKey));
991
+ this.cache.setField(ref, fieldKey, value);
992
+ }
993
+ /** Roll back every write in reverse order. */
994
+ rollback() {
995
+ for (let i = this.undo.length - 1; i >= 0; i--) this.undo[i]();
996
+ this.undo.length = 0;
997
+ }
998
+ };
999
+ /**
1000
+ * Execute a mutation, normalize its result into the cache, surface userErrors,
1001
+ * and apply optimistic/invalidation policy. The returned promise never rejects
1002
+ * for logical failures — inspect `ok`/`userErrors`/`errors`.
1003
+ */
1004
+ async function runMutation(options) {
1005
+ const { runtime, adapter, context, variables } = options;
1006
+ const tx = new MutationTransaction(runtime.cache);
1007
+ if (options.optimistic) options.optimistic(tx);
1008
+ let result;
1009
+ try {
1010
+ result = await adapter.execute({
1011
+ name: options.operation.name,
1012
+ kind: "mutation",
1013
+ document: options.operation.document
1014
+ }, variables, context);
1015
+ } catch (error) {
1016
+ tx.rollback();
1017
+ return {
1018
+ userErrors: [],
1019
+ errors: [{ message: errorMessage(error) }],
1020
+ ok: false
1021
+ };
1022
+ }
1023
+ if (result.errors && result.errors.length > 0) {
1024
+ tx.rollback();
1025
+ return {
1026
+ userErrors: [],
1027
+ errors: result.errors,
1028
+ ok: false
1029
+ };
1030
+ }
1031
+ const data = result.data;
1032
+ const userErrors = data ? extractUserErrors(data) : [];
1033
+ if (userErrors.length > 0) {
1034
+ tx.rollback();
1035
+ return {
1036
+ data,
1037
+ userErrors,
1038
+ ok: false
1039
+ };
1040
+ }
1041
+ if (data) runtime.seedResult(data);
1042
+ if (options.update && data) options.update(data, tx);
1043
+ if (options.invalidate && data) for (const target of options.invalidate(data)) {
1044
+ const ref = toRef(target);
1045
+ if (ref) runtime.invalidate(ref);
1046
+ }
1047
+ return {
1048
+ data,
1049
+ userErrors: [],
1050
+ ok: true
1051
+ };
1052
+ }
1053
+ /** Invalidate a record by graph value (proxy) or raw ref — next read re-fetches. */
1054
+ function invalidateValue(runtime, value) {
1055
+ const ref = toRef(value);
1056
+ if (ref) runtime.invalidate(ref);
1057
+ }
1058
+ /** Collect `userErrors` from each top-level mutation payload in the result. */
1059
+ function extractUserErrors(data) {
1060
+ const out = [];
1061
+ for (const value of Object.values(data)) if (value && typeof value === "object" && !Array.isArray(value)) {
1062
+ const ue = value.userErrors;
1063
+ if (Array.isArray(ue)) {
1064
+ for (const e of ue) if (e && typeof e === "object") out.push(e);
1065
+ }
1066
+ }
1067
+ return out;
1068
+ }
1069
+ function toRef(value) {
1070
+ const selection = selectionOf(value);
1071
+ if (selection) return selection.ref;
1072
+ if (value && typeof value === "object") {
1073
+ const v = value;
1074
+ if (v.__typename != null && v.id != null || typeof v.path === "string") return v;
1075
+ }
1076
+ }
1077
+ /** One rule for stringifying unknown errors, shared across hooks and transports. */
1078
+ function errorMessage(error) {
1079
+ return error instanceof Error ? error.message : String(error);
1080
+ }
1081
+ //#endregion
1082
+ //#region src/mutator.ts
1083
+ function createMutator(options) {
1084
+ const mutate = {};
1085
+ for (const [name, operation] of Object.entries(options.operations)) {
1086
+ if (operation.kind !== "mutation") continue;
1087
+ const mutationOp = {
1088
+ name: operation.name,
1089
+ kind: "mutation",
1090
+ document: operation.document
1091
+ };
1092
+ mutate[name] = (variables, opts) => runMutation({
1093
+ operation: mutationOp,
1094
+ variables,
1095
+ adapter: options.adapter,
1096
+ context: options.context,
1097
+ runtime: options.runtime,
1098
+ ...opts
1099
+ });
1100
+ }
1101
+ return mutate;
1102
+ }
1103
+ //#endregion
1104
+ //#region src/context.ts
1105
+ /**
1106
+ * Build the route/request context from a RequestInfo. `params` and `search` come
1107
+ * from the URL; everything else is contributed by `options.context`. The raw
1108
+ * `request` is included for header derivation but is *not* serialized to the
1109
+ * client (see `serializeGraph`).
1110
+ */
1111
+ function buildRouteContext(requestInfo, options = {}) {
1112
+ const url = new URL(requestInfo.request.url);
1113
+ return {
1114
+ ...options.context?.(requestInfo) ?? {},
1115
+ params: requestInfo.params,
1116
+ search: url.searchParams,
1117
+ request: requestInfo.request
1118
+ };
1119
+ }
1120
+ //#endregion
1121
+ //#region src/integration.ts
1122
+ /**
1123
+ * RedwoodSDK integration.
1124
+ *
1125
+ * Answers the four questions the brief asks of a framework adapter:
1126
+ * - Which operation drives this entrypoint? -> `resolveOperationName` / explicit name
1127
+ * - How do we read params/search/request/env? -> `buildRouteContext`
1128
+ * - How do we preload + seed? -> `runRoute` into a fresh per-request cache
1129
+ * - How do we expose the graph to components? -> attach a bound graph to `ctx`
1130
+ *
1131
+ * Per request: pick the operation, compute variables, execute via the client
1132
+ * adapter, seed a fresh cache, and attach `{ runtime, graph, ... }` to
1133
+ * `requestInfo.ctx` so Pages/components read graph fields with cache hits. Unseeded
1134
+ * (lazy) fields fall through to the Suspense runtime.
1135
+ */
1136
+ const CTX_KEY = "__graph";
1137
+ function createGraphIntegration(options) {
1138
+ const keyOf = (typename, obj) => options.schema.identityOf(typename, obj);
1139
+ function makeRuntime(requestContext) {
1140
+ return new GraphRuntime({
1141
+ keyOf,
1142
+ unexpectedMissingField: options.unexpectedMissingField,
1143
+ onWarn: options.onWarn,
1144
+ fetchMissing: async (misses) => options.fetchMissing ? options.fetchMissing(misses, requestContext) : missesUnresolved(misses)
1145
+ });
1146
+ }
1147
+ async function preload(requestInfo, operationName) {
1148
+ const name = operationName ?? options.resolveOperationName?.(requestInfo);
1149
+ const operation = name ? options.operations[name] : void 0;
1150
+ if (!operation) return void 0;
1151
+ const requestContext = buildRouteContext(requestInfo, options);
1152
+ const runtime = makeRuntime(requestContext);
1153
+ const { variables, roots, errors } = await runRoute({
1154
+ operation,
1155
+ routeContext: requestContext,
1156
+ adapter: options.adapter,
1157
+ context: requestContext,
1158
+ runtime
1159
+ });
1160
+ const active = {
1161
+ runtime,
1162
+ graph: bindGraph({
1163
+ schema: options.schema,
1164
+ getRuntime: () => runtime,
1165
+ roots
1166
+ }),
1167
+ mutate: createMutator({
1168
+ operations: options.operations,
1169
+ adapter: options.adapter,
1170
+ runtime,
1171
+ context: requestContext
1172
+ }),
1173
+ roots,
1174
+ operation,
1175
+ variables,
1176
+ requestContext,
1177
+ ...errors ? { errors } : {}
1178
+ };
1179
+ requestInfo.ctx[CTX_KEY] = active;
1180
+ return active;
1181
+ }
1182
+ function getActive(requestInfo) {
1183
+ return requestInfo.ctx[CTX_KEY];
1184
+ }
1185
+ async function refetch(requestInfo, operationName) {
1186
+ const active = getActive(requestInfo);
1187
+ if (!active) return;
1188
+ const operation = options.operations[operationName ?? active.operation.name];
1189
+ if (!operation) return;
1190
+ await runRoute({
1191
+ operation,
1192
+ routeContext: active.requestContext,
1193
+ adapter: options.adapter,
1194
+ context: active.requestContext,
1195
+ runtime: active.runtime,
1196
+ options: { cacheFirst: false }
1197
+ });
1198
+ }
1199
+ function getGraph(requestInfo) {
1200
+ const active = getActive(requestInfo);
1201
+ if (!active) throw new Error("No graph attached to this request. Call integration.preload(requestInfo) before rendering (e.g. in a middleware or at the top of the Page).");
1202
+ return active.graph;
1203
+ }
1204
+ function getMutator(requestInfo) {
1205
+ const active = getActive(requestInfo);
1206
+ if (!active) throw new Error("No graph attached to this request. Call integration.preload(requestInfo) first.");
1207
+ return active.mutate;
1208
+ }
1209
+ function invalidate(requestInfo, value) {
1210
+ const active = getActive(requestInfo);
1211
+ if (active) invalidateValue(active.runtime, value);
1212
+ }
1213
+ function runInScope(requestInfo, fn) {
1214
+ const active = getActive(requestInfo);
1215
+ if (!active || !options.scope) return fn();
1216
+ return options.scope.run({
1217
+ runtime: active.runtime,
1218
+ graph: active.graph
1219
+ }, fn);
1220
+ }
1221
+ return {
1222
+ preload,
1223
+ getGraph,
1224
+ getMutator,
1225
+ invalidate,
1226
+ getActive,
1227
+ refetch,
1228
+ runInScope
1229
+ };
1230
+ }
1231
+ /** Default missing-field resolution: leave each miss undefined (hybrid allow/warn). */
1232
+ function missesUnresolved(misses) {
1233
+ return misses.map((m) => ({
1234
+ ref: m.ref,
1235
+ fieldKey: m.fieldKey,
1236
+ value: void 0
1237
+ }));
1238
+ }
1239
+ //#endregion
1240
+ //#region src/serialize.ts
1241
+ /** Build the JSON-safe hydration payload from an active request. */
1242
+ function serializeGraph(active, options = {}) {
1243
+ const allow = new Set(options.clientSafeContext ?? []);
1244
+ const context = {};
1245
+ for (const key of allow) if (key in active.requestContext) context[key] = active.requestContext[key];
1246
+ return {
1247
+ operationName: active.operation.name,
1248
+ variables: active.variables,
1249
+ snapshot: active.runtime.snapshot(),
1250
+ roots: active.roots,
1251
+ context
1252
+ };
1253
+ }
1254
+ const DEFAULT_GLOBAL = "__GRAPH_STATE__";
1255
+ /**
1256
+ * Render a `<script>` that publishes the payload on `window[globalKey]` for
1257
+ * client hydration. JSON is escaped so it cannot break out of the script element
1258
+ * or be interpreted as HTML (`<`, `>`, `&`, U+2028/U+2029).
1259
+ */
1260
+ function renderGraphHydrationScript(payload, options = {}) {
1261
+ return `<script${options.nonce ? ` nonce="${options.nonce}"` : ""}>${graphHydrationScriptContent(payload, options.globalKey)}<\/script>`;
1262
+ }
1263
+ /**
1264
+ * Just the inner JS of the hydration script (no `<script>` wrapper), for JSX
1265
+ * hosts that inject via `dangerouslySetInnerHTML` and set the nonce themselves.
1266
+ * JSON is escaped so it can't break out of the script element or be parsed as HTML.
1267
+ */
1268
+ function graphHydrationScriptContent(payload, globalKey = DEFAULT_GLOBAL) {
1269
+ const json = JSON.stringify(payload).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
1270
+ return `window[${JSON.stringify(globalKey)}]=${json}`;
1271
+ }
1272
+ /** Read the hydration payload published by `renderGraphHydrationScript` (client). */
1273
+ function readGraphHydrationPayload(globalKey = DEFAULT_GLOBAL) {
1274
+ return globalThis[globalKey];
1275
+ }
1276
+ /**
1277
+ * Rebuild the runtime + bound graph on the client from a hydration payload.
1278
+ * Reads present in the snapshot resolve synchronously; missing fields suspend and
1279
+ * fetch through the client adapter.
1280
+ */
1281
+ function hydrateGraph(payload, options) {
1282
+ const runtime = GraphRuntime.hydrate(payload.snapshot, {
1283
+ keyOf: (typename, obj) => options.schema.identityOf(typename, obj),
1284
+ unexpectedMissingField: options.unexpectedMissingField,
1285
+ onWarn: options.onWarn,
1286
+ fetchMissing: async (misses) => options.fetchMissing ? options.fetchMissing(misses, payload.context) : misses.map((m) => ({
1287
+ ref: m.ref,
1288
+ fieldKey: m.fieldKey,
1289
+ value: void 0
1290
+ }))
1291
+ });
1292
+ const graph = bindGraph({
1293
+ schema: options.schema,
1294
+ getRuntime: () => runtime,
1295
+ roots: payload.roots
1296
+ });
1297
+ const active = {
1298
+ runtime,
1299
+ graph,
1300
+ roots: payload.roots,
1301
+ context: payload.context
1302
+ };
1303
+ options.scope?.set({
1304
+ runtime,
1305
+ graph
1306
+ });
1307
+ return active;
1308
+ }
1309
+ /**
1310
+ * Render-phase merge: fold a payload's snapshot into a live runtime, write-only
1311
+ * (no subscriber notify — the caller bumps in a commit-phase effect). Idempotent.
1312
+ * Returns whether anything changed.
1313
+ */
1314
+ function absorbHydrationPayload(runtime, payload) {
1315
+ return runtime.absorbRecords(payload.snapshot);
1316
+ }
1317
+ /** Derive the current-page pointer from a payload (drives `refresh()`). */
1318
+ function pagePointer(payload) {
1319
+ return {
1320
+ operationName: payload.operationName,
1321
+ variables: payload.variables,
1322
+ context: payload.context,
1323
+ roots: payload.roots
1324
+ };
1325
+ }
1326
+ //#endregion
1327
+ //#region src/adapter-ws.ts
1328
+ function toErrors(error) {
1329
+ if (Array.isArray(error)) return error.map((e) => ({ message: String(e?.message ?? e) }));
1330
+ if (error instanceof Error) return [{ message: error.message }];
1331
+ if (error && typeof error === "object" && "reason" in error) return [{ message: String(error.reason) || "subscription socket closed" }];
1332
+ return [{ message: String(error) }];
1333
+ }
1334
+ function createGraphWsAdapter(options) {
1335
+ const { client } = options;
1336
+ const payloadFor = (operation, variables, context) => {
1337
+ const extensions = options.extensions?.(context);
1338
+ return {
1339
+ query: operation.document,
1340
+ variables: variables ?? {},
1341
+ operationName: operation.name,
1342
+ ...extensions && Object.keys(extensions).length > 0 ? { extensions } : {}
1343
+ };
1344
+ };
1345
+ return {
1346
+ execute(operation, variables, context) {
1347
+ return new Promise((resolve) => {
1348
+ let settled = false;
1349
+ let dispose;
1350
+ const settle = (r) => {
1351
+ if (settled) return;
1352
+ settled = true;
1353
+ resolve(r);
1354
+ dispose?.();
1355
+ };
1356
+ dispose = client.subscribe(payloadFor(operation, variables, context), {
1357
+ next: (value) => settle(value),
1358
+ error: (error) => settle({ errors: toErrors(error) }),
1359
+ complete: () => settle({})
1360
+ });
1361
+ if (settled) dispose?.();
1362
+ });
1363
+ },
1364
+ subscribe(operation, variables, context) {
1365
+ return { [Symbol.asyncIterator]() {
1366
+ let dispose;
1367
+ const it = pushPullIterator(() => dispose?.());
1368
+ dispose = client.subscribe(payloadFor(operation, variables, context), {
1369
+ next: (value) => it.push(value),
1370
+ error: (error) => {
1371
+ it.push({ errors: toErrors(error) });
1372
+ it.finish();
1373
+ },
1374
+ complete: () => it.finish()
1375
+ });
1376
+ return it;
1377
+ } };
1378
+ }
1379
+ };
1380
+ }
1381
+ //#endregion
1382
+ //#region src/persisted.ts
1383
+ function createPersistedResolver(operations, options = {}) {
1384
+ const byHash = /* @__PURE__ */ new Map();
1385
+ const documents = /* @__PURE__ */ new Set();
1386
+ for (const op of Object.values(operations)) {
1387
+ documents.add(op.document);
1388
+ if (op.hash) byHash.set(op.hash, op.document);
1389
+ }
1390
+ return (body) => {
1391
+ const hash = body.extensions?.persistedQuery?.sha256Hash;
1392
+ if (hash) {
1393
+ const document = byHash.get(hash);
1394
+ if (document) return {
1395
+ kind: "ok",
1396
+ document
1397
+ };
1398
+ if (body.query) return documents.has(body.query) || options.allowUnpersisted ? {
1399
+ kind: "ok",
1400
+ document: body.query
1401
+ } : { kind: "rejected" };
1402
+ return { kind: "not-found" };
1403
+ }
1404
+ if (body.query) return documents.has(body.query) || options.allowUnpersisted ? {
1405
+ kind: "ok",
1406
+ document: body.query
1407
+ } : { kind: "rejected" };
1408
+ return { kind: "rejected" };
1409
+ };
1410
+ }
1411
+ //#endregion
1412
+ export { GRAPH_REF, GRAPH_TRAIL, GRAPH_TYPE, GraphCache, GraphRuntime, GraphScope, MutationTransaction, absorbHydrationPayload, bindGraph, bindScope, buildRouteContext, createFetchAdapter, createGraphIntegration, createGraphProxy, createGraphWsAdapter, createMutator, createPersistedResolver, errorMessage, graphHydrationScriptContent, hydrateGraph, invalidateValue, isGraphRef, normalizeValue, pagePointer, persistRootLinks, pushPullIterator, readGraphHydrationPayload, refetch, renderGraphHydrationScript, resolveFromCache, responseKeyCandidates, result, runMutation, runRoute, seedResult, selectionOf, serializeGraph, setReadTracker, toArgMap, trailOf };