@mearie/core 0.1.2 → 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.
package/dist/index.cjs CHANGED
@@ -1,4 +1,5 @@
1
- const require_stream = require('./stream-DXHFB0xP.cjs');
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ const require_make = require('./make-DxW2Pxe-.cjs');
2
3
 
3
4
  //#region src/errors.ts
4
5
  /**
@@ -74,30 +75,14 @@ const isAggregatedError = (error) => {
74
75
  return error instanceof AggregatedError;
75
76
  };
76
77
 
77
- //#endregion
78
- //#region src/exchanges/compose.ts
79
- const composeExchange = (options) => {
80
- const { exchanges } = options;
81
- return ({ forward, client }) => {
82
- return exchanges.reduceRight((forward$1, exchange) => {
83
- return (ops$) => {
84
- return require_stream.pipe(ops$, require_stream.share(), exchange({
85
- forward: forward$1,
86
- client
87
- }), require_stream.share());
88
- };
89
- }, forward);
90
- };
91
- };
92
-
93
78
  //#endregion
94
79
  //#region src/exchanges/http.ts
95
- const executeFetch = async ({ url, fetchOptions, operation, signal }) => {
80
+ const executeFetch = async ({ url, fetchFn, fetchOptions, operation, signal }) => {
96
81
  const { artifact, variables } = operation;
97
82
  let response;
98
83
  try {
99
84
  await Promise.resolve();
100
- response = await fetch(url, {
85
+ response = await fetchFn(url, {
101
86
  method: "POST",
102
87
  mode: fetchOptions.mode,
103
88
  credentials: fetchOptions.credentials,
@@ -152,16 +137,18 @@ const executeFetch = async ({ url, fetchOptions, operation, signal }) => {
152
137
  };
153
138
  };
154
139
  const httpExchange = (options) => {
155
- const { url, headers, mode, credentials } = options;
156
- return ({ forward }) => {
157
- return (ops$) => {
140
+ const { url, headers, mode, credentials, fetch: fetchFn = globalThis.fetch } = options;
141
+ return ({ forward }) => ({
142
+ name: "http",
143
+ io: (ops$) => {
158
144
  const inflight = /* @__PURE__ */ new Map();
159
- return require_stream.merge(require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "request" && (op.artifact.kind === "query" || op.artifact.kind === "mutation")), require_stream.mergeMap((op) => {
145
+ return require_make.merge(require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && (op.artifact.kind === "query" || op.artifact.kind === "mutation")), require_make.mergeMap((op) => {
160
146
  inflight.get(op.key)?.abort();
161
147
  const controller = new AbortController();
162
148
  inflight.set(op.key, controller);
163
- return require_stream.fromPromise(executeFetch({
149
+ return require_make.fromPromise(executeFetch({
164
150
  url,
151
+ fetchFn,
165
152
  fetchOptions: {
166
153
  mode,
167
154
  credentials,
@@ -173,14 +160,14 @@ const httpExchange = (options) => {
173
160
  inflight.delete(op.key);
174
161
  return result;
175
162
  }));
176
- }), require_stream.filter((result) => result !== null)), require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "teardown" || op.variant === "request" && (op.artifact.kind === "subscription" || op.artifact.kind === "fragment")), require_stream.tap((op) => {
163
+ }), require_make.filter((result) => result !== null)), require_make.pipe(ops$, require_make.filter((op) => op.variant === "teardown" || op.variant === "request" && (op.artifact.kind === "subscription" || op.artifact.kind === "fragment")), require_make.tap((op) => {
177
164
  if (op.variant === "teardown") {
178
165
  inflight.get(op.key)?.abort();
179
166
  inflight.delete(op.key);
180
167
  }
181
168
  }), forward));
182
- };
183
- };
169
+ }
170
+ });
184
171
  };
185
172
 
186
173
  //#endregion
@@ -247,7 +234,7 @@ const stringify = (value) => {
247
234
  * @param value - Value to check.
248
235
  * @returns True if the value is nullish.
249
236
  */
250
- const isNullish = (value) => {
237
+ const isNullish$1 = (value) => {
251
238
  return value === void 0 || value === null;
252
239
  };
253
240
 
@@ -272,16 +259,17 @@ const makeDedupKey = (op) => {
272
259
  * @returns An exchange that deduplicates in-flight operations.
273
260
  */
274
261
  const dedupExchange = () => {
275
- return ({ forward }) => {
276
- return (ops$) => {
262
+ return ({ forward }) => ({
263
+ name: "dedup",
264
+ io: (ops$) => {
277
265
  const operations = /* @__PURE__ */ new Map();
278
- return require_stream.pipe(require_stream.merge(require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "fragment"))), require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "request" && op.artifact.kind !== "mutation" && op.artifact.kind !== "fragment"), require_stream.filter((op) => {
266
+ return require_make.pipe(require_make.merge(require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "fragment"))), require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind !== "mutation" && op.artifact.kind !== "fragment"), require_make.filter((op) => {
279
267
  const dedupKey = makeDedupKey(op);
280
268
  const isInflight = operations.has(dedupKey);
281
269
  if (isInflight) operations.get(dedupKey).add(op.key);
282
270
  else operations.set(dedupKey, new Set([op.key]));
283
271
  return (op.metadata.dedup?.skip ?? false) || !isInflight;
284
- }), delay(0)), require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "teardown"), require_stream.filter((teardown) => {
272
+ }), delay(0)), require_make.pipe(ops$, require_make.filter((op) => op.variant === "teardown"), require_make.filter((teardown) => {
285
273
  for (const [dedupKey, subs] of operations.entries()) if (subs.delete(teardown.key)) {
286
274
  if (subs.size === 0) {
287
275
  operations.delete(dedupKey);
@@ -290,10 +278,10 @@ const dedupExchange = () => {
290
278
  return false;
291
279
  }
292
280
  return true;
293
- }))), forward, require_stream.mergeMap((result) => {
294
- if (result.operation.variant !== "request" || result.operation.artifact.kind === "mutation" || result.operation.artifact.kind === "fragment") return require_stream.fromValue(result);
281
+ }))), forward, require_make.mergeMap((result) => {
282
+ if (result.operation.variant !== "request" || result.operation.artifact.kind === "mutation" || result.operation.artifact.kind === "fragment") return require_make.fromValue(result);
295
283
  const dedupKey = makeDedupKey(result.operation);
296
- return require_stream.fromArray([...operations.get(dedupKey) ?? /* @__PURE__ */ new Set()].map((key) => ({
284
+ return require_make.fromArray([...operations.get(dedupKey) ?? /* @__PURE__ */ new Set()].map((key) => ({
297
285
  ...result,
298
286
  operation: {
299
287
  ...result.operation,
@@ -301,8 +289,8 @@ const dedupExchange = () => {
301
289
  }
302
290
  })));
303
291
  }));
304
- };
305
- };
292
+ }
293
+ });
306
294
  };
307
295
 
308
296
  //#endregion
@@ -361,6 +349,15 @@ const makeFieldKey = (selection, variables) => {
361
349
  return `${selection.name}@${args}`;
362
350
  };
363
351
  /**
352
+ * Generates a unique key for tracking memoized denormalized results for structural sharing.
353
+ * @internal
354
+ * @param kind - The operation kind ('query', 'fragment', 'fragments').
355
+ * @param name - The artifact name.
356
+ * @param id - Serialized identifier (variables, entity key, etc.).
357
+ * @returns A unique memo key.
358
+ */
359
+ const makeMemoKey = (kind, name, id) => `${kind}:${name}:${id}`;
360
+ /**
364
361
  * Gets a unique key for tracking a field dependency.
365
362
  * @internal
366
363
  * @param storageKey Storage key (entity or root query key).
@@ -389,46 +386,166 @@ const isFragmentRef = (value) => {
389
386
  return typeof value === "object" && value !== null && FragmentRefKey in value;
390
387
  };
391
388
  /**
389
+ * Type guard to check if a value is an array of fragment references.
390
+ * @internal
391
+ * @param value - Value to check.
392
+ * @returns True if the value is a FragmentRef array.
393
+ */
394
+ const isFragmentRefArray = (value) => {
395
+ return Array.isArray(value) && value.length > 0 && isFragmentRef(value[0]);
396
+ };
397
+ /**
392
398
  * Type guard to check if a value is nullish.
393
399
  * @internal
394
400
  * @param value - Value to check.
395
401
  * @returns True if the value is nullish.
396
402
  */
397
- const isNullish$1 = (value) => {
403
+ const isNullish = (value) => {
398
404
  return value === void 0 || value === null;
399
405
  };
406
+ /**
407
+ * Deep equality check for normalized cache values.
408
+ * Handles scalars, arrays, and plain objects (entity links, value objects).
409
+ * @internal
410
+ */
411
+ const isEqual = (a, b) => {
412
+ if (a === b) return true;
413
+ if (typeof a !== typeof b || a === null || b === null) return false;
414
+ if (Array.isArray(a)) {
415
+ if (!Array.isArray(b) || a.length !== b.length) return false;
416
+ for (const [i, item] of a.entries()) if (!isEqual(item, b[i])) return false;
417
+ return true;
418
+ }
419
+ if (typeof a === "object") {
420
+ const aObj = a;
421
+ const bObj = b;
422
+ const aKeys = Object.keys(aObj);
423
+ if (aKeys.length !== Object.keys(bObj).length) return false;
424
+ for (const key of aKeys) if (!isEqual(aObj[key], bObj[key])) return false;
425
+ return true;
426
+ }
427
+ return false;
428
+ };
429
+ /**
430
+ * Recursively replaces a new value tree with the previous one wherever structurally equal,
431
+ * preserving referential identity for unchanged subtrees.
432
+ *
433
+ * Returns `prev` (same reference) when the entire subtree is structurally equal.
434
+ * @internal
435
+ */
436
+ const replaceEqualDeep = (prev, next) => {
437
+ if (prev === next) return prev;
438
+ if (typeof prev !== typeof next || prev === null || next === null || typeof prev !== "object") return next;
439
+ if (Array.isArray(prev)) {
440
+ if (!Array.isArray(next)) return next;
441
+ let allSame = prev.length === next.length;
442
+ const result = [];
443
+ for (const [i, item] of next.entries()) {
444
+ const shared = i < prev.length ? replaceEqualDeep(prev[i], item) : item;
445
+ result.push(shared);
446
+ if (shared !== prev[i]) allSame = false;
447
+ }
448
+ return allSame ? prev : result;
449
+ }
450
+ if (Array.isArray(next)) return next;
451
+ const prevObj = prev;
452
+ const nextObj = next;
453
+ const nextKeys = Object.keys(nextObj);
454
+ const prevKeys = Object.keys(prevObj);
455
+ let allSame = nextKeys.length === prevKeys.length;
456
+ const result = {};
457
+ for (const key of nextKeys) if (key in prevObj) {
458
+ result[key] = replaceEqualDeep(prevObj[key], nextObj[key]);
459
+ if (result[key] !== prevObj[key]) allSame = false;
460
+ } else {
461
+ result[key] = nextObj[key];
462
+ allSame = false;
463
+ }
464
+ return allSame ? prev : result;
465
+ };
466
+ /**
467
+ * Deeply merges two values. Objects are recursively merged, arrays are element-wise merged,
468
+ * entity links and primitives use last-write-wins.
469
+ * @internal
470
+ */
471
+ const mergeFieldValue = (existing, incoming) => {
472
+ if (isNullish(existing) || isNullish(incoming)) return incoming;
473
+ if (typeof existing !== "object" || typeof incoming !== "object") return incoming;
474
+ if (isEntityLink(existing) || isEntityLink(incoming)) return incoming;
475
+ if (Array.isArray(existing) && Array.isArray(incoming)) return incoming.map((item, i) => i < existing.length ? mergeFieldValue(existing[i], item) : item);
476
+ if (Array.isArray(existing) || Array.isArray(incoming)) return incoming;
477
+ mergeFields(existing, incoming);
478
+ return existing;
479
+ };
480
+ /**
481
+ * Deeply merges source fields into target. Objects are recursively merged,
482
+ * arrays are element-wise merged, entity links and primitives use last-write-wins.
483
+ * @internal
484
+ */
485
+ const mergeFields = (target, source) => {
486
+ if (isNullish(source) || typeof source !== "object" || Array.isArray(source)) return;
487
+ for (const key of Object.keys(source)) target[key] = mergeFieldValue(target[key], source[key]);
488
+ };
489
+ /**
490
+ * Creates a FieldKey from a raw field name and optional arguments.
491
+ * @internal
492
+ * @param field - The field name.
493
+ * @param args - Optional argument values.
494
+ * @returns A FieldKey in "field@args" format.
495
+ */
496
+ const makeFieldKeyFromArgs = (field, args) => {
497
+ return `${field}@${args && Object.keys(args).length > 0 ? stringify(args) : "{}"}`;
498
+ };
499
+ /**
500
+ * Converts an EntityId to an EntityKey.
501
+ * @internal
502
+ * @param typename - The GraphQL typename of the entity.
503
+ * @param id - The entity identifier (string, number, or composite key record).
504
+ * @param keyFields - Optional ordered list of key field names for composite keys.
505
+ * @returns An EntityKey.
506
+ */
507
+ const resolveEntityKey = (typename, id, keyFields) => {
508
+ if (typeof id === "string" || typeof id === "number") return makeEntityKey(typename, [id]);
509
+ return makeEntityKey(typename, keyFields ? keyFields.map((f) => id[f]) : Object.values(id));
510
+ };
400
511
 
401
512
  //#endregion
402
513
  //#region src/cache/normalize.ts
514
+ const SKIP = Symbol();
403
515
  const normalize = (schemaMeta, selections, storage, data, variables, accessor) => {
404
- const normalizeField = (storageKey, selections$1, value) => {
405
- if (isNullish$1(value)) return value;
406
- if (Array.isArray(value)) return value.map((item) => normalizeField(storageKey, selections$1, item));
407
- const data$1 = value;
408
- const typename = data$1.__typename;
516
+ const normalizeField = (storageKey, selections, value) => {
517
+ if (isNullish(value)) return value;
518
+ if (Array.isArray(value)) return value.map((item) => normalizeField(storageKey, selections, item));
519
+ const data = value;
520
+ const typename = data.__typename;
409
521
  const entityMeta = schemaMeta.entities[typename];
410
- if (entityMeta) storageKey = makeEntityKey(typename, entityMeta.keyFields.map((field) => data$1[field]));
411
- const fields$1 = {};
412
- for (const selection of selections$1) if (selection.kind === "Field") {
522
+ if (entityMeta) {
523
+ const keys = entityMeta.keyFields.map((field) => data[field]);
524
+ if (!keys.every((k) => k !== void 0 && k !== null)) return SKIP;
525
+ storageKey = makeEntityKey(typename, keys);
526
+ }
527
+ const fields = {};
528
+ for (const selection of selections) if (selection.kind === "Field") {
413
529
  const fieldKey = makeFieldKey(selection, variables);
414
- const fieldValue = data$1[selection.alias ?? selection.name];
415
- if (storageKey !== null) {
416
- const oldValue = storage[storageKey]?.[fieldKey];
417
- if (!selection.selections || isNullish$1(oldValue) || isNullish$1(fieldValue)) accessor?.(storageKey, fieldKey, oldValue, fieldValue);
418
- }
419
- fields$1[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
530
+ const fieldValue = data[selection.alias ?? selection.name];
531
+ const oldValue = storageKey === null ? void 0 : storage[storageKey]?.[fieldKey];
532
+ if (storageKey !== null && (!selection.selections || isNullish(oldValue) || isNullish(fieldValue))) accessor?.(storageKey, fieldKey, oldValue, fieldValue);
533
+ const normalized = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
534
+ if (normalized === SKIP) continue;
535
+ fields[fieldKey] = normalized;
536
+ if (storageKey !== null && selection.selections && !isNullish(oldValue) && !isNullish(fieldValue) && !isEntityLink(fields[fieldKey]) && !isEqual(oldValue, fields[fieldKey])) accessor?.(storageKey, fieldKey, oldValue, fields[fieldKey]);
420
537
  } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === typename) {
421
538
  const inner = normalizeField(storageKey, selection.selections, value);
422
- if (!isEntityLink(inner)) Object.assign(fields$1, inner);
539
+ if (inner !== SKIP && !isEntityLink(inner)) mergeFields(fields, inner);
423
540
  }
424
541
  if (entityMeta && storageKey !== null) {
425
542
  storage[storageKey] = {
426
543
  ...storage[storageKey],
427
- ...fields$1
544
+ ...fields
428
545
  };
429
546
  return { [EntityLinkKey]: storageKey };
430
547
  }
431
- return fields$1;
548
+ return fields;
432
549
  };
433
550
  const fields = normalizeField(RootFieldKey, selections, data);
434
551
  storage[RootFieldKey] = {
@@ -446,10 +563,10 @@ const typenameFieldKey = makeFieldKey({
446
563
  }, {});
447
564
  const denormalize = (selections, storage, value, variables, accessor) => {
448
565
  let partial = false;
449
- const denormalizeField = (storageKey, selections$1, value$1) => {
450
- if (isNullish$1(value$1)) return value$1;
451
- if (Array.isArray(value$1)) return value$1.map((item) => denormalizeField(storageKey, selections$1, item));
452
- const data = value$1;
566
+ const denormalizeField = (storageKey, selections, value) => {
567
+ if (isNullish(value)) return value;
568
+ if (Array.isArray(value)) return value.map((item) => denormalizeField(storageKey, selections, item));
569
+ const data = value;
453
570
  if (isEntityLink(data)) {
454
571
  const entityKey = data[EntityLinkKey];
455
572
  const entity = storage[entityKey];
@@ -458,10 +575,10 @@ const denormalize = (selections, storage, value, variables, accessor) => {
458
575
  partial = true;
459
576
  return null;
460
577
  }
461
- return denormalizeField(entityKey, selections$1, entity);
578
+ return denormalizeField(entityKey, selections, entity);
462
579
  }
463
580
  const fields = {};
464
- for (const selection of selections$1) if (selection.kind === "Field") {
581
+ for (const selection of selections) if (selection.kind === "Field") {
465
582
  const fieldKey = makeFieldKey(selection, variables);
466
583
  const fieldValue = data[fieldKey];
467
584
  if (storageKey !== null) accessor?.(storageKey, fieldKey);
@@ -469,10 +586,13 @@ const denormalize = (selections, storage, value, variables, accessor) => {
469
586
  partial = true;
470
587
  continue;
471
588
  }
472
- fields[selection.alias ?? selection.name] = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
589
+ const name = selection.alias ?? selection.name;
590
+ const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
591
+ if (name in fields) mergeFields(fields, { [name]: value });
592
+ else fields[name] = value;
473
593
  } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) fields[FragmentRefKey] = storageKey;
474
- else Object.assign(fields, denormalizeField(storageKey, selection.selections, value$1));
475
- else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) Object.assign(fields, denormalizeField(storageKey, selection.selections, value$1));
594
+ else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
595
+ else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
476
596
  return fields;
477
597
  };
478
598
  return {
@@ -491,6 +611,7 @@ var Cache = class {
491
611
  #schemaMeta;
492
612
  #storage = { [RootFieldKey]: {} };
493
613
  #subscriptions = /* @__PURE__ */ new Map();
614
+ #memo = /* @__PURE__ */ new Map();
494
615
  constructor(schemaMetadata) {
495
616
  this.#schemaMeta = schemaMetadata;
496
617
  }
@@ -517,13 +638,19 @@ var Cache = class {
517
638
  }
518
639
  /**
519
640
  * Reads a query result from the cache, denormalizing entities if available.
641
+ * Uses structural sharing to preserve referential identity for unchanged subtrees.
520
642
  * @param artifact - GraphQL document artifact.
521
643
  * @param variables - Query variables.
522
644
  * @returns Denormalized query result or null if not found.
523
645
  */
524
646
  readQuery(artifact, variables) {
525
647
  const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables);
526
- return partial ? null : data;
648
+ if (partial) return null;
649
+ const key = makeMemoKey("query", artifact.name, stringify(variables));
650
+ const prev = this.#memo.get(key);
651
+ const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
652
+ this.#memo.set(key, result);
653
+ return result;
527
654
  }
528
655
  /**
529
656
  * Subscribes to cache invalidations for a specific query.
@@ -542,8 +669,7 @@ var Cache = class {
542
669
  }
543
670
  /**
544
671
  * Reads a fragment from the cache for a specific entity.
545
- * Returns null for invalid or missing fragment references, making it safe for
546
- * defensive reads. For subscriptions, use subscribeFragment which throws errors.
672
+ * Uses structural sharing to preserve referential identity for unchanged subtrees.
547
673
  * @param artifact - GraphQL fragment artifact.
548
674
  * @param fragmentRef - Fragment reference containing entity key.
549
675
  * @returns Denormalized fragment data or null if not found or invalid.
@@ -552,7 +678,12 @@ var Cache = class {
552
678
  const entityKey = fragmentRef[FragmentRefKey];
553
679
  if (!this.#storage[entityKey]) return null;
554
680
  const { data, partial } = denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {});
555
- return partial ? null : data;
681
+ if (partial) return null;
682
+ const key = makeMemoKey("fragment", artifact.name, entityKey);
683
+ const prev = this.#memo.get(key);
684
+ const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
685
+ this.#memo.set(key, result);
686
+ return result;
556
687
  }
557
688
  subscribeFragment(artifact, fragmentRef, listener) {
558
689
  const entityKey = fragmentRef[FragmentRefKey];
@@ -563,6 +694,80 @@ var Cache = class {
563
694
  });
564
695
  return this.#subscribe(dependencies, listener);
565
696
  }
697
+ readFragments(artifact, fragmentRefs) {
698
+ const results = [];
699
+ for (const ref of fragmentRefs) {
700
+ const data = this.readFragment(artifact, ref);
701
+ if (data === null) return null;
702
+ results.push(data);
703
+ }
704
+ const entityKeys = fragmentRefs.map((ref) => ref[FragmentRefKey]);
705
+ const key = makeMemoKey("fragments", artifact.name, entityKeys.join(","));
706
+ const prev = this.#memo.get(key);
707
+ const result = prev === void 0 ? results : replaceEqualDeep(prev, results);
708
+ this.#memo.set(key, result);
709
+ return result;
710
+ }
711
+ subscribeFragments(artifact, fragmentRefs, listener) {
712
+ const dependencies = /* @__PURE__ */ new Set();
713
+ for (const ref of fragmentRefs) {
714
+ const entityKey = ref[FragmentRefKey];
715
+ denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
716
+ dependencies.add(makeDependencyKey(storageKey, fieldKey));
717
+ });
718
+ }
719
+ return this.#subscribe(dependencies, listener);
720
+ }
721
+ /**
722
+ * Invalidates one or more cache entries and notifies affected subscribers.
723
+ * @param targets - Cache entries to invalidate.
724
+ */
725
+ invalidate(...targets) {
726
+ const subscriptions = /* @__PURE__ */ new Set();
727
+ for (const target of targets) if (target.__typename === "Query") if ("field" in target) {
728
+ const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
729
+ delete this.#storage[RootFieldKey]?.[fieldKey];
730
+ this.#collectSubscriptions(RootFieldKey, fieldKey, subscriptions);
731
+ } else {
732
+ this.#storage[RootFieldKey] = {};
733
+ this.#collectSubscriptions(RootFieldKey, void 0, subscriptions);
734
+ }
735
+ else if ("id" in target) {
736
+ const entityKey = resolveEntityKey(target.__typename, target.id, this.#schemaMeta.entities[target.__typename]?.keyFields);
737
+ if ("field" in target) {
738
+ const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
739
+ delete this.#storage[entityKey]?.[fieldKey];
740
+ this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
741
+ } else {
742
+ delete this.#storage[entityKey];
743
+ this.#collectSubscriptions(entityKey, void 0, subscriptions);
744
+ }
745
+ } else {
746
+ const prefix = `${target.__typename}:`;
747
+ for (const key of Object.keys(this.#storage)) if (key.startsWith(prefix)) {
748
+ const entityKey = key;
749
+ if ("field" in target) {
750
+ const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
751
+ delete this.#storage[entityKey]?.[fieldKey];
752
+ this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
753
+ } else {
754
+ delete this.#storage[entityKey];
755
+ this.#collectSubscriptions(entityKey, void 0, subscriptions);
756
+ }
757
+ }
758
+ }
759
+ for (const subscription of subscriptions) subscription.listener();
760
+ }
761
+ #collectSubscriptions(storageKey, fieldKey, out) {
762
+ if (fieldKey === void 0) {
763
+ const prefix = `${storageKey}.`;
764
+ for (const [depKey, ss] of this.#subscriptions) if (depKey.startsWith(prefix)) for (const s of ss) out.add(s);
765
+ } else {
766
+ const depKey = makeDependencyKey(storageKey, fieldKey);
767
+ const ss = this.#subscriptions.get(depKey);
768
+ if (ss) for (const s of ss) out.add(s);
769
+ }
770
+ }
566
771
  #subscribe(dependencies, listener) {
567
772
  const subscription = { listener };
568
773
  for (const dependency of dependencies) {
@@ -579,62 +784,134 @@ var Cache = class {
579
784
  };
580
785
  }
581
786
  /**
787
+ * Extracts a serializable snapshot of the cache storage and structural sharing state.
788
+ */
789
+ extract() {
790
+ return {
791
+ storage: structuredClone(this.#storage),
792
+ memo: Object.fromEntries(this.#memo)
793
+ };
794
+ }
795
+ /**
796
+ * Hydrates the cache with a previously extracted snapshot.
797
+ */
798
+ hydrate(snapshot) {
799
+ const { storage, memo } = snapshot;
800
+ for (const [key, fields] of Object.entries(storage)) this.#storage[key] = {
801
+ ...this.#storage[key],
802
+ ...fields
803
+ };
804
+ for (const [key, value] of Object.entries(memo)) this.#memo.set(key, value);
805
+ }
806
+ /**
582
807
  * Clears all cache data.
583
808
  */
584
809
  clear() {
585
810
  this.#storage = { [RootFieldKey]: {} };
586
811
  this.#subscriptions.clear();
812
+ this.#memo.clear();
587
813
  }
588
814
  };
589
815
 
816
+ //#endregion
817
+ //#region src/stream/sources/empty.ts
818
+ /**
819
+ * Creates a source that completes immediately without emitting any values.
820
+ * @returns An empty source.
821
+ */
822
+ const empty = () => {
823
+ return (sink) => {
824
+ sink.complete();
825
+ return { unsubscribe() {} };
826
+ };
827
+ };
828
+
590
829
  //#endregion
591
830
  //#region src/exchanges/cache.ts
592
831
  const cacheExchange = (options = {}) => {
593
832
  const { fetchPolicy = "cache-first" } = options;
594
833
  return ({ forward, client }) => {
595
834
  const cache = new Cache(client.schema);
596
- return (ops$) => {
597
- const fragment$ = require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), require_stream.mergeMap((op) => {
598
- const fragmentRef = op.metadata?.fragmentRef;
599
- if (!fragmentRef) return require_stream.fromValue({
600
- operation: op,
601
- errors: [new ExchangeError("Fragment operation missing fragmentRef in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
602
- });
603
- if (!isFragmentRef(fragmentRef)) return require_stream.fromValue({
604
- operation: op,
605
- data: fragmentRef,
606
- errors: []
607
- });
608
- const trigger = require_stream.makeSubject();
609
- const teardown$ = require_stream.pipe(ops$, require_stream.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_stream.tap(() => trigger.complete()));
610
- return require_stream.pipe(require_stream.merge(require_stream.fromValue(void 0), trigger.source), require_stream.switchMap(() => require_stream.fromSubscription(() => cache.readFragment(op.artifact, fragmentRef), () => cache.subscribeFragment(op.artifact, fragmentRef, async () => {
611
- await Promise.resolve();
612
- trigger.next();
613
- }))), require_stream.takeUntil(teardown$), require_stream.map((data) => ({
614
- operation: op,
615
- data,
616
- errors: []
617
- })));
618
- }));
619
- const nonCache$ = require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "subscription" || op.artifact.kind === "query" && fetchPolicy === "network-only")));
620
- const query$ = require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), require_stream.share());
621
- return require_stream.merge(fragment$, require_stream.pipe(query$, require_stream.mergeMap((op) => {
622
- const trigger = require_stream.makeSubject();
623
- const teardown$ = require_stream.pipe(ops$, require_stream.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_stream.tap(() => trigger.complete()));
624
- return require_stream.pipe(require_stream.merge(require_stream.fromValue(void 0), trigger.source), require_stream.switchMap(() => require_stream.fromSubscription(() => cache.readQuery(op.artifact, op.variables), () => cache.subscribeQuery(op.artifact, op.variables, async () => {
625
- await Promise.resolve();
626
- trigger.next();
627
- }))), require_stream.takeUntil(teardown$), require_stream.map((data) => ({
628
- operation: op,
629
- data,
630
- errors: []
631
- })));
632
- }), require_stream.filter((result) => fetchPolicy === "cache-only" || fetchPolicy === "cache-and-network" && result.data !== null || fetchPolicy === "cache-first" && result.data !== null)), require_stream.pipe(require_stream.merge(nonCache$, require_stream.pipe(query$, require_stream.filter((op) => {
633
- const cached = cache.readQuery(op.artifact, op.variables);
634
- return fetchPolicy === "cache-and-network" || cached === null;
635
- })), require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "teardown"))), forward, require_stream.tap((result) => {
636
- if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
637
- }), require_stream.filter((result) => result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only")));
835
+ return {
836
+ name: "cache",
837
+ extension: {
838
+ extract: () => cache.extract(),
839
+ hydrate: (snapshot) => cache.hydrate(snapshot),
840
+ invalidate: (...targets) => cache.invalidate(...targets),
841
+ clear: () => cache.clear()
842
+ },
843
+ io: (ops$) => {
844
+ const fragment$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), require_make.mergeMap((op) => {
845
+ const fragmentRef = op.metadata?.fragmentRef;
846
+ if (!fragmentRef) return require_make.fromValue({
847
+ operation: op,
848
+ errors: [new ExchangeError("Fragment operation missing fragmentRef in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
849
+ });
850
+ if (isFragmentRefArray(fragmentRef)) {
851
+ const trigger = require_make.makeSubject();
852
+ const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => trigger.complete()));
853
+ return require_make.pipe(require_make.merge(require_make.fromValue(void 0), trigger.source), require_make.switchMap(() => require_make.fromSubscription(() => cache.readFragments(op.artifact, fragmentRef), () => cache.subscribeFragments(op.artifact, fragmentRef, async () => {
854
+ await Promise.resolve();
855
+ trigger.next();
856
+ }))), require_make.takeUntil(teardown$), require_make.map((data) => ({
857
+ operation: op,
858
+ data,
859
+ errors: []
860
+ })));
861
+ }
862
+ if (!isFragmentRef(fragmentRef)) return require_make.fromValue({
863
+ operation: op,
864
+ data: fragmentRef,
865
+ errors: []
866
+ });
867
+ const trigger = require_make.makeSubject();
868
+ const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => trigger.complete()));
869
+ return require_make.pipe(require_make.merge(require_make.fromValue(void 0), trigger.source), require_make.switchMap(() => require_make.fromSubscription(() => cache.readFragment(op.artifact, fragmentRef), () => cache.subscribeFragment(op.artifact, fragmentRef, async () => {
870
+ await Promise.resolve();
871
+ trigger.next();
872
+ }))), require_make.takeUntil(teardown$), require_make.map((data) => ({
873
+ operation: op,
874
+ data,
875
+ errors: []
876
+ })));
877
+ }));
878
+ const nonCache$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "subscription" || op.artifact.kind === "query" && fetchPolicy === "network-only")));
879
+ const query$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), require_make.share());
880
+ const refetch$ = require_make.makeSubject();
881
+ return require_make.merge(fragment$, require_make.pipe(query$, require_make.mergeMap((op) => {
882
+ const trigger = require_make.makeSubject();
883
+ let hasData = false;
884
+ const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => trigger.complete()));
885
+ return require_make.pipe(require_make.merge(require_make.fromValue(void 0), trigger.source), require_make.switchMap(() => require_make.fromSubscription(() => cache.readQuery(op.artifact, op.variables), () => cache.subscribeQuery(op.artifact, op.variables, async () => {
886
+ await Promise.resolve();
887
+ trigger.next();
888
+ }))), require_make.takeUntil(teardown$), require_make.mergeMap((data) => {
889
+ if (data !== null) {
890
+ hasData = true;
891
+ return require_make.fromValue({
892
+ operation: op,
893
+ data,
894
+ errors: []
895
+ });
896
+ }
897
+ if (hasData) {
898
+ refetch$.next(op);
899
+ return empty();
900
+ }
901
+ if (fetchPolicy === "cache-only") return require_make.fromValue({
902
+ operation: op,
903
+ data: null,
904
+ errors: []
905
+ });
906
+ return empty();
907
+ }));
908
+ }), require_make.filter(() => fetchPolicy === "cache-only" || fetchPolicy === "cache-and-network" || fetchPolicy === "cache-first")), require_make.pipe(require_make.merge(nonCache$, require_make.pipe(query$, require_make.filter((op) => {
909
+ const cached = cache.readQuery(op.artifact, op.variables);
910
+ return fetchPolicy === "cache-and-network" || cached === null;
911
+ })), require_make.pipe(ops$, require_make.filter((op) => op.variant === "teardown")), refetch$.source), forward, require_make.tap((result) => {
912
+ if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
913
+ }), require_make.filter((result) => result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0))));
914
+ }
638
915
  };
639
916
  };
640
917
  };
@@ -644,17 +921,18 @@ const cacheExchange = (options = {}) => {
644
921
  const defaultShouldRetry = (error) => isExchangeError(error, "http") && error.extensions?.statusCode !== void 0 && error.extensions.statusCode >= 500;
645
922
  const retryExchange = (options = {}) => {
646
923
  const { maxAttempts = 3, backoff = (attempt) => Math.min(1e3 * 2 ** attempt, 3e4), shouldRetry = defaultShouldRetry } = options;
647
- return ({ forward }) => {
648
- return (ops$) => {
649
- const { source: retries$, next } = require_stream.makeSubject();
924
+ return ({ forward }) => ({
925
+ name: "retry",
926
+ io: (ops$) => {
927
+ const { source: retries$, next } = require_make.makeSubject();
650
928
  const tornDown = /* @__PURE__ */ new Set();
651
- const teardown$ = require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "teardown"), require_stream.tap((op) => {
929
+ const teardown$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "teardown"), require_make.tap((op) => {
652
930
  tornDown.add(op.key);
653
931
  }));
654
- return require_stream.pipe(require_stream.merge(require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "request")), require_stream.pipe(retries$, require_stream.filter((op) => !tornDown.has(op.key)), require_stream.mergeMap((op) => {
655
- const teardown$$1 = require_stream.pipe(ops$, require_stream.filter((operation) => operation.variant === "teardown" && operation.key === op.key));
656
- return require_stream.pipe(require_stream.fromValue(op), delay(op.metadata.retry.delay), require_stream.takeUntil(teardown$$1));
657
- })), teardown$), forward, require_stream.filter((result) => {
932
+ return require_make.pipe(require_make.merge(require_make.pipe(ops$, require_make.filter((op) => op.variant === "request")), require_make.pipe(retries$, require_make.filter((op) => !tornDown.has(op.key)), require_make.mergeMap((op) => {
933
+ const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key));
934
+ return require_make.pipe(require_make.fromValue(op), delay(op.metadata.retry.delay), require_make.takeUntil(teardown$));
935
+ })), teardown$), forward, require_make.filter((result) => {
658
936
  if (!result.errors || result.errors.length === 0) return true;
659
937
  if (result.operation.variant === "request" && result.operation.artifact.kind === "mutation") return true;
660
938
  const attempt = result.operation.metadata.retry?.attempt ?? 0;
@@ -673,16 +951,17 @@ const retryExchange = (options = {}) => {
673
951
  });
674
952
  return false;
675
953
  }));
676
- };
677
- };
954
+ }
955
+ });
678
956
  };
679
957
 
680
958
  //#endregion
681
959
  //#region src/exchanges/fragment.ts
682
960
  const fragmentExchange = () => {
683
- return ({ forward }) => {
684
- return (ops$) => {
685
- return require_stream.merge(require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), require_stream.map((op) => {
961
+ return ({ forward }) => ({
962
+ name: "fragment",
963
+ io: (ops$) => {
964
+ return require_make.merge(require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), require_make.map((op) => {
686
965
  const fragmentRef = op.metadata.fragmentRef;
687
966
  if (!fragmentRef) return {
688
967
  operation: op,
@@ -692,13 +971,104 @@ const fragmentExchange = () => {
692
971
  operation: op,
693
972
  data: fragmentRef
694
973
  };
695
- })), require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "teardown" || op.artifact.kind !== "fragment"), forward));
696
- };
697
- };
974
+ })), require_make.pipe(ops$, require_make.filter((op) => op.variant === "teardown" || op.artifact.kind !== "fragment"), forward));
975
+ }
976
+ });
977
+ };
978
+
979
+ //#endregion
980
+ //#region src/required.ts
981
+ const CASCADE_NULL = Symbol("CASCADE_NULL");
982
+ var RequiredFieldError = class extends Error {
983
+ fieldPath;
984
+ fieldName;
985
+ constructor(fieldPath, fieldName) {
986
+ super(`Required field '${fieldPath.join(".")}.${fieldName}' is null`);
987
+ this.name = "RequiredFieldError";
988
+ this.fieldPath = fieldPath;
989
+ this.fieldName = fieldName;
990
+ }
991
+ };
992
+ const getRequiredAction = (directives) => {
993
+ if (!directives) return null;
994
+ const requiredDirective = directives.find((d) => d.name === "required");
995
+ if (!requiredDirective) return null;
996
+ if (requiredDirective.args?.action === "CASCADE") return "CASCADE";
997
+ return "THROW";
998
+ };
999
+ const validateRequiredInner = (selections, data, fieldPath, validatedMap) => {
1000
+ if (data === null || data === void 0) return data;
1001
+ if (typeof data !== "object") return data;
1002
+ if (Array.isArray(data)) return data.map((item, index) => {
1003
+ const result = validateRequiredInner(selections, item, [...fieldPath, `[${index}]`], validatedMap);
1004
+ return result === CASCADE_NULL ? null : result;
1005
+ });
1006
+ const obj = data;
1007
+ validatedMap ??= /* @__PURE__ */ new WeakMap();
1008
+ let validated = validatedMap.get(obj);
1009
+ if (!validated) {
1010
+ validated = /* @__PURE__ */ new Set();
1011
+ validatedMap.set(obj, validated);
1012
+ }
1013
+ for (const selection of selections) if (selection.kind === "Field") {
1014
+ const fieldName = selection.alias ?? selection.name;
1015
+ if (!(fieldName in obj)) continue;
1016
+ const fieldValue = obj[fieldName];
1017
+ const action = getRequiredAction(selection.directives);
1018
+ if (selection.selections) {
1019
+ if (action && fieldValue === null) {
1020
+ if (action === "THROW") throw new RequiredFieldError(fieldPath, fieldName);
1021
+ else if (action === "CASCADE") return CASCADE_NULL;
1022
+ }
1023
+ if (fieldValue !== null && fieldValue !== void 0) {
1024
+ if (validateRequiredInner(selection.selections, fieldValue, [...fieldPath, fieldName], validatedMap) === CASCADE_NULL) if (selection.nullable && !getRequiredAction(selection.directives)) obj[fieldName] = null;
1025
+ else return CASCADE_NULL;
1026
+ }
1027
+ } else {
1028
+ if (validated.has(fieldName)) continue;
1029
+ validated.add(fieldName);
1030
+ if (action && fieldValue === null) {
1031
+ if (action === "THROW") throw new RequiredFieldError(fieldPath, fieldName);
1032
+ else if (action === "CASCADE") return CASCADE_NULL;
1033
+ }
1034
+ }
1035
+ } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment") {
1036
+ if (validateRequiredInner(selection.selections, data, fieldPath, validatedMap) === CASCADE_NULL) return CASCADE_NULL;
1037
+ }
1038
+ return data;
1039
+ };
1040
+ const validateRequired = (selections, data, fieldPath = []) => {
1041
+ const result = validateRequiredInner(selections, data, fieldPath);
1042
+ return result === CASCADE_NULL ? null : result;
1043
+ };
1044
+
1045
+ //#endregion
1046
+ //#region src/exchanges/required.ts
1047
+ const requiredExchange = () => {
1048
+ return ({ forward }) => ({
1049
+ name: "required",
1050
+ io: (ops$) => {
1051
+ return require_make.pipe(ops$, forward, require_make.map((result) => {
1052
+ if (result.operation.variant !== "request" || !result.data) return result;
1053
+ try {
1054
+ return {
1055
+ ...result,
1056
+ data: validateRequired(result.operation.artifact.selections, result.data)
1057
+ };
1058
+ } catch (error) {
1059
+ return {
1060
+ ...result,
1061
+ errors: [new ExchangeError(error instanceof Error ? error.message : String(error), { exchangeName: "required" })]
1062
+ };
1063
+ }
1064
+ }));
1065
+ }
1066
+ });
698
1067
  };
699
1068
 
700
1069
  //#endregion
701
1070
  //#region src/exchanges/subscription.ts
1071
+ const shouldHandle = (op) => op.variant === "request" && (op.artifact.kind === "subscription" || op.metadata.subscription?.transport === true);
702
1072
  /**
703
1073
  * Creates an exchange for handling GraphQL subscriptions using a subscription client.
704
1074
  *
@@ -728,11 +1098,12 @@ const fragmentExchange = () => {
728
1098
  */
729
1099
  const subscriptionExchange = (options) => {
730
1100
  const { client } = options;
731
- return ({ forward }) => {
732
- return (ops$) => {
733
- return require_stream.merge(require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "request" && op.artifact.kind === "subscription"), require_stream.mergeMap((op) => {
734
- const teardown$ = require_stream.pipe(ops$, require_stream.filter((operation) => operation.variant === "teardown" && operation.key === op.key));
735
- return require_stream.pipe(require_stream.make((observer) => {
1101
+ return ({ forward }) => ({
1102
+ name: "subscription",
1103
+ io: (ops$) => {
1104
+ return require_make.merge(require_make.pipe(ops$, require_make.filter(shouldHandle), require_make.mergeMap((op) => {
1105
+ const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key));
1106
+ return require_make.pipe(require_make.make((observer) => {
736
1107
  let unsubscribe;
737
1108
  let completed = false;
738
1109
  Promise.resolve().then(() => {
@@ -771,42 +1142,74 @@ const subscriptionExchange = (options) => {
771
1142
  completed = true;
772
1143
  unsubscribe?.();
773
1144
  };
774
- }), require_stream.takeUntil(teardown$));
775
- })), require_stream.pipe(ops$, require_stream.filter((op) => op.variant === "teardown" || op.artifact.kind !== "subscription"), forward));
776
- };
1145
+ }), require_make.takeUntil(teardown$));
1146
+ })), require_make.pipe(ops$, require_make.filter((op) => !shouldHandle(op)), forward));
1147
+ }
1148
+ });
1149
+ };
1150
+
1151
+ //#endregion
1152
+ //#region src/compose.ts
1153
+ /** @internal */
1154
+ const composeExchanges = (options, input) => {
1155
+ const { exchanges } = options;
1156
+ const { client } = input;
1157
+ const extensions = /* @__PURE__ */ new Map();
1158
+ return {
1159
+ io: exchanges.reduceRight((forward, exchange) => {
1160
+ const result = exchange({
1161
+ forward,
1162
+ client
1163
+ });
1164
+ if ("extension" in result) extensions.set(result.name, result.extension);
1165
+ return (ops$) => {
1166
+ return require_make.pipe(ops$, require_make.share(), result.io, require_make.share());
1167
+ };
1168
+ }, input.forward),
1169
+ extensions
777
1170
  };
778
1171
  };
779
1172
 
780
1173
  //#endregion
781
1174
  //#region src/scalars.ts
782
1175
  const parse = (selections, scalars, value) => {
783
- const parseValue = (selection, value$1) => {
784
- if (isNullish(value$1)) return value$1;
785
- if (selection.array && Array.isArray(value$1)) return value$1.map((item) => parseValue({
1176
+ const parseValue = (selection, value, parsedMap) => {
1177
+ if (isNullish$1(value)) return value;
1178
+ if (selection.array && Array.isArray(value)) return value.map((item) => parseValue({
786
1179
  ...selection,
787
1180
  array: false
788
- }, item));
789
- if (selection.selections) return parseField(selection.selections, value$1);
1181
+ }, item, parsedMap));
1182
+ if (selection.selections) return parseField(selection.selections, value, parsedMap);
790
1183
  const transformer = scalars[selection.type];
791
- if (transformer) return transformer.parse(value$1);
792
- return value$1;
1184
+ if (transformer) return transformer.parse(value);
1185
+ return value;
793
1186
  };
794
- const parseField = (selections$1, value$1) => {
795
- if (isNullish(value$1)) return value$1;
796
- const data = value$1;
797
- const fields = {};
798
- for (const selection of selections$1) if (selection.kind === "Field") {
1187
+ const parseField = (selections, value, parsedMap) => {
1188
+ if (isNullish$1(value)) return value;
1189
+ const data = value;
1190
+ parsedMap ??= /* @__PURE__ */ new WeakMap();
1191
+ let parsed = parsedMap.get(data);
1192
+ if (!parsed) {
1193
+ parsed = /* @__PURE__ */ new Set();
1194
+ parsedMap.set(data, parsed);
1195
+ }
1196
+ for (const selection of selections) if (selection.kind === "Field") {
799
1197
  const fieldName = selection.alias ?? selection.name;
800
- const fieldValue = data[fieldName];
801
- fields[fieldName] = parseValue(selection, fieldValue);
802
- } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === data.__typename) Object.assign(fields, parseField(selection.selections, value$1));
803
- return fields;
1198
+ if (!(fieldName in data)) continue;
1199
+ if (selection.selections) data[fieldName] = parseValue(selection, data[fieldName], parsedMap);
1200
+ else {
1201
+ if (parsed.has(fieldName)) continue;
1202
+ parsed.add(fieldName);
1203
+ data[fieldName] = parseValue(selection, data[fieldName], parsedMap);
1204
+ }
1205
+ } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === data.__typename) parseField(selection.selections, value, parsedMap);
1206
+ return data;
804
1207
  };
805
1208
  return parseField(selections, value);
806
1209
  };
807
1210
  const serialize = (schemaMeta, variableDefs, scalars, variables) => {
808
1211
  const serializeValue = (variableDef, value) => {
809
- if (isNullish(value)) return value;
1212
+ if (isNullish$1(value)) return value;
810
1213
  if (variableDef.array && Array.isArray(value)) return value.map((item) => serializeValue({
811
1214
  ...variableDef,
812
1215
  array: false
@@ -817,11 +1220,11 @@ const serialize = (schemaMeta, variableDefs, scalars, variables) => {
817
1220
  if (transformer) return transformer.serialize(value);
818
1221
  return value;
819
1222
  };
820
- const serializeField = (variableDefs$1, value) => {
821
- if (isNullish(value)) return value;
1223
+ const serializeField = (variableDefs, value) => {
1224
+ if (isNullish$1(value)) return value;
822
1225
  const data = value;
823
1226
  const fields = {};
824
- for (const variableDef of variableDefs$1) {
1227
+ for (const variableDef of variableDefs) {
825
1228
  const variableValue = data[variableDef.name];
826
1229
  fields[variableDef.name] = serializeValue(variableDef, variableValue);
827
1230
  }
@@ -833,36 +1236,38 @@ const serialize = (schemaMeta, variableDefs, scalars, variables) => {
833
1236
  //#endregion
834
1237
  //#region src/exchanges/scalar.ts
835
1238
  const scalarExchange = () => {
836
- return ({ forward, client }) => {
837
- return (ops$) => {
838
- return require_stream.pipe(ops$, require_stream.map((op) => {
1239
+ return ({ forward, client }) => ({
1240
+ name: "scalar",
1241
+ io: (ops$) => {
1242
+ return require_make.pipe(ops$, require_make.map((op) => {
839
1243
  if (op.variant !== "request" || !op.artifact.variableDefs || !client.scalars) return op;
840
1244
  return {
841
1245
  ...op,
842
1246
  variables: serialize(client.schema, op.artifact.variableDefs, client.scalars, op.variables)
843
1247
  };
844
- }), forward, require_stream.map((result) => {
1248
+ }), forward, require_make.map((result) => {
845
1249
  if (result.operation.variant !== "request" || !result.data || !client.scalars) return result;
846
1250
  return {
847
1251
  ...result,
848
1252
  data: parse(result.operation.artifact.selections, client.scalars, result.data)
849
1253
  };
850
1254
  }));
851
- };
852
- };
1255
+ }
1256
+ });
853
1257
  };
854
1258
 
855
1259
  //#endregion
856
1260
  //#region src/exchanges/terminal.ts
857
1261
  const terminalExchange = () => {
858
- return () => {
859
- return (ops$) => {
860
- return require_stream.pipe(ops$, require_stream.filter((op) => op.variant !== "teardown"), require_stream.mergeMap((op) => require_stream.fromValue({
1262
+ return () => ({
1263
+ name: "terminal",
1264
+ io: (ops$) => {
1265
+ return require_make.pipe(ops$, require_make.filter((op) => op.variant !== "teardown"), require_make.mergeMap((op) => require_make.fromValue({
861
1266
  operation: op,
862
1267
  errors: [new ExchangeError("No terminal exchange found in exchange chain. Did you forget to add httpExchange to your exchanges array?", { exchangeName: "terminal" })]
863
1268
  })));
864
- };
865
- };
1269
+ }
1270
+ });
866
1271
  };
867
1272
 
868
1273
  //#endregion
@@ -882,22 +1287,25 @@ const never = () => {
882
1287
  var Client = class {
883
1288
  #schema;
884
1289
  #scalars;
1290
+ #extensions;
885
1291
  operations$;
886
1292
  results$;
887
1293
  constructor(config) {
888
1294
  this.#schema = config.schema;
889
1295
  this.#scalars = config.scalars;
890
- const exchange = composeExchange({ exchanges: [
1296
+ const { io, extensions } = composeExchanges({ exchanges: [
1297
+ requiredExchange(),
891
1298
  scalarExchange(),
892
1299
  ...config.exchanges,
893
1300
  fragmentExchange(),
894
1301
  terminalExchange()
895
- ] });
896
- this.operations$ = require_stream.makeSubject();
897
- this.results$ = exchange({
1302
+ ] }, {
898
1303
  forward: never,
899
1304
  client: this
900
- })(this.operations$.source);
1305
+ });
1306
+ this.#extensions = extensions;
1307
+ this.operations$ = require_make.makeSubject();
1308
+ this.results$ = io(this.operations$.source);
901
1309
  }
902
1310
  get schema() {
903
1311
  return this.#schema;
@@ -908,44 +1316,67 @@ var Client = class {
908
1316
  createOperationKey() {
909
1317
  return Math.random().toString(36).slice(2) + Date.now().toString(36);
910
1318
  }
911
- createOperation(artifact, variables) {
1319
+ createOperation(artifact, variables, metadata) {
912
1320
  return {
913
1321
  variant: "request",
914
1322
  key: this.createOperationKey(),
915
- metadata: {},
1323
+ metadata: { ...metadata },
916
1324
  artifact,
917
1325
  variables: variables ?? {}
918
1326
  };
919
1327
  }
920
1328
  executeOperation(operation) {
921
- return require_stream.pipe(this.results$, require_stream.initialize(() => this.operations$.next(operation)), require_stream.filter((result) => result.operation.key === operation.key), require_stream.finalize(() => this.operations$.next({
1329
+ return require_make.pipe(this.results$, require_make.initialize(() => this.operations$.next(operation)), require_make.filter((result) => result.operation.key === operation.key), require_make.finalize(() => this.operations$.next({
922
1330
  variant: "teardown",
923
1331
  key: operation.key,
924
1332
  metadata: {}
925
- })), require_stream.share());
1333
+ })), require_make.share());
926
1334
  }
927
1335
  executeQuery(artifact, ...[variables, options]) {
928
- const operation = this.createOperation(artifact, variables);
1336
+ const operation = this.createOperation(artifact, variables, options?.metadata);
929
1337
  return this.executeOperation(operation);
930
1338
  }
931
1339
  executeMutation(artifact, ...[variables, options]) {
932
- const operation = this.createOperation(artifact, variables);
1340
+ const operation = this.createOperation(artifact, variables, options?.metadata);
933
1341
  return this.executeOperation(operation);
934
1342
  }
935
1343
  executeSubscription(artifact, ...[variables, options]) {
936
- const operation = this.createOperation(artifact, variables);
1344
+ const operation = this.createOperation(artifact, variables, options?.metadata);
937
1345
  return this.executeOperation(operation);
938
1346
  }
939
1347
  executeFragment(artifact, fragmentRef, options) {
940
1348
  const operation = {
941
1349
  variant: "request",
942
1350
  key: this.createOperationKey(),
943
- metadata: { fragmentRef },
1351
+ metadata: {
1352
+ ...options?.metadata,
1353
+ fragmentRef
1354
+ },
944
1355
  artifact,
945
1356
  variables: {}
946
1357
  };
947
1358
  return this.executeOperation(operation);
948
1359
  }
1360
+ async query(artifact, ...[variables, options]) {
1361
+ const operation = this.createOperation(artifact, variables, options?.metadata);
1362
+ const result = await require_make.pipe(this.executeOperation(operation), require_make.take(1), require_make.collect);
1363
+ if (result.errors && result.errors.length > 0) throw new AggregatedError(result.errors);
1364
+ return result.data;
1365
+ }
1366
+ async mutation(artifact, ...[variables, options]) {
1367
+ const operation = this.createOperation(artifact, variables, options?.metadata);
1368
+ const result = await require_make.pipe(this.executeOperation(operation), require_make.take(1), require_make.collect);
1369
+ if (result.errors && result.errors.length > 0) throw new AggregatedError(result.errors);
1370
+ return result.data;
1371
+ }
1372
+ extension(name) {
1373
+ const ext = this.#extensions.get(name);
1374
+ if (!ext) throw new Error(`Exchange extension '${name}' is not registered. Check your exchange configuration.`);
1375
+ return ext;
1376
+ }
1377
+ maybeExtension(name) {
1378
+ return this.#extensions.get(name);
1379
+ }
949
1380
  dispose() {
950
1381
  this.operations$.complete();
951
1382
  }
@@ -959,8 +1390,8 @@ exports.AggregatedError = AggregatedError;
959
1390
  exports.Client = Client;
960
1391
  exports.ExchangeError = ExchangeError;
961
1392
  exports.GraphQLError = GraphQLError;
1393
+ exports.RequiredFieldError = RequiredFieldError;
962
1394
  exports.cacheExchange = cacheExchange;
963
- exports.composeExchange = composeExchange;
964
1395
  exports.createClient = createClient;
965
1396
  exports.dedupExchange = dedupExchange;
966
1397
  exports.fragmentExchange = fragmentExchange;
@@ -968,6 +1399,7 @@ exports.httpExchange = httpExchange;
968
1399
  exports.isAggregatedError = isAggregatedError;
969
1400
  exports.isExchangeError = isExchangeError;
970
1401
  exports.isGraphQLError = isGraphQLError;
1402
+ exports.requiredExchange = requiredExchange;
971
1403
  exports.retryExchange = retryExchange;
972
1404
  exports.stringify = stringify;
973
1405
  exports.subscriptionExchange = subscriptionExchange;