@mearie/core 0.1.0 → 0.2.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.
@@ -1,3 +1,5 @@
1
+ import { C as mergeMap, S as filter, a as finalize, b as merge, c as share, d as map, i as fromValue, l as takeUntil, m as collect, n as fromSubscription, o as initialize, r as makeSubject, s as switchMap, t as make, u as take, v as fromArray, w as pipe, x as fromPromise, y as tap } from "./make-C7I1YIXm.mjs";
2
+
1
3
  //#region src/errors.ts
2
4
  /**
3
5
  *
@@ -72,253 +74,14 @@ const isAggregatedError = (error) => {
72
74
  return error instanceof AggregatedError;
73
75
  };
74
76
 
75
- //#endregion
76
- //#region src/stream/pipe.ts
77
- /**
78
- * @param source - The source stream.
79
- * @param operators - The operators to apply.
80
- * @returns The result of the last operator.
81
- */
82
- function pipe(source, ...operators) {
83
- return operators.reduce((src, operator) => operator(src), source);
84
- }
85
-
86
- //#endregion
87
- //#region src/stream/operators/share.ts
88
- /**
89
- * Shares a single source across multiple subscribers (multicast).
90
- * The source is only executed once, and all subscribers receive the same values.
91
- * This is essential for deduplication and caching scenarios.
92
- * @returns An operator that shares the source.
93
- */
94
- const share = () => {
95
- return (source) => {
96
- const sinks = [];
97
- let subscription = null;
98
- let started = false;
99
- let completed = false;
100
- return (sink) => {
101
- if (completed) {
102
- sink.complete();
103
- return { unsubscribe() {} };
104
- }
105
- sinks.push(sink);
106
- if (!started) {
107
- started = true;
108
- subscription = source({
109
- next(value) {
110
- for (const s of [...sinks]) {
111
- if (completed) break;
112
- s.next(value);
113
- }
114
- },
115
- complete() {
116
- if (!completed) {
117
- completed = true;
118
- for (const s of [...sinks]) s.complete();
119
- sinks.length = 0;
120
- }
121
- }
122
- });
123
- }
124
- return { unsubscribe() {
125
- const idx = sinks.indexOf(sink);
126
- if (idx !== -1) sinks.splice(idx, 1);
127
- if (sinks.length === 0 && subscription) {
128
- subscription.unsubscribe();
129
- subscription = null;
130
- started = false;
131
- }
132
- } };
133
- };
134
- };
135
- };
136
-
137
- //#endregion
138
- //#region src/exchanges/compose.ts
139
- const composeExchange = (options) => {
140
- const { exchanges } = options;
141
- return ({ forward, client }) => {
142
- return exchanges.reduceRight((forward$1, exchange) => {
143
- return (ops$) => {
144
- return pipe(ops$, share(), exchange({
145
- forward: forward$1,
146
- client
147
- }), share());
148
- };
149
- }, forward);
150
- };
151
- };
152
-
153
- //#endregion
154
- //#region src/stream/operators/merge-map.ts
155
- /**
156
- * Maps each value to a source and flattens all sources into a single output source.
157
- * Similar to flatMap. Values from all inner sources are merged concurrently.
158
- * @param fn - Function that returns a source for each value.
159
- * @returns An operator that flattens mapped sources.
160
- */
161
- const mergeMap = (fn) => {
162
- return (source) => {
163
- return (sink) => {
164
- let outerCompleted = false;
165
- let activeInner = 0;
166
- let ended = false;
167
- const innerSubscriptions = [];
168
- const checkComplete = () => {
169
- if (outerCompleted && activeInner === 0 && !ended) {
170
- ended = true;
171
- sink.complete();
172
- }
173
- };
174
- const outerSubscription = source({
175
- next(value) {
176
- if (ended) return;
177
- activeInner++;
178
- const innerSubscription = fn(value)({
179
- next(innerValue) {
180
- if (!ended) sink.next(innerValue);
181
- },
182
- complete() {
183
- activeInner--;
184
- checkComplete();
185
- }
186
- });
187
- innerSubscriptions.push(innerSubscription);
188
- },
189
- complete() {
190
- outerCompleted = true;
191
- checkComplete();
192
- }
193
- });
194
- return { unsubscribe() {
195
- ended = true;
196
- outerSubscription.unsubscribe();
197
- for (const sub of innerSubscriptions) sub.unsubscribe();
198
- innerSubscriptions.length = 0;
199
- } };
200
- };
201
- };
202
- };
203
-
204
- //#endregion
205
- //#region src/stream/operators/filter.ts
206
- function filter(predicate) {
207
- return (source) => {
208
- return (sink) => {
209
- return source({
210
- next(value) {
211
- if (predicate(value)) sink.next(value);
212
- },
213
- complete() {
214
- sink.complete();
215
- }
216
- });
217
- };
218
- };
219
- }
220
-
221
- //#endregion
222
- //#region src/stream/sources/from-promise.ts
223
- const fromPromise = (promise) => {
224
- return (sink) => {
225
- let cancelled = false;
226
- promise.then((value) => {
227
- if (!cancelled) {
228
- sink.next(value);
229
- sink.complete();
230
- }
231
- }, () => {
232
- if (!cancelled) sink.complete();
233
- });
234
- return { unsubscribe() {
235
- cancelled = true;
236
- } };
237
- };
238
- };
239
-
240
- //#endregion
241
- //#region src/stream/operators/merge.ts
242
- /**
243
- * Merges multiple sources into a single source.
244
- * Values are emitted as soon as they arrive from any source.
245
- * Completes when all sources complete.
246
- * @param sources - The sources to merge.
247
- * @returns A merged source.
248
- */
249
- const merge = (...sources) => {
250
- return (sink) => {
251
- if (sources.length === 0) {
252
- sink.complete();
253
- return { unsubscribe() {} };
254
- }
255
- let activeCount = sources.length;
256
- const subscriptions = [];
257
- let ended = false;
258
- let ready = false;
259
- const buffer = [];
260
- const checkComplete = () => {
261
- if (activeCount === 0 && !ended) {
262
- ended = true;
263
- sink.complete();
264
- }
265
- };
266
- for (const source of sources) {
267
- const subscription = source({
268
- next(value) {
269
- if (!ended) if (ready) sink.next(value);
270
- else buffer.push(value);
271
- },
272
- complete() {
273
- activeCount--;
274
- if (ready) checkComplete();
275
- }
276
- });
277
- subscriptions.push(subscription);
278
- }
279
- ready = true;
280
- for (const value of buffer) if (!ended) sink.next(value);
281
- buffer.length = 0;
282
- checkComplete();
283
- return { unsubscribe() {
284
- ended = true;
285
- for (const sub of subscriptions) sub.unsubscribe();
286
- } };
287
- };
288
- };
289
-
290
- //#endregion
291
- //#region src/stream/operators/tap.ts
292
- /**
293
- * Executes a side effect for each value without modifying the stream.
294
- * Useful for debugging, logging, or triggering side effects.
295
- * @param fn - The side effect function.
296
- * @returns An operator that taps into the stream.
297
- */
298
- const tap = (fn) => {
299
- return (source) => {
300
- return (sink) => {
301
- return source({
302
- next(value) {
303
- fn(value);
304
- sink.next(value);
305
- },
306
- complete() {
307
- sink.complete();
308
- }
309
- });
310
- };
311
- };
312
- };
313
-
314
77
  //#endregion
315
78
  //#region src/exchanges/http.ts
316
- const executeFetch = async ({ url, fetchOptions, operation, signal }) => {
79
+ const executeFetch = async ({ url, fetchFn, fetchOptions, operation, signal }) => {
317
80
  const { artifact, variables } = operation;
318
81
  let response;
319
82
  try {
320
83
  await Promise.resolve();
321
- response = await fetch(url, {
84
+ response = await fetchFn(url, {
322
85
  method: "POST",
323
86
  mode: fetchOptions.mode,
324
87
  credentials: fetchOptions.credentials,
@@ -373,9 +136,10 @@ const executeFetch = async ({ url, fetchOptions, operation, signal }) => {
373
136
  };
374
137
  };
375
138
  const httpExchange = (options) => {
376
- const { url, headers, mode, credentials } = options;
377
- return ({ forward }) => {
378
- return (ops$) => {
139
+ const { url, headers, mode, credentials, fetch: fetchFn = globalThis.fetch } = options;
140
+ return ({ forward }) => ({
141
+ name: "http",
142
+ io: (ops$) => {
379
143
  const inflight = /* @__PURE__ */ new Map();
380
144
  return merge(pipe(ops$, filter((op) => op.variant === "request" && (op.artifact.kind === "query" || op.artifact.kind === "mutation")), mergeMap((op) => {
381
145
  inflight.get(op.key)?.abort();
@@ -383,6 +147,7 @@ const httpExchange = (options) => {
383
147
  inflight.set(op.key, controller);
384
148
  return fromPromise(executeFetch({
385
149
  url,
150
+ fetchFn,
386
151
  fetchOptions: {
387
152
  mode,
388
153
  credentials,
@@ -400,8 +165,8 @@ const httpExchange = (options) => {
400
165
  inflight.delete(op.key);
401
166
  }
402
167
  }), forward));
403
- };
404
- };
168
+ }
169
+ });
405
170
  };
406
171
 
407
172
  //#endregion
@@ -440,27 +205,6 @@ const delay = (ms) => {
440
205
  };
441
206
  };
442
207
 
443
- //#endregion
444
- //#region src/stream/sources/from-array.ts
445
- /**
446
- * Creates a source that emits values from an array and completes.
447
- * @param values - The array of values to emit.
448
- * @returns A source containing the array values.
449
- */
450
- const fromArray = (values) => {
451
- return (sink) => {
452
- let cancelled = false;
453
- for (const value of values) {
454
- if (cancelled) break;
455
- sink.next(value);
456
- }
457
- if (!cancelled) sink.complete();
458
- return { unsubscribe() {
459
- cancelled = true;
460
- } };
461
- };
462
- };
463
-
464
208
  //#endregion
465
209
  //#region src/utils.ts
466
210
  /**
@@ -489,306 +233,10 @@ const stringify = (value) => {
489
233
  * @param value - Value to check.
490
234
  * @returns True if the value is nullish.
491
235
  */
492
- const isNullish = (value) => {
236
+ const isNullish$1 = (value) => {
493
237
  return value === void 0 || value === null;
494
238
  };
495
239
 
496
- //#endregion
497
- //#region src/stream/operators/map.ts
498
- /**
499
- * Maps each value from the source through a transformation function.
500
- * @param fn - The transformation function.
501
- * @returns An operator that maps values.
502
- */
503
- const map = (fn) => {
504
- return (source) => {
505
- return (sink) => {
506
- return source({
507
- next(value) {
508
- sink.next(fn(value));
509
- },
510
- complete() {
511
- sink.complete();
512
- }
513
- });
514
- };
515
- };
516
- };
517
-
518
- //#endregion
519
- //#region src/stream/operators/take-until.ts
520
- /**
521
- * Emits values from the source until the notifier source emits a value.
522
- * When the notifier emits, the source is cancelled and completes immediately.
523
- * @param notifier - Source that signals when to complete.
524
- * @returns Operator that completes when notifier emits.
525
- */
526
- const takeUntil = (notifier) => {
527
- return (source) => {
528
- return (sink) => {
529
- let sourceSubscription = null;
530
- let notifierSubscription = null;
531
- let completed = false;
532
- const complete = () => {
533
- if (completed) return;
534
- completed = true;
535
- if (sourceSubscription) sourceSubscription.unsubscribe();
536
- if (notifierSubscription) notifierSubscription.unsubscribe();
537
- sink.complete();
538
- };
539
- notifierSubscription = notifier({
540
- next() {
541
- complete();
542
- },
543
- complete() {}
544
- });
545
- sourceSubscription = source({
546
- next(value) {
547
- if (!completed) sink.next(value);
548
- },
549
- complete() {
550
- complete();
551
- }
552
- });
553
- return { unsubscribe() {
554
- complete();
555
- } };
556
- };
557
- };
558
- };
559
-
560
- //#endregion
561
- //#region src/stream/operators/switch-map.ts
562
- const switchMap = (fn) => {
563
- return (source) => {
564
- return (sink) => {
565
- let outerCompleted = false;
566
- let ended = false;
567
- let innerSubscription = null;
568
- let hasInner = false;
569
- const checkComplete = () => {
570
- if (outerCompleted && !hasInner && !ended) {
571
- ended = true;
572
- sink.complete();
573
- }
574
- };
575
- const outerSubscription = source({
576
- next(value) {
577
- if (ended) return;
578
- if (innerSubscription) {
579
- innerSubscription.unsubscribe();
580
- innerSubscription = null;
581
- }
582
- hasInner = true;
583
- innerSubscription = fn(value)({
584
- next(innerValue) {
585
- if (!ended) sink.next(innerValue);
586
- },
587
- complete() {
588
- hasInner = false;
589
- innerSubscription = null;
590
- checkComplete();
591
- }
592
- });
593
- },
594
- complete() {
595
- outerCompleted = true;
596
- checkComplete();
597
- }
598
- });
599
- return { unsubscribe() {
600
- ended = true;
601
- outerSubscription.unsubscribe();
602
- if (innerSubscription) {
603
- innerSubscription.unsubscribe();
604
- innerSubscription = null;
605
- }
606
- } };
607
- };
608
- };
609
- };
610
-
611
- //#endregion
612
- //#region src/stream/operators/initialize.ts
613
- /**
614
- * Executes a side effect when the source is initialized (being subscribed to).
615
- * @param fn - The side effect function.
616
- * @returns An operator that executes the side effect when the source is initialized.
617
- */
618
- const initialize = (fn) => {
619
- return (source) => {
620
- return (sink) => {
621
- let completed = false;
622
- const subscription = source({
623
- next(value) {
624
- if (!completed) sink.next(value);
625
- },
626
- complete() {
627
- if (!completed) {
628
- completed = true;
629
- sink.complete();
630
- }
631
- }
632
- });
633
- fn();
634
- return { unsubscribe() {
635
- completed = true;
636
- subscription.unsubscribe();
637
- } };
638
- };
639
- };
640
- };
641
-
642
- //#endregion
643
- //#region src/stream/operators/finalize.ts
644
- /**
645
- * Executes a side effect when the source terminates (completes or unsubscribes).
646
- * @param fn - The side effect function.
647
- * @returns An operator that executes the side effect when the source terminates.
648
- */
649
- const finalize = (fn) => {
650
- return (source) => {
651
- return (sink) => {
652
- let completed = false;
653
- const subscription = source({
654
- next(value) {
655
- if (!completed) sink.next(value);
656
- },
657
- complete() {
658
- if (!completed) {
659
- completed = true;
660
- fn();
661
- sink.complete();
662
- }
663
- }
664
- });
665
- return { unsubscribe() {
666
- if (!completed) {
667
- completed = true;
668
- fn();
669
- }
670
- subscription.unsubscribe();
671
- } };
672
- };
673
- };
674
- };
675
-
676
- //#endregion
677
- //#region src/stream/sources/from-value.ts
678
- /**
679
- * Creates a source that emits a single value and completes.
680
- * @param value - The value to emit.
681
- * @returns A source containing the single value.
682
- */
683
- const fromValue = (value) => {
684
- return (sink) => {
685
- let cancelled = false;
686
- if (!cancelled) {
687
- sink.next(value);
688
- sink.complete();
689
- }
690
- return { unsubscribe() {
691
- cancelled = true;
692
- } };
693
- };
694
- };
695
-
696
- //#endregion
697
- //#region src/stream/sources/make-subject.ts
698
- /**
699
- * Creates a new Subject which can be used as an IO event hub.
700
- * @returns A new Subject.
701
- */
702
- const makeSubject = () => {
703
- const sinks = [];
704
- const source = (sink) => {
705
- sinks.push(sink);
706
- return { unsubscribe() {
707
- const idx = sinks.indexOf(sink);
708
- if (idx !== -1) sinks.splice(idx, 1);
709
- } };
710
- };
711
- const next = (value) => {
712
- for (const sink of [...sinks]) sink.next(value);
713
- };
714
- const complete = () => {
715
- for (const sink of [...sinks]) sink.complete();
716
- sinks.length = 0;
717
- };
718
- return {
719
- source,
720
- next,
721
- complete
722
- };
723
- };
724
-
725
- //#endregion
726
- //#region src/stream/sources/from-subscription.ts
727
- const fromSubscription = (pull, poke) => {
728
- return (sink) => {
729
- let teardown = null;
730
- let cancelled = false;
731
- const initialValue = pull();
732
- sink.next(initialValue);
733
- if (cancelled) return { unsubscribe() {
734
- cancelled = true;
735
- } };
736
- teardown = poke(() => {
737
- if (!cancelled) {
738
- const value = pull();
739
- sink.next(value);
740
- }
741
- });
742
- return { unsubscribe() {
743
- cancelled = true;
744
- if (teardown) {
745
- teardown();
746
- teardown = null;
747
- }
748
- } };
749
- };
750
- };
751
-
752
- //#endregion
753
- //#region src/stream/sources/make.ts
754
- /**
755
- * Creates a new Source from scratch from a passed subscriber function.
756
- *
757
- * The subscriber function receives an observer with next and complete callbacks.
758
- * It must return a teardown function which is called when the source is cancelled.
759
- * @internal
760
- * @param subscriber - A callback that is called when the Source is subscribed to.
761
- * @returns A Source created from the subscriber parameter.
762
- */
763
- const make = (subscriber) => {
764
- return (sink) => {
765
- let cancelled = false;
766
- let teardown = null;
767
- teardown = subscriber({
768
- next: (value) => {
769
- if (!cancelled) sink.next(value);
770
- },
771
- complete: () => {
772
- if (!cancelled) {
773
- cancelled = true;
774
- if (teardown) {
775
- teardown();
776
- teardown = null;
777
- }
778
- sink.complete();
779
- }
780
- }
781
- });
782
- return { unsubscribe() {
783
- cancelled = true;
784
- if (teardown) {
785
- teardown();
786
- teardown = null;
787
- }
788
- } };
789
- };
790
- };
791
-
792
240
  //#endregion
793
241
  //#region src/exchanges/dedup.ts
794
242
  const makeDedupKey = (op) => {
@@ -810,8 +258,9 @@ const makeDedupKey = (op) => {
810
258
  * @returns An exchange that deduplicates in-flight operations.
811
259
  */
812
260
  const dedupExchange = () => {
813
- return ({ forward }) => {
814
- return (ops$) => {
261
+ return ({ forward }) => ({
262
+ name: "dedup",
263
+ io: (ops$) => {
815
264
  const operations = /* @__PURE__ */ new Map();
816
265
  return pipe(merge(pipe(ops$, filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "fragment"))), pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind !== "mutation" && op.artifact.kind !== "fragment"), filter((op) => {
817
266
  const dedupKey = makeDedupKey(op);
@@ -839,8 +288,8 @@ const dedupExchange = () => {
839
288
  }
840
289
  })));
841
290
  }));
842
- };
843
- };
291
+ }
292
+ });
844
293
  };
845
294
 
846
295
  //#endregion
@@ -899,6 +348,15 @@ const makeFieldKey = (selection, variables) => {
899
348
  return `${selection.name}@${args}`;
900
349
  };
901
350
  /**
351
+ * Generates a unique key for tracking memoized denormalized results for structural sharing.
352
+ * @internal
353
+ * @param kind - The operation kind ('query', 'fragment', 'fragments').
354
+ * @param name - The artifact name.
355
+ * @param id - Serialized identifier (variables, entity key, etc.).
356
+ * @returns A unique memo key.
357
+ */
358
+ const makeMemoKey = (kind, name, id) => `${kind}:${name}:${id}`;
359
+ /**
902
360
  * Gets a unique key for tracking a field dependency.
903
361
  * @internal
904
362
  * @param storageKey Storage key (entity or root query key).
@@ -927,46 +385,166 @@ const isFragmentRef = (value) => {
927
385
  return typeof value === "object" && value !== null && FragmentRefKey in value;
928
386
  };
929
387
  /**
388
+ * Type guard to check if a value is an array of fragment references.
389
+ * @internal
390
+ * @param value - Value to check.
391
+ * @returns True if the value is a FragmentRef array.
392
+ */
393
+ const isFragmentRefArray = (value) => {
394
+ return Array.isArray(value) && value.length > 0 && isFragmentRef(value[0]);
395
+ };
396
+ /**
930
397
  * Type guard to check if a value is nullish.
931
398
  * @internal
932
399
  * @param value - Value to check.
933
400
  * @returns True if the value is nullish.
934
401
  */
935
- const isNullish$1 = (value) => {
402
+ const isNullish = (value) => {
936
403
  return value === void 0 || value === null;
937
404
  };
405
+ /**
406
+ * Deep equality check for normalized cache values.
407
+ * Handles scalars, arrays, and plain objects (entity links, value objects).
408
+ * @internal
409
+ */
410
+ const isEqual = (a, b) => {
411
+ if (a === b) return true;
412
+ if (typeof a !== typeof b || a === null || b === null) return false;
413
+ if (Array.isArray(a)) {
414
+ if (!Array.isArray(b) || a.length !== b.length) return false;
415
+ for (const [i, item] of a.entries()) if (!isEqual(item, b[i])) return false;
416
+ return true;
417
+ }
418
+ if (typeof a === "object") {
419
+ const aObj = a;
420
+ const bObj = b;
421
+ const aKeys = Object.keys(aObj);
422
+ if (aKeys.length !== Object.keys(bObj).length) return false;
423
+ for (const key of aKeys) if (!isEqual(aObj[key], bObj[key])) return false;
424
+ return true;
425
+ }
426
+ return false;
427
+ };
428
+ /**
429
+ * Recursively replaces a new value tree with the previous one wherever structurally equal,
430
+ * preserving referential identity for unchanged subtrees.
431
+ *
432
+ * Returns `prev` (same reference) when the entire subtree is structurally equal.
433
+ * @internal
434
+ */
435
+ const replaceEqualDeep = (prev, next) => {
436
+ if (prev === next) return prev;
437
+ if (typeof prev !== typeof next || prev === null || next === null || typeof prev !== "object") return next;
438
+ if (Array.isArray(prev)) {
439
+ if (!Array.isArray(next)) return next;
440
+ let allSame = prev.length === next.length;
441
+ const result = [];
442
+ for (const [i, item] of next.entries()) {
443
+ const shared = i < prev.length ? replaceEqualDeep(prev[i], item) : item;
444
+ result.push(shared);
445
+ if (shared !== prev[i]) allSame = false;
446
+ }
447
+ return allSame ? prev : result;
448
+ }
449
+ if (Array.isArray(next)) return next;
450
+ const prevObj = prev;
451
+ const nextObj = next;
452
+ const nextKeys = Object.keys(nextObj);
453
+ const prevKeys = Object.keys(prevObj);
454
+ let allSame = nextKeys.length === prevKeys.length;
455
+ const result = {};
456
+ for (const key of nextKeys) if (key in prevObj) {
457
+ result[key] = replaceEqualDeep(prevObj[key], nextObj[key]);
458
+ if (result[key] !== prevObj[key]) allSame = false;
459
+ } else {
460
+ result[key] = nextObj[key];
461
+ allSame = false;
462
+ }
463
+ return allSame ? prev : result;
464
+ };
465
+ /**
466
+ * Deeply merges two values. Objects are recursively merged, arrays are element-wise merged,
467
+ * entity links and primitives use last-write-wins.
468
+ * @internal
469
+ */
470
+ const mergeFieldValue = (existing, incoming) => {
471
+ if (isNullish(existing) || isNullish(incoming)) return incoming;
472
+ if (typeof existing !== "object" || typeof incoming !== "object") return incoming;
473
+ if (isEntityLink(existing) || isEntityLink(incoming)) return incoming;
474
+ if (Array.isArray(existing) && Array.isArray(incoming)) return incoming.map((item, i) => i < existing.length ? mergeFieldValue(existing[i], item) : item);
475
+ if (Array.isArray(existing) || Array.isArray(incoming)) return incoming;
476
+ mergeFields(existing, incoming);
477
+ return existing;
478
+ };
479
+ /**
480
+ * Deeply merges source fields into target. Objects are recursively merged,
481
+ * arrays are element-wise merged, entity links and primitives use last-write-wins.
482
+ * @internal
483
+ */
484
+ const mergeFields = (target, source) => {
485
+ if (isNullish(source) || typeof source !== "object" || Array.isArray(source)) return;
486
+ for (const key of Object.keys(source)) target[key] = mergeFieldValue(target[key], source[key]);
487
+ };
488
+ /**
489
+ * Creates a FieldKey from a raw field name and optional arguments.
490
+ * @internal
491
+ * @param field - The field name.
492
+ * @param args - Optional argument values.
493
+ * @returns A FieldKey in "field@args" format.
494
+ */
495
+ const makeFieldKeyFromArgs = (field, args) => {
496
+ return `${field}@${args && Object.keys(args).length > 0 ? stringify(args) : "{}"}`;
497
+ };
498
+ /**
499
+ * Converts an EntityId to an EntityKey.
500
+ * @internal
501
+ * @param typename - The GraphQL typename of the entity.
502
+ * @param id - The entity identifier (string, number, or composite key record).
503
+ * @param keyFields - Optional ordered list of key field names for composite keys.
504
+ * @returns An EntityKey.
505
+ */
506
+ const resolveEntityKey = (typename, id, keyFields) => {
507
+ if (typeof id === "string" || typeof id === "number") return makeEntityKey(typename, [id]);
508
+ return makeEntityKey(typename, keyFields ? keyFields.map((f) => id[f]) : Object.values(id));
509
+ };
938
510
 
939
511
  //#endregion
940
512
  //#region src/cache/normalize.ts
513
+ const SKIP = Symbol();
941
514
  const normalize = (schemaMeta, selections, storage, data, variables, accessor) => {
942
- const normalizeField = (storageKey, selections$1, value) => {
943
- if (isNullish$1(value)) return value;
944
- if (Array.isArray(value)) return value.map((item) => normalizeField(storageKey, selections$1, item));
945
- const data$1 = value;
946
- const typename = data$1.__typename;
515
+ const normalizeField = (storageKey, selections, value) => {
516
+ if (isNullish(value)) return value;
517
+ if (Array.isArray(value)) return value.map((item) => normalizeField(storageKey, selections, item));
518
+ const data = value;
519
+ const typename = data.__typename;
947
520
  const entityMeta = schemaMeta.entities[typename];
948
- if (entityMeta) storageKey = makeEntityKey(typename, entityMeta.keyFields.map((field) => data$1[field]));
949
- const fields$1 = {};
950
- for (const selection of selections$1) if (selection.kind === "Field") {
521
+ if (entityMeta) {
522
+ const keys = entityMeta.keyFields.map((field) => data[field]);
523
+ if (!keys.every((k) => k !== void 0 && k !== null)) return SKIP;
524
+ storageKey = makeEntityKey(typename, keys);
525
+ }
526
+ const fields = {};
527
+ for (const selection of selections) if (selection.kind === "Field") {
951
528
  const fieldKey = makeFieldKey(selection, variables);
952
- const fieldValue = data$1[selection.alias ?? selection.name];
953
- if (storageKey !== null) {
954
- const oldValue = storage[storageKey]?.[fieldKey];
955
- if (!selection.selections || isNullish$1(oldValue) || isNullish$1(fieldValue)) accessor?.(storageKey, fieldKey, oldValue, fieldValue);
956
- }
957
- fields$1[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
529
+ const fieldValue = data[selection.alias ?? selection.name];
530
+ const oldValue = storageKey === null ? void 0 : storage[storageKey]?.[fieldKey];
531
+ if (storageKey !== null && (!selection.selections || isNullish(oldValue) || isNullish(fieldValue))) accessor?.(storageKey, fieldKey, oldValue, fieldValue);
532
+ const normalized = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
533
+ if (normalized === SKIP) continue;
534
+ fields[fieldKey] = normalized;
535
+ if (storageKey !== null && selection.selections && !isNullish(oldValue) && !isNullish(fieldValue) && !isEntityLink(fields[fieldKey]) && !isEqual(oldValue, fields[fieldKey])) accessor?.(storageKey, fieldKey, oldValue, fields[fieldKey]);
958
536
  } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === typename) {
959
537
  const inner = normalizeField(storageKey, selection.selections, value);
960
- if (!isEntityLink(inner)) Object.assign(fields$1, inner);
538
+ if (inner !== SKIP && !isEntityLink(inner)) mergeFields(fields, inner);
961
539
  }
962
540
  if (entityMeta && storageKey !== null) {
963
541
  storage[storageKey] = {
964
542
  ...storage[storageKey],
965
- ...fields$1
543
+ ...fields
966
544
  };
967
545
  return { [EntityLinkKey]: storageKey };
968
546
  }
969
- return fields$1;
547
+ return fields;
970
548
  };
971
549
  const fields = normalizeField(RootFieldKey, selections, data);
972
550
  storage[RootFieldKey] = {
@@ -984,10 +562,10 @@ const typenameFieldKey = makeFieldKey({
984
562
  }, {});
985
563
  const denormalize = (selections, storage, value, variables, accessor) => {
986
564
  let partial = false;
987
- const denormalizeField = (storageKey, selections$1, value$1) => {
988
- if (isNullish$1(value$1)) return value$1;
989
- if (Array.isArray(value$1)) return value$1.map((item) => denormalizeField(storageKey, selections$1, item));
990
- const data = value$1;
565
+ const denormalizeField = (storageKey, selections, value) => {
566
+ if (isNullish(value)) return value;
567
+ if (Array.isArray(value)) return value.map((item) => denormalizeField(storageKey, selections, item));
568
+ const data = value;
991
569
  if (isEntityLink(data)) {
992
570
  const entityKey = data[EntityLinkKey];
993
571
  const entity = storage[entityKey];
@@ -996,10 +574,10 @@ const denormalize = (selections, storage, value, variables, accessor) => {
996
574
  partial = true;
997
575
  return null;
998
576
  }
999
- return denormalizeField(entityKey, selections$1, entity);
577
+ return denormalizeField(entityKey, selections, entity);
1000
578
  }
1001
579
  const fields = {};
1002
- for (const selection of selections$1) if (selection.kind === "Field") {
580
+ for (const selection of selections) if (selection.kind === "Field") {
1003
581
  const fieldKey = makeFieldKey(selection, variables);
1004
582
  const fieldValue = data[fieldKey];
1005
583
  if (storageKey !== null) accessor?.(storageKey, fieldKey);
@@ -1007,10 +585,13 @@ const denormalize = (selections, storage, value, variables, accessor) => {
1007
585
  partial = true;
1008
586
  continue;
1009
587
  }
1010
- fields[selection.alias ?? selection.name] = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
588
+ const name = selection.alias ?? selection.name;
589
+ const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
590
+ if (name in fields) mergeFields(fields, { [name]: value });
591
+ else fields[name] = value;
1011
592
  } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) fields[FragmentRefKey] = storageKey;
1012
- else Object.assign(fields, denormalizeField(storageKey, selection.selections, value$1));
1013
- else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) Object.assign(fields, denormalizeField(storageKey, selection.selections, value$1));
593
+ else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
594
+ else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
1014
595
  return fields;
1015
596
  };
1016
597
  return {
@@ -1029,6 +610,7 @@ var Cache = class {
1029
610
  #schemaMeta;
1030
611
  #storage = { [RootFieldKey]: {} };
1031
612
  #subscriptions = /* @__PURE__ */ new Map();
613
+ #memo = /* @__PURE__ */ new Map();
1032
614
  constructor(schemaMetadata) {
1033
615
  this.#schemaMeta = schemaMetadata;
1034
616
  }
@@ -1055,13 +637,19 @@ var Cache = class {
1055
637
  }
1056
638
  /**
1057
639
  * Reads a query result from the cache, denormalizing entities if available.
640
+ * Uses structural sharing to preserve referential identity for unchanged subtrees.
1058
641
  * @param artifact - GraphQL document artifact.
1059
642
  * @param variables - Query variables.
1060
643
  * @returns Denormalized query result or null if not found.
1061
644
  */
1062
645
  readQuery(artifact, variables) {
1063
646
  const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables);
1064
- return partial ? null : data;
647
+ if (partial) return null;
648
+ const key = makeMemoKey("query", artifact.name, stringify(variables));
649
+ const prev = this.#memo.get(key);
650
+ const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
651
+ this.#memo.set(key, result);
652
+ return result;
1065
653
  }
1066
654
  /**
1067
655
  * Subscribes to cache invalidations for a specific query.
@@ -1080,8 +668,7 @@ var Cache = class {
1080
668
  }
1081
669
  /**
1082
670
  * Reads a fragment from the cache for a specific entity.
1083
- * Returns null for invalid or missing fragment references, making it safe for
1084
- * defensive reads. For subscriptions, use subscribeFragment which throws errors.
671
+ * Uses structural sharing to preserve referential identity for unchanged subtrees.
1085
672
  * @param artifact - GraphQL fragment artifact.
1086
673
  * @param fragmentRef - Fragment reference containing entity key.
1087
674
  * @returns Denormalized fragment data or null if not found or invalid.
@@ -1090,7 +677,12 @@ var Cache = class {
1090
677
  const entityKey = fragmentRef[FragmentRefKey];
1091
678
  if (!this.#storage[entityKey]) return null;
1092
679
  const { data, partial } = denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {});
1093
- return partial ? null : data;
680
+ if (partial) return null;
681
+ const key = makeMemoKey("fragment", artifact.name, entityKey);
682
+ const prev = this.#memo.get(key);
683
+ const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
684
+ this.#memo.set(key, result);
685
+ return result;
1094
686
  }
1095
687
  subscribeFragment(artifact, fragmentRef, listener) {
1096
688
  const entityKey = fragmentRef[FragmentRefKey];
@@ -1101,6 +693,80 @@ var Cache = class {
1101
693
  });
1102
694
  return this.#subscribe(dependencies, listener);
1103
695
  }
696
+ readFragments(artifact, fragmentRefs) {
697
+ const results = [];
698
+ for (const ref of fragmentRefs) {
699
+ const data = this.readFragment(artifact, ref);
700
+ if (data === null) return null;
701
+ results.push(data);
702
+ }
703
+ const entityKeys = fragmentRefs.map((ref) => ref[FragmentRefKey]);
704
+ const key = makeMemoKey("fragments", artifact.name, entityKeys.join(","));
705
+ const prev = this.#memo.get(key);
706
+ const result = prev === void 0 ? results : replaceEqualDeep(prev, results);
707
+ this.#memo.set(key, result);
708
+ return result;
709
+ }
710
+ subscribeFragments(artifact, fragmentRefs, listener) {
711
+ const dependencies = /* @__PURE__ */ new Set();
712
+ for (const ref of fragmentRefs) {
713
+ const entityKey = ref[FragmentRefKey];
714
+ denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
715
+ dependencies.add(makeDependencyKey(storageKey, fieldKey));
716
+ });
717
+ }
718
+ return this.#subscribe(dependencies, listener);
719
+ }
720
+ /**
721
+ * Invalidates one or more cache entries and notifies affected subscribers.
722
+ * @param targets - Cache entries to invalidate.
723
+ */
724
+ invalidate(...targets) {
725
+ const subscriptions = /* @__PURE__ */ new Set();
726
+ for (const target of targets) if (target.__typename === "Query") if ("field" in target) {
727
+ const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
728
+ delete this.#storage[RootFieldKey]?.[fieldKey];
729
+ this.#collectSubscriptions(RootFieldKey, fieldKey, subscriptions);
730
+ } else {
731
+ this.#storage[RootFieldKey] = {};
732
+ this.#collectSubscriptions(RootFieldKey, void 0, subscriptions);
733
+ }
734
+ else if ("id" in target) {
735
+ const entityKey = resolveEntityKey(target.__typename, target.id, this.#schemaMeta.entities[target.__typename]?.keyFields);
736
+ if ("field" in target) {
737
+ const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
738
+ delete this.#storage[entityKey]?.[fieldKey];
739
+ this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
740
+ } else {
741
+ delete this.#storage[entityKey];
742
+ this.#collectSubscriptions(entityKey, void 0, subscriptions);
743
+ }
744
+ } else {
745
+ const prefix = `${target.__typename}:`;
746
+ for (const key of Object.keys(this.#storage)) if (key.startsWith(prefix)) {
747
+ const entityKey = key;
748
+ if ("field" in target) {
749
+ const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
750
+ delete this.#storage[entityKey]?.[fieldKey];
751
+ this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
752
+ } else {
753
+ delete this.#storage[entityKey];
754
+ this.#collectSubscriptions(entityKey, void 0, subscriptions);
755
+ }
756
+ }
757
+ }
758
+ for (const subscription of subscriptions) subscription.listener();
759
+ }
760
+ #collectSubscriptions(storageKey, fieldKey, out) {
761
+ if (fieldKey === void 0) {
762
+ const prefix = `${storageKey}.`;
763
+ for (const [depKey, ss] of this.#subscriptions) if (depKey.startsWith(prefix)) for (const s of ss) out.add(s);
764
+ } else {
765
+ const depKey = makeDependencyKey(storageKey, fieldKey);
766
+ const ss = this.#subscriptions.get(depKey);
767
+ if (ss) for (const s of ss) out.add(s);
768
+ }
769
+ }
1104
770
  #subscribe(dependencies, listener) {
1105
771
  const subscription = { listener };
1106
772
  for (const dependency of dependencies) {
@@ -1117,62 +783,134 @@ var Cache = class {
1117
783
  };
1118
784
  }
1119
785
  /**
786
+ * Extracts a serializable snapshot of the cache storage and structural sharing state.
787
+ */
788
+ extract() {
789
+ return {
790
+ storage: structuredClone(this.#storage),
791
+ memo: Object.fromEntries(this.#memo)
792
+ };
793
+ }
794
+ /**
795
+ * Hydrates the cache with a previously extracted snapshot.
796
+ */
797
+ hydrate(snapshot) {
798
+ const { storage, memo } = snapshot;
799
+ for (const [key, fields] of Object.entries(storage)) this.#storage[key] = {
800
+ ...this.#storage[key],
801
+ ...fields
802
+ };
803
+ for (const [key, value] of Object.entries(memo)) this.#memo.set(key, value);
804
+ }
805
+ /**
1120
806
  * Clears all cache data.
1121
807
  */
1122
808
  clear() {
1123
809
  this.#storage = { [RootFieldKey]: {} };
1124
810
  this.#subscriptions.clear();
811
+ this.#memo.clear();
1125
812
  }
1126
813
  };
1127
814
 
815
+ //#endregion
816
+ //#region src/stream/sources/empty.ts
817
+ /**
818
+ * Creates a source that completes immediately without emitting any values.
819
+ * @returns An empty source.
820
+ */
821
+ const empty = () => {
822
+ return (sink) => {
823
+ sink.complete();
824
+ return { unsubscribe() {} };
825
+ };
826
+ };
827
+
1128
828
  //#endregion
1129
829
  //#region src/exchanges/cache.ts
1130
830
  const cacheExchange = (options = {}) => {
1131
831
  const { fetchPolicy = "cache-first" } = options;
1132
832
  return ({ forward, client }) => {
1133
833
  const cache = new Cache(client.schema);
1134
- return (ops$) => {
1135
- const fragment$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), mergeMap((op) => {
1136
- const fragmentRef = op.metadata?.fragmentRef;
1137
- if (!fragmentRef) return fromValue({
1138
- operation: op,
1139
- errors: [new ExchangeError("Fragment operation missing fragmentRef in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
1140
- });
1141
- if (!isFragmentRef(fragmentRef)) return fromValue({
1142
- operation: op,
1143
- data: fragmentRef,
1144
- errors: []
1145
- });
1146
- const trigger = makeSubject();
1147
- const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
1148
- return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readFragment(op.artifact, fragmentRef), () => cache.subscribeFragment(op.artifact, fragmentRef, async () => {
1149
- await Promise.resolve();
1150
- trigger.next();
1151
- }))), takeUntil(teardown$), map((data) => ({
1152
- operation: op,
1153
- data,
1154
- errors: []
1155
- })));
1156
- }));
1157
- const nonCache$ = pipe(ops$, filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "subscription" || op.artifact.kind === "query" && fetchPolicy === "network-only")));
1158
- const query$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), share());
1159
- return merge(fragment$, pipe(query$, mergeMap((op) => {
1160
- const trigger = makeSubject();
1161
- const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
1162
- return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readQuery(op.artifact, op.variables), () => cache.subscribeQuery(op.artifact, op.variables, async () => {
1163
- await Promise.resolve();
1164
- trigger.next();
1165
- }))), takeUntil(teardown$), map((data) => ({
1166
- operation: op,
1167
- data,
1168
- errors: []
1169
- })));
1170
- }), filter((result) => fetchPolicy === "cache-only" || fetchPolicy === "cache-and-network" && result.data !== null || fetchPolicy === "cache-first" && result.data !== null)), pipe(merge(nonCache$, pipe(query$, filter((op) => {
1171
- const cached = cache.readQuery(op.artifact, op.variables);
1172
- return fetchPolicy === "cache-and-network" || cached === null;
1173
- })), pipe(ops$, filter((op) => op.variant === "teardown"))), forward, tap((result) => {
1174
- if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
1175
- }), filter((result) => result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only")));
834
+ return {
835
+ name: "cache",
836
+ extension: {
837
+ extract: () => cache.extract(),
838
+ hydrate: (snapshot) => cache.hydrate(snapshot),
839
+ invalidate: (...targets) => cache.invalidate(...targets),
840
+ clear: () => cache.clear()
841
+ },
842
+ io: (ops$) => {
843
+ const fragment$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), mergeMap((op) => {
844
+ const fragmentRef = op.metadata?.fragmentRef;
845
+ if (!fragmentRef) return fromValue({
846
+ operation: op,
847
+ errors: [new ExchangeError("Fragment operation missing fragmentRef in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
848
+ });
849
+ if (isFragmentRefArray(fragmentRef)) {
850
+ const trigger = makeSubject();
851
+ const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
852
+ return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readFragments(op.artifact, fragmentRef), () => cache.subscribeFragments(op.artifact, fragmentRef, async () => {
853
+ await Promise.resolve();
854
+ trigger.next();
855
+ }))), takeUntil(teardown$), map((data) => ({
856
+ operation: op,
857
+ data,
858
+ errors: []
859
+ })));
860
+ }
861
+ if (!isFragmentRef(fragmentRef)) return fromValue({
862
+ operation: op,
863
+ data: fragmentRef,
864
+ errors: []
865
+ });
866
+ const trigger = makeSubject();
867
+ const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
868
+ return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readFragment(op.artifact, fragmentRef), () => cache.subscribeFragment(op.artifact, fragmentRef, async () => {
869
+ await Promise.resolve();
870
+ trigger.next();
871
+ }))), takeUntil(teardown$), map((data) => ({
872
+ operation: op,
873
+ data,
874
+ errors: []
875
+ })));
876
+ }));
877
+ const nonCache$ = pipe(ops$, filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "subscription" || op.artifact.kind === "query" && fetchPolicy === "network-only")));
878
+ const query$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), share());
879
+ const refetch$ = makeSubject();
880
+ return merge(fragment$, pipe(query$, mergeMap((op) => {
881
+ const trigger = makeSubject();
882
+ let hasData = false;
883
+ const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
884
+ return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readQuery(op.artifact, op.variables), () => cache.subscribeQuery(op.artifact, op.variables, async () => {
885
+ await Promise.resolve();
886
+ trigger.next();
887
+ }))), takeUntil(teardown$), mergeMap((data) => {
888
+ if (data !== null) {
889
+ hasData = true;
890
+ return fromValue({
891
+ operation: op,
892
+ data,
893
+ errors: []
894
+ });
895
+ }
896
+ if (hasData) {
897
+ refetch$.next(op);
898
+ return empty();
899
+ }
900
+ if (fetchPolicy === "cache-only") return fromValue({
901
+ operation: op,
902
+ data: null,
903
+ errors: []
904
+ });
905
+ return empty();
906
+ }));
907
+ }), filter(() => fetchPolicy === "cache-only" || fetchPolicy === "cache-and-network" || fetchPolicy === "cache-first")), pipe(merge(nonCache$, pipe(query$, filter((op) => {
908
+ const cached = cache.readQuery(op.artifact, op.variables);
909
+ return fetchPolicy === "cache-and-network" || cached === null;
910
+ })), pipe(ops$, filter((op) => op.variant === "teardown")), refetch$.source), forward, tap((result) => {
911
+ if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
912
+ }), filter((result) => result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0))));
913
+ }
1176
914
  };
1177
915
  };
1178
916
  };
@@ -1182,16 +920,17 @@ const cacheExchange = (options = {}) => {
1182
920
  const defaultShouldRetry = (error) => isExchangeError(error, "http") && error.extensions?.statusCode !== void 0 && error.extensions.statusCode >= 500;
1183
921
  const retryExchange = (options = {}) => {
1184
922
  const { maxAttempts = 3, backoff = (attempt) => Math.min(1e3 * 2 ** attempt, 3e4), shouldRetry = defaultShouldRetry } = options;
1185
- return ({ forward }) => {
1186
- return (ops$) => {
923
+ return ({ forward }) => ({
924
+ name: "retry",
925
+ io: (ops$) => {
1187
926
  const { source: retries$, next } = makeSubject();
1188
927
  const tornDown = /* @__PURE__ */ new Set();
1189
928
  const teardown$ = pipe(ops$, filter((op) => op.variant === "teardown"), tap((op) => {
1190
929
  tornDown.add(op.key);
1191
930
  }));
1192
931
  return pipe(merge(pipe(ops$, filter((op) => op.variant === "request")), pipe(retries$, filter((op) => !tornDown.has(op.key)), mergeMap((op) => {
1193
- const teardown$$1 = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key));
1194
- return pipe(fromValue(op), delay(op.metadata.retry.delay), takeUntil(teardown$$1));
932
+ const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key));
933
+ return pipe(fromValue(op), delay(op.metadata.retry.delay), takeUntil(teardown$));
1195
934
  })), teardown$), forward, filter((result) => {
1196
935
  if (!result.errors || result.errors.length === 0) return true;
1197
936
  if (result.operation.variant === "request" && result.operation.artifact.kind === "mutation") return true;
@@ -1211,15 +950,16 @@ const retryExchange = (options = {}) => {
1211
950
  });
1212
951
  return false;
1213
952
  }));
1214
- };
1215
- };
953
+ }
954
+ });
1216
955
  };
1217
956
 
1218
957
  //#endregion
1219
958
  //#region src/exchanges/fragment.ts
1220
959
  const fragmentExchange = () => {
1221
- return ({ forward }) => {
1222
- return (ops$) => {
960
+ return ({ forward }) => ({
961
+ name: "fragment",
962
+ io: (ops$) => {
1223
963
  return merge(pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), map((op) => {
1224
964
  const fragmentRef = op.metadata.fragmentRef;
1225
965
  if (!fragmentRef) return {
@@ -1231,12 +971,103 @@ const fragmentExchange = () => {
1231
971
  data: fragmentRef
1232
972
  };
1233
973
  })), pipe(ops$, filter((op) => op.variant === "teardown" || op.artifact.kind !== "fragment"), forward));
1234
- };
1235
- };
974
+ }
975
+ });
976
+ };
977
+
978
+ //#endregion
979
+ //#region src/required.ts
980
+ const CASCADE_NULL = Symbol("CASCADE_NULL");
981
+ var RequiredFieldError = class extends Error {
982
+ fieldPath;
983
+ fieldName;
984
+ constructor(fieldPath, fieldName) {
985
+ super(`Required field '${fieldPath.join(".")}.${fieldName}' is null`);
986
+ this.name = "RequiredFieldError";
987
+ this.fieldPath = fieldPath;
988
+ this.fieldName = fieldName;
989
+ }
990
+ };
991
+ const getRequiredAction = (directives) => {
992
+ if (!directives) return null;
993
+ const requiredDirective = directives.find((d) => d.name === "required");
994
+ if (!requiredDirective) return null;
995
+ if (requiredDirective.args?.action === "CASCADE") return "CASCADE";
996
+ return "THROW";
997
+ };
998
+ const validateRequiredInner = (selections, data, fieldPath, validatedMap) => {
999
+ if (data === null || data === void 0) return data;
1000
+ if (typeof data !== "object") return data;
1001
+ if (Array.isArray(data)) return data.map((item, index) => {
1002
+ const result = validateRequiredInner(selections, item, [...fieldPath, `[${index}]`], validatedMap);
1003
+ return result === CASCADE_NULL ? null : result;
1004
+ });
1005
+ const obj = data;
1006
+ validatedMap ??= /* @__PURE__ */ new WeakMap();
1007
+ let validated = validatedMap.get(obj);
1008
+ if (!validated) {
1009
+ validated = /* @__PURE__ */ new Set();
1010
+ validatedMap.set(obj, validated);
1011
+ }
1012
+ for (const selection of selections) if (selection.kind === "Field") {
1013
+ const fieldName = selection.alias ?? selection.name;
1014
+ if (!(fieldName in obj)) continue;
1015
+ const fieldValue = obj[fieldName];
1016
+ const action = getRequiredAction(selection.directives);
1017
+ if (selection.selections) {
1018
+ if (action && fieldValue === null) {
1019
+ if (action === "THROW") throw new RequiredFieldError(fieldPath, fieldName);
1020
+ else if (action === "CASCADE") return CASCADE_NULL;
1021
+ }
1022
+ if (fieldValue !== null && fieldValue !== void 0) {
1023
+ if (validateRequiredInner(selection.selections, fieldValue, [...fieldPath, fieldName], validatedMap) === CASCADE_NULL) if (selection.nullable && !getRequiredAction(selection.directives)) obj[fieldName] = null;
1024
+ else return CASCADE_NULL;
1025
+ }
1026
+ } else {
1027
+ if (validated.has(fieldName)) continue;
1028
+ validated.add(fieldName);
1029
+ if (action && fieldValue === null) {
1030
+ if (action === "THROW") throw new RequiredFieldError(fieldPath, fieldName);
1031
+ else if (action === "CASCADE") return CASCADE_NULL;
1032
+ }
1033
+ }
1034
+ } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment") {
1035
+ if (validateRequiredInner(selection.selections, data, fieldPath, validatedMap) === CASCADE_NULL) return CASCADE_NULL;
1036
+ }
1037
+ return data;
1038
+ };
1039
+ const validateRequired = (selections, data, fieldPath = []) => {
1040
+ const result = validateRequiredInner(selections, data, fieldPath);
1041
+ return result === CASCADE_NULL ? null : result;
1042
+ };
1043
+
1044
+ //#endregion
1045
+ //#region src/exchanges/required.ts
1046
+ const requiredExchange = () => {
1047
+ return ({ forward }) => ({
1048
+ name: "required",
1049
+ io: (ops$) => {
1050
+ return pipe(ops$, forward, map((result) => {
1051
+ if (result.operation.variant !== "request" || !result.data) return result;
1052
+ try {
1053
+ return {
1054
+ ...result,
1055
+ data: validateRequired(result.operation.artifact.selections, result.data)
1056
+ };
1057
+ } catch (error) {
1058
+ return {
1059
+ ...result,
1060
+ errors: [new ExchangeError(error instanceof Error ? error.message : String(error), { exchangeName: "required" })]
1061
+ };
1062
+ }
1063
+ }));
1064
+ }
1065
+ });
1236
1066
  };
1237
1067
 
1238
1068
  //#endregion
1239
1069
  //#region src/exchanges/subscription.ts
1070
+ const shouldHandle = (op) => op.variant === "request" && (op.artifact.kind === "subscription" || op.metadata.subscription?.transport === true);
1240
1071
  /**
1241
1072
  * Creates an exchange for handling GraphQL subscriptions using a subscription client.
1242
1073
  *
@@ -1266,9 +1097,10 @@ const fragmentExchange = () => {
1266
1097
  */
1267
1098
  const subscriptionExchange = (options) => {
1268
1099
  const { client } = options;
1269
- return ({ forward }) => {
1270
- return (ops$) => {
1271
- return merge(pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "subscription"), mergeMap((op) => {
1100
+ return ({ forward }) => ({
1101
+ name: "subscription",
1102
+ io: (ops$) => {
1103
+ return merge(pipe(ops$, filter(shouldHandle), mergeMap((op) => {
1272
1104
  const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key));
1273
1105
  return pipe(make((observer) => {
1274
1106
  let unsubscribe;
@@ -1310,41 +1142,73 @@ const subscriptionExchange = (options) => {
1310
1142
  unsubscribe?.();
1311
1143
  };
1312
1144
  }), takeUntil(teardown$));
1313
- })), pipe(ops$, filter((op) => op.variant === "teardown" || op.artifact.kind !== "subscription"), forward));
1314
- };
1145
+ })), pipe(ops$, filter((op) => !shouldHandle(op)), forward));
1146
+ }
1147
+ });
1148
+ };
1149
+
1150
+ //#endregion
1151
+ //#region src/compose.ts
1152
+ /** @internal */
1153
+ const composeExchanges = (options, input) => {
1154
+ const { exchanges } = options;
1155
+ const { client } = input;
1156
+ const extensions = /* @__PURE__ */ new Map();
1157
+ return {
1158
+ io: exchanges.reduceRight((forward, exchange) => {
1159
+ const result = exchange({
1160
+ forward,
1161
+ client
1162
+ });
1163
+ if ("extension" in result) extensions.set(result.name, result.extension);
1164
+ return (ops$) => {
1165
+ return pipe(ops$, share(), result.io, share());
1166
+ };
1167
+ }, input.forward),
1168
+ extensions
1315
1169
  };
1316
1170
  };
1317
1171
 
1318
1172
  //#endregion
1319
1173
  //#region src/scalars.ts
1320
1174
  const parse = (selections, scalars, value) => {
1321
- const parseValue = (selection, value$1) => {
1322
- if (isNullish(value$1)) return value$1;
1323
- if (selection.array && Array.isArray(value$1)) return value$1.map((item) => parseValue({
1175
+ const parseValue = (selection, value, parsedMap) => {
1176
+ if (isNullish$1(value)) return value;
1177
+ if (selection.array && Array.isArray(value)) return value.map((item) => parseValue({
1324
1178
  ...selection,
1325
1179
  array: false
1326
- }, item));
1327
- if (selection.selections) return parseField(selection.selections, value$1);
1180
+ }, item, parsedMap));
1181
+ if (selection.selections) return parseField(selection.selections, value, parsedMap);
1328
1182
  const transformer = scalars[selection.type];
1329
- if (transformer) return transformer.parse(value$1);
1330
- return value$1;
1183
+ if (transformer) return transformer.parse(value);
1184
+ return value;
1331
1185
  };
1332
- const parseField = (selections$1, value$1) => {
1333
- if (isNullish(value$1)) return value$1;
1334
- const data = value$1;
1335
- const fields = {};
1336
- for (const selection of selections$1) if (selection.kind === "Field") {
1186
+ const parseField = (selections, value, parsedMap) => {
1187
+ if (isNullish$1(value)) return value;
1188
+ const data = value;
1189
+ parsedMap ??= /* @__PURE__ */ new WeakMap();
1190
+ let parsed = parsedMap.get(data);
1191
+ if (!parsed) {
1192
+ parsed = /* @__PURE__ */ new Set();
1193
+ parsedMap.set(data, parsed);
1194
+ }
1195
+ for (const selection of selections) if (selection.kind === "Field") {
1337
1196
  const fieldName = selection.alias ?? selection.name;
1338
- const fieldValue = data[fieldName];
1339
- fields[fieldName] = parseValue(selection, fieldValue);
1340
- } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === data.__typename) Object.assign(fields, parseField(selection.selections, value$1));
1341
- return fields;
1197
+ if (!(fieldName in data)) continue;
1198
+ if (selection.selections) data[fieldName] = parseValue(selection, data[fieldName], parsedMap);
1199
+ else {
1200
+ if (parsed.has(fieldName)) continue;
1201
+ parsed.add(fieldName);
1202
+ data[fieldName] = parseValue(selection, data[fieldName], parsedMap);
1203
+ }
1204
+ } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === data.__typename) parseField(selection.selections, value, parsedMap);
1205
+ return data;
1342
1206
  };
1343
1207
  return parseField(selections, value);
1344
1208
  };
1345
1209
  const serialize = (schemaMeta, variableDefs, scalars, variables) => {
1346
1210
  const serializeValue = (variableDef, value) => {
1347
- if (isNullish(value)) return value;
1211
+ if (isNullish$1(value)) return value;
1348
1212
  if (variableDef.array && Array.isArray(value)) return value.map((item) => serializeValue({
1349
1213
  ...variableDef,
1350
1214
  array: false
@@ -1355,11 +1219,11 @@ const serialize = (schemaMeta, variableDefs, scalars, variables) => {
1355
1219
  if (transformer) return transformer.serialize(value);
1356
1220
  return value;
1357
1221
  };
1358
- const serializeField = (variableDefs$1, value) => {
1359
- if (isNullish(value)) return value;
1222
+ const serializeField = (variableDefs, value) => {
1223
+ if (isNullish$1(value)) return value;
1360
1224
  const data = value;
1361
1225
  const fields = {};
1362
- for (const variableDef of variableDefs$1) {
1226
+ for (const variableDef of variableDefs) {
1363
1227
  const variableValue = data[variableDef.name];
1364
1228
  fields[variableDef.name] = serializeValue(variableDef, variableValue);
1365
1229
  }
@@ -1371,8 +1235,9 @@ const serialize = (schemaMeta, variableDefs, scalars, variables) => {
1371
1235
  //#endregion
1372
1236
  //#region src/exchanges/scalar.ts
1373
1237
  const scalarExchange = () => {
1374
- return ({ forward, client }) => {
1375
- return (ops$) => {
1238
+ return ({ forward, client }) => ({
1239
+ name: "scalar",
1240
+ io: (ops$) => {
1376
1241
  return pipe(ops$, map((op) => {
1377
1242
  if (op.variant !== "request" || !op.artifact.variableDefs || !client.scalars) return op;
1378
1243
  return {
@@ -1386,21 +1251,22 @@ const scalarExchange = () => {
1386
1251
  data: parse(result.operation.artifact.selections, client.scalars, result.data)
1387
1252
  };
1388
1253
  }));
1389
- };
1390
- };
1254
+ }
1255
+ });
1391
1256
  };
1392
1257
 
1393
1258
  //#endregion
1394
1259
  //#region src/exchanges/terminal.ts
1395
1260
  const terminalExchange = () => {
1396
- return () => {
1397
- return (ops$) => {
1261
+ return () => ({
1262
+ name: "terminal",
1263
+ io: (ops$) => {
1398
1264
  return pipe(ops$, filter((op) => op.variant !== "teardown"), mergeMap((op) => fromValue({
1399
1265
  operation: op,
1400
1266
  errors: [new ExchangeError("No terminal exchange found in exchange chain. Did you forget to add httpExchange to your exchanges array?", { exchangeName: "terminal" })]
1401
1267
  })));
1402
- };
1403
- };
1268
+ }
1269
+ });
1404
1270
  };
1405
1271
 
1406
1272
  //#endregion
@@ -1420,22 +1286,25 @@ const never = () => {
1420
1286
  var Client = class {
1421
1287
  #schema;
1422
1288
  #scalars;
1289
+ #extensions;
1423
1290
  operations$;
1424
1291
  results$;
1425
1292
  constructor(config) {
1426
1293
  this.#schema = config.schema;
1427
1294
  this.#scalars = config.scalars;
1428
- const exchange = composeExchange({ exchanges: [
1295
+ const { io, extensions } = composeExchanges({ exchanges: [
1296
+ requiredExchange(),
1429
1297
  scalarExchange(),
1430
1298
  ...config.exchanges,
1431
1299
  fragmentExchange(),
1432
1300
  terminalExchange()
1433
- ] });
1434
- this.operations$ = makeSubject();
1435
- this.results$ = exchange({
1301
+ ] }, {
1436
1302
  forward: never,
1437
1303
  client: this
1438
- })(this.operations$.source);
1304
+ });
1305
+ this.#extensions = extensions;
1306
+ this.operations$ = makeSubject();
1307
+ this.results$ = io(this.operations$.source);
1439
1308
  }
1440
1309
  get schema() {
1441
1310
  return this.#schema;
@@ -1446,11 +1315,11 @@ var Client = class {
1446
1315
  createOperationKey() {
1447
1316
  return Math.random().toString(36).slice(2) + Date.now().toString(36);
1448
1317
  }
1449
- createOperation(artifact, variables) {
1318
+ createOperation(artifact, variables, metadata) {
1450
1319
  return {
1451
1320
  variant: "request",
1452
1321
  key: this.createOperationKey(),
1453
- metadata: {},
1322
+ metadata: { ...metadata },
1454
1323
  artifact,
1455
1324
  variables: variables ?? {}
1456
1325
  };
@@ -1463,27 +1332,50 @@ var Client = class {
1463
1332
  })), share());
1464
1333
  }
1465
1334
  executeQuery(artifact, ...[variables, options]) {
1466
- const operation = this.createOperation(artifact, variables);
1335
+ const operation = this.createOperation(artifact, variables, options?.metadata);
1467
1336
  return this.executeOperation(operation);
1468
1337
  }
1469
1338
  executeMutation(artifact, ...[variables, options]) {
1470
- const operation = this.createOperation(artifact, variables);
1339
+ const operation = this.createOperation(artifact, variables, options?.metadata);
1471
1340
  return this.executeOperation(operation);
1472
1341
  }
1473
1342
  executeSubscription(artifact, ...[variables, options]) {
1474
- const operation = this.createOperation(artifact, variables);
1343
+ const operation = this.createOperation(artifact, variables, options?.metadata);
1475
1344
  return this.executeOperation(operation);
1476
1345
  }
1477
1346
  executeFragment(artifact, fragmentRef, options) {
1478
1347
  const operation = {
1479
1348
  variant: "request",
1480
1349
  key: this.createOperationKey(),
1481
- metadata: { fragmentRef },
1350
+ metadata: {
1351
+ ...options?.metadata,
1352
+ fragmentRef
1353
+ },
1482
1354
  artifact,
1483
1355
  variables: {}
1484
1356
  };
1485
1357
  return this.executeOperation(operation);
1486
1358
  }
1359
+ async query(artifact, ...[variables, options]) {
1360
+ const operation = this.createOperation(artifact, variables, options?.metadata);
1361
+ const result = await pipe(this.executeOperation(operation), take(1), collect);
1362
+ if (result.errors && result.errors.length > 0) throw new AggregatedError(result.errors);
1363
+ return result.data;
1364
+ }
1365
+ async mutation(artifact, ...[variables, options]) {
1366
+ const operation = this.createOperation(artifact, variables, options?.metadata);
1367
+ const result = await pipe(this.executeOperation(operation), take(1), collect);
1368
+ if (result.errors && result.errors.length > 0) throw new AggregatedError(result.errors);
1369
+ return result.data;
1370
+ }
1371
+ extension(name) {
1372
+ const ext = this.#extensions.get(name);
1373
+ if (!ext) throw new Error(`Exchange extension '${name}' is not registered. Check your exchange configuration.`);
1374
+ return ext;
1375
+ }
1376
+ maybeExtension(name) {
1377
+ return this.#extensions.get(name);
1378
+ }
1487
1379
  dispose() {
1488
1380
  this.operations$.complete();
1489
1381
  }
@@ -1493,4 +1385,4 @@ const createClient = (config) => {
1493
1385
  };
1494
1386
 
1495
1387
  //#endregion
1496
- export { AggregatedError, Client, ExchangeError, GraphQLError, cacheExchange, composeExchange, createClient, dedupExchange, fragmentExchange, httpExchange, isAggregatedError, isExchangeError, isGraphQLError, retryExchange, stringify, subscriptionExchange };
1388
+ export { AggregatedError, Client, ExchangeError, GraphQLError, RequiredFieldError, cacheExchange, createClient, dedupExchange, fragmentExchange, httpExchange, isAggregatedError, isExchangeError, isGraphQLError, requiredExchange, retryExchange, stringify, subscriptionExchange };