@mearie/core 0.4.0 → 0.5.1

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
@@ -318,6 +318,12 @@ const RootFieldKey = "__root";
318
318
  * @internal
319
319
  */
320
320
  const FragmentRefKey = "__fragmentRef";
321
+ /**
322
+ * Special key used to carry merged variable context (fragment args + operation variables)
323
+ * on fragment references. Used by readFragment to resolve variable-dependent field keys.
324
+ * @internal
325
+ */
326
+ const FragmentVarsKey = "__fragmentVars";
321
327
 
322
328
  //#endregion
323
329
  //#region src/cache/utils.ts
@@ -391,6 +397,14 @@ const isFragmentRef = (value) => {
391
397
  return typeof value === "object" && value !== null && FragmentRefKey in value;
392
398
  };
393
399
  /**
400
+ * Extracts the merged variable context for a specific fragment from a fragment reference.
401
+ * Returns the merged variables (fragment args + operation variables) if present, or an empty object.
402
+ * @internal
403
+ */
404
+ const getFragmentVars = (fragmentRef, fragmentName) => {
405
+ return fragmentRef[FragmentVarsKey]?.[fragmentName] ?? {};
406
+ };
407
+ /**
394
408
  * Type guard to check if a value is an array of fragment references.
395
409
  * @internal
396
410
  * @param value - Value to check.
@@ -504,38 +518,47 @@ const makeFieldKeyFromArgs = (field, args) => {
504
518
 
505
519
  //#endregion
506
520
  //#region src/cache/normalize.ts
507
- const SKIP = Symbol();
521
+ const resolveTypename = (selections, data) => {
522
+ for (const s of selections) if (s.kind === "Field" && s.name === "__typename") return data[s.alias ?? "__typename"];
523
+ return data.__typename;
524
+ };
508
525
  const normalize = (schemaMeta, selections, storage, data, variables, accessor) => {
526
+ const resolveEntityKey = (typename, data) => {
527
+ if (!typename) return null;
528
+ const entityMeta = schemaMeta.entities[typename];
529
+ if (!entityMeta) return null;
530
+ const keys = entityMeta.keyFields.map((field) => data[field]);
531
+ if (keys.every((k) => k !== void 0 && k !== null)) return makeEntityKey(typename, keys);
532
+ return null;
533
+ };
509
534
  const normalizeField = (storageKey, selections, value) => {
510
535
  if (isNullish(value)) return value;
511
536
  if (Array.isArray(value)) return value.map((item) => normalizeField(storageKey, selections, item));
512
537
  const data = value;
513
- const typename = data.__typename;
514
- const entityMeta = schemaMeta.entities[typename];
515
- if (entityMeta) {
516
- const keys = entityMeta.keyFields.map((field) => data[field]);
517
- if (!keys.every((k) => k !== void 0 && k !== null)) return SKIP;
518
- storageKey = makeEntityKey(typename, keys);
519
- }
538
+ const typename = resolveTypename(selections, data);
539
+ const entityKey = resolveEntityKey(typename, data);
540
+ if (entityKey) storageKey = entityKey;
520
541
  const fields = {};
521
542
  for (const selection of selections) if (selection.kind === "Field") {
522
543
  const fieldKey = makeFieldKey(selection, variables);
523
544
  const fieldValue = data[selection.alias ?? selection.name];
545
+ if (storageKey !== null && selection.selections && typeof fieldValue === "object" && fieldValue !== null && !Array.isArray(fieldValue)) {
546
+ const fieldTypename = resolveTypename(selection.selections, fieldValue);
547
+ if (fieldTypename && schemaMeta.entities[fieldTypename] && !resolveEntityKey(fieldTypename, fieldValue) && isEntityLink(storage[storageKey]?.[fieldKey])) continue;
548
+ }
524
549
  const oldValue = storageKey === null ? void 0 : storage[storageKey]?.[fieldKey];
525
550
  if (storageKey !== null && (!selection.selections || isNullish(oldValue) || isNullish(fieldValue))) accessor?.(storageKey, fieldKey, oldValue, fieldValue);
526
- const normalized = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
527
- if (normalized === SKIP) continue;
528
- fields[fieldKey] = normalized;
551
+ fields[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
529
552
  if (storageKey !== null && selection.selections && !isNullish(oldValue) && !isNullish(fieldValue) && !isEntityLink(fields[fieldKey]) && !isEqual(oldValue, fields[fieldKey])) accessor?.(storageKey, fieldKey, oldValue, fields[fieldKey]);
530
553
  } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === typename) {
531
554
  const inner = normalizeField(storageKey, selection.selections, value);
532
- if (inner !== SKIP && !isEntityLink(inner)) mergeFields(fields, inner);
555
+ if (!isEntityLink(inner)) mergeFields(fields, inner);
533
556
  }
534
- if (entityMeta && storageKey !== null) {
535
- const existing = storage[storageKey];
557
+ if (entityKey) {
558
+ const existing = storage[entityKey];
536
559
  if (existing) mergeFields(existing, fields);
537
- else storage[storageKey] = fields;
538
- return { [EntityLinkKey]: storageKey };
560
+ else storage[entityKey] = fields;
561
+ return { [EntityLinkKey]: entityKey };
539
562
  }
540
563
  return fields;
541
564
  };
@@ -584,6 +607,17 @@ const denormalize = (selections, storage, value, variables, accessor) => {
584
607
  else fields[name] = value;
585
608
  } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
586
609
  fields[FragmentRefKey] = storageKey;
610
+ if (selection.args) {
611
+ const resolvedArgs = resolveArguments(selection.args, variables);
612
+ const mergedVars = {
613
+ ...variables,
614
+ ...resolvedArgs
615
+ };
616
+ fields[FragmentVarsKey] = {
617
+ ...fields[FragmentVarsKey],
618
+ [selection.name]: mergedVars
619
+ };
620
+ }
587
621
  if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
588
622
  } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
589
623
  else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
@@ -607,9 +641,74 @@ var Cache = class {
607
641
  #subscriptions = /* @__PURE__ */ new Map();
608
642
  #memo = /* @__PURE__ */ new Map();
609
643
  #stale = /* @__PURE__ */ new Set();
644
+ #optimisticKeys = [];
645
+ #optimisticLayers = /* @__PURE__ */ new Map();
646
+ #storageView = null;
610
647
  constructor(schemaMetadata) {
611
648
  this.#schemaMeta = schemaMetadata;
612
649
  }
650
+ #getStorageView() {
651
+ if (this.#optimisticKeys.length === 0) return this.#storage;
652
+ if (this.#storageView) return this.#storageView;
653
+ const merged = { ...this.#storage };
654
+ for (const storageKey of Object.keys(this.#storage)) merged[storageKey] = { ...this.#storage[storageKey] };
655
+ for (const key of this.#optimisticKeys) {
656
+ const layer = this.#optimisticLayers.get(key);
657
+ if (!layer) continue;
658
+ for (const storageKey of Object.keys(layer.storage)) merged[storageKey] = merged[storageKey] ? {
659
+ ...merged[storageKey],
660
+ ...layer.storage[storageKey]
661
+ } : { ...layer.storage[storageKey] };
662
+ }
663
+ this.#storageView = merged;
664
+ return merged;
665
+ }
666
+ /**
667
+ * Writes an optimistic response to a separate cache layer.
668
+ * The optimistic data is immediately visible in reads but does not affect the base storage.
669
+ * @internal
670
+ * @param key - Unique key identifying this optimistic mutation (typically the operation key).
671
+ * @param artifact - GraphQL document artifact.
672
+ * @param variables - Operation variables.
673
+ * @param data - The optimistic response data.
674
+ */
675
+ writeOptimistic(key, artifact, variables, data) {
676
+ const layerStorage = { [RootFieldKey]: {} };
677
+ const dependencies = /* @__PURE__ */ new Set();
678
+ normalize(this.#schemaMeta, artifact.selections, layerStorage, data, variables, (storageKey, fieldKey) => {
679
+ dependencies.add(makeDependencyKey(storageKey, fieldKey));
680
+ });
681
+ this.#optimisticKeys.push(key);
682
+ this.#optimisticLayers.set(key, {
683
+ storage: layerStorage,
684
+ dependencies
685
+ });
686
+ this.#storageView = null;
687
+ const subscriptions = /* @__PURE__ */ new Set();
688
+ for (const depKey of dependencies) {
689
+ const ss = this.#subscriptions.get(depKey);
690
+ if (ss) for (const s of ss) subscriptions.add(s);
691
+ }
692
+ for (const subscription of subscriptions) subscription.listener();
693
+ }
694
+ /**
695
+ * Removes an optimistic layer and notifies affected subscribers.
696
+ * @internal
697
+ * @param key - The key of the optimistic layer to remove.
698
+ */
699
+ removeOptimistic(key) {
700
+ const layer = this.#optimisticLayers.get(key);
701
+ if (!layer) return;
702
+ this.#optimisticLayers.delete(key);
703
+ this.#optimisticKeys = this.#optimisticKeys.filter((k) => k !== key);
704
+ this.#storageView = null;
705
+ const subscriptions = /* @__PURE__ */ new Set();
706
+ for (const depKey of layer.dependencies) {
707
+ const ss = this.#subscriptions.get(depKey);
708
+ if (ss) for (const s of ss) subscriptions.add(s);
709
+ }
710
+ for (const subscription of subscriptions) subscription.listener();
711
+ }
613
712
  /**
614
713
  * Writes a query result to the cache, normalizing entities.
615
714
  * @param artifact - GraphQL document artifact.
@@ -642,7 +741,8 @@ var Cache = class {
642
741
  */
643
742
  readQuery(artifact, variables) {
644
743
  let stale = false;
645
- const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables, (storageKey, fieldKey) => {
744
+ const storage = this.#getStorageView();
745
+ const { data, partial } = denormalize(artifact.selections, storage, storage[RootFieldKey], variables, (storageKey, fieldKey) => {
646
746
  if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
647
747
  });
648
748
  if (partial) return {
@@ -667,7 +767,8 @@ var Cache = class {
667
767
  */
668
768
  subscribeQuery(artifact, variables, listener) {
669
769
  const dependencies = /* @__PURE__ */ new Set();
670
- denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables, (storageKey, fieldKey) => {
770
+ const storageView = this.#getStorageView();
771
+ denormalize(artifact.selections, storageView, storageView[RootFieldKey], variables, (storageKey, fieldKey) => {
671
772
  const dependencyKey = makeDependencyKey(storageKey, fieldKey);
672
773
  dependencies.add(dependencyKey);
673
774
  });
@@ -682,19 +783,22 @@ var Cache = class {
682
783
  */
683
784
  readFragment(artifact, fragmentRef) {
684
785
  const entityKey = fragmentRef[FragmentRefKey];
685
- if (!this.#storage[entityKey]) return {
786
+ const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
787
+ const storageView = this.#getStorageView();
788
+ if (!storageView[entityKey]) return {
686
789
  data: null,
687
790
  stale: false
688
791
  };
689
792
  let stale = false;
690
- const { data, partial } = denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
793
+ const { data, partial } = denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
691
794
  if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
692
795
  });
693
796
  if (partial) return {
694
797
  data: null,
695
798
  stale: false
696
799
  };
697
- const key = makeMemoKey("fragment", artifact.name, entityKey);
800
+ const argsId = Object.keys(fragmentVars).length > 0 ? entityKey + stringify(fragmentVars) : entityKey;
801
+ const key = makeMemoKey("fragment", artifact.name, argsId);
698
802
  const prev = this.#memo.get(key);
699
803
  const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
700
804
  this.#memo.set(key, result);
@@ -705,8 +809,10 @@ var Cache = class {
705
809
  }
706
810
  subscribeFragment(artifact, fragmentRef, listener) {
707
811
  const entityKey = fragmentRef[FragmentRefKey];
812
+ const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
708
813
  const dependencies = /* @__PURE__ */ new Set();
709
- denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
814
+ const storageView = this.#getStorageView();
815
+ denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
710
816
  const dependencyKey = makeDependencyKey(storageKey, fieldKey);
711
817
  dependencies.add(dependencyKey);
712
818
  });
@@ -736,9 +842,11 @@ var Cache = class {
736
842
  }
737
843
  subscribeFragments(artifact, fragmentRefs, listener) {
738
844
  const dependencies = /* @__PURE__ */ new Set();
845
+ const storageView = this.#getStorageView();
739
846
  for (const ref of fragmentRefs) {
740
847
  const entityKey = ref[FragmentRefKey];
741
- denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
848
+ const fragmentVars = getFragmentVars(ref, artifact.name);
849
+ denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
742
850
  dependencies.add(makeDependencyKey(storageKey, fieldKey));
743
851
  });
744
852
  }
@@ -819,6 +927,7 @@ var Cache = class {
819
927
  }
820
928
  /**
821
929
  * Extracts a serializable snapshot of the cache storage and structural sharing state.
930
+ * Optimistic layers are excluded because they represent transient in-flight state.
822
931
  */
823
932
  extract() {
824
933
  return {
@@ -836,6 +945,7 @@ var Cache = class {
836
945
  ...fields
837
946
  };
838
947
  for (const [key, value] of Object.entries(memo)) this.#memo.set(key, value);
948
+ this.#storageView = null;
839
949
  }
840
950
  /**
841
951
  * Clears all cache data.
@@ -845,6 +955,9 @@ var Cache = class {
845
955
  this.#subscriptions.clear();
846
956
  this.#memo.clear();
847
957
  this.#stale.clear();
958
+ this.#optimisticKeys = [];
959
+ this.#optimisticLayers.clear();
960
+ this.#storageView = null;
848
961
  }
849
962
  };
850
963
 
@@ -877,10 +990,10 @@ const cacheExchange = (options = {}) => {
877
990
  },
878
991
  io: (ops$) => {
879
992
  const fragment$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), require_make.mergeMap((op) => {
880
- const fragmentRef = op.metadata?.fragmentRef;
993
+ const fragmentRef = op.metadata?.fragment?.ref;
881
994
  if (!fragmentRef) return require_make.fromValue({
882
995
  operation: op,
883
- errors: [new ExchangeError("Fragment operation missing fragmentRef in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
996
+ errors: [new ExchangeError("Fragment operation missing fragment.ref in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
884
997
  });
885
998
  if (isFragmentRefArray(fragmentRef)) {
886
999
  const trigger = require_make.makeSubject();
@@ -912,7 +1025,9 @@ const cacheExchange = (options = {}) => {
912
1025
  errors: []
913
1026
  })));
914
1027
  }));
915
- 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")));
1028
+ 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")), require_make.tap((op) => {
1029
+ if (op.artifact.kind === "mutation" && op.metadata?.cache?.optimisticResponse) cache.writeOptimistic(op.key, op.artifact, op.variables, op.metadata.cache.optimisticResponse);
1030
+ }));
916
1031
  const query$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), require_make.share());
917
1032
  const refetch$ = require_make.makeSubject();
918
1033
  return require_make.merge(fragment$, require_make.pipe(query$, require_make.mergeMap((op) => {
@@ -955,9 +1070,18 @@ const cacheExchange = (options = {}) => {
955
1070
  }), 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) => {
956
1071
  const { data } = cache.readQuery(op.artifact, op.variables);
957
1072
  return fetchPolicy === "cache-and-network" || data === null;
958
- })), require_make.pipe(ops$, require_make.filter((op) => op.variant === "teardown")), refetch$.source), forward, require_make.tap((result) => {
1073
+ })), require_make.pipe(ops$, require_make.filter((op) => op.variant === "teardown")), refetch$.source), forward, require_make.mergeMap((result) => {
1074
+ if (result.operation.variant === "request" && result.operation.artifact.kind === "mutation" && result.operation.metadata?.cache?.optimisticResponse) cache.removeOptimistic(result.operation.key);
959
1075
  if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
960
- }), require_make.filter((result) => result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0))));
1076
+ if (result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0)) return require_make.fromValue(result);
1077
+ const { data } = cache.readQuery(result.operation.artifact, result.operation.variables);
1078
+ if (data !== null) return empty();
1079
+ return require_make.fromValue({
1080
+ operation: result.operation,
1081
+ data: void 0,
1082
+ errors: [new ExchangeError("Cache failed to denormalize the network response. This is likely a bug in the cache normalizer.", { exchangeName: "cache" })]
1083
+ });
1084
+ })));
961
1085
  }
962
1086
  };
963
1087
  };
@@ -1009,10 +1133,10 @@ const fragmentExchange = () => {
1009
1133
  name: "fragment",
1010
1134
  io: (ops$) => {
1011
1135
  return require_make.merge(require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), require_make.map((op) => {
1012
- const fragmentRef = op.metadata.fragmentRef;
1136
+ const fragmentRef = op.metadata.fragment?.ref;
1013
1137
  if (!fragmentRef) return {
1014
1138
  operation: op,
1015
- errors: [new ExchangeError("Fragment operation missing fragmentRef in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "fragment" })]
1139
+ errors: [new ExchangeError("Fragment operation missing fragment.ref in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "fragment" })]
1016
1140
  };
1017
1141
  return {
1018
1142
  operation: op,
@@ -1398,7 +1522,7 @@ var Client = class {
1398
1522
  key: this.createOperationKey(),
1399
1523
  metadata: {
1400
1524
  ...options?.metadata,
1401
- fragmentRef
1525
+ fragment: { ref: fragmentRef }
1402
1526
  },
1403
1527
  artifact,
1404
1528
  variables: {}
package/dist/index.d.cts CHANGED
@@ -68,8 +68,8 @@ type QueryOptions<T extends Artifact$1<'query'> = Artifact$1<'query'>> = {
68
68
  initialData?: DataOf$1<T>;
69
69
  metadata?: OperationMetadata;
70
70
  };
71
- type MutationOptions = {
72
- metadata?: OperationMetadata;
71
+ type MutationOptions<T extends Artifact$1<'mutation'> = Artifact$1<'mutation'>> = {
72
+ metadata?: OperationMetadata<T>;
73
73
  };
74
74
  type SubscriptionOptions = {
75
75
  metadata?: OperationMetadata;
@@ -93,14 +93,14 @@ declare class Client<TMeta extends SchemaMeta$1 = SchemaMeta$1> {
93
93
  get schema(): TMeta;
94
94
  get scalars(): ScalarsConfig<TMeta> | undefined;
95
95
  private createOperationKey;
96
- createOperation(artifact: Artifact$1, variables?: unknown, metadata?: OperationMetadata): Operation;
96
+ createOperation(artifact: Artifact$1, variables?: unknown, metadata?: Record<string, unknown>): Operation;
97
97
  executeOperation(operation: Operation): Source<OperationResult>;
98
98
  executeQuery<T extends Artifact$1<'query'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, QueryOptions<T>?] : [VariablesOf$1<T>, QueryOptions<T>?]): Source<OperationResult>;
99
- executeMutation<T extends Artifact$1<'mutation'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, MutationOptions?] : [VariablesOf$1<T>, MutationOptions?]): Source<OperationResult>;
99
+ executeMutation<T extends Artifact$1<'mutation'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, MutationOptions<T>?] : [VariablesOf$1<T>, MutationOptions<T>?]): Source<OperationResult>;
100
100
  executeSubscription<T extends Artifact$1<'subscription'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, SubscriptionOptions?] : [VariablesOf$1<T>, SubscriptionOptions?]): Source<OperationResult>;
101
101
  executeFragment<T extends Artifact$1<'fragment'>>(artifact: T, fragmentRef: FragmentRefs$1<T['name']> | FragmentRefs$1<T['name']>[], options?: FragmentOptions): Source<OperationResult>;
102
102
  query<T extends Artifact$1<'query'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, QueryOptions<T>?] : [VariablesOf$1<T>, QueryOptions<T>?]): Promise<DataOf$1<T>>;
103
- mutation<T extends Artifact$1<'mutation'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, MutationOptions?] : [VariablesOf$1<T>, MutationOptions?]): Promise<DataOf$1<T>>;
103
+ mutation<T extends Artifact$1<'mutation'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, MutationOptions<T>?] : [VariablesOf$1<T>, MutationOptions<T>?]): Promise<DataOf$1<T>>;
104
104
  /**
105
105
  * Returns the extension registered by a named exchange.
106
106
  * @param name - The exchange name.
@@ -115,9 +115,9 @@ declare class Client<TMeta extends SchemaMeta$1 = SchemaMeta$1> {
115
115
  declare const createClient: <T extends SchemaMeta$1>(config: ClientOptions<T>) => Client<T>;
116
116
  //#endregion
117
117
  //#region src/exchange.d.ts
118
- interface OperationMetadataMap {}
118
+ interface OperationMetadataMap<T extends Artifact$1 = Artifact$1> {}
119
119
  interface OperationResultMetadataMap {}
120
- type OperationMetadata = { [K in keyof OperationMetadataMap]?: OperationMetadataMap[K] } & Record<string, unknown>;
120
+ type OperationMetadata<T extends Artifact$1 = Artifact$1> = { [K in keyof OperationMetadataMap<T>]?: OperationMetadataMap<T>[K] } & Record<string, unknown>;
121
121
  type BaseOperation = {
122
122
  key: string;
123
123
  metadata: OperationMetadataMap & Record<string, unknown>;
@@ -248,6 +248,11 @@ declare module '@mearie/core' {
248
248
  interface ExchangeExtensionMap<TMeta extends SchemaMeta$1> {
249
249
  cache: CacheOperations<TMeta>;
250
250
  }
251
+ interface OperationMetadataMap<T extends Artifact$1> {
252
+ cache?: {
253
+ optimisticResponse?: T extends Artifact$1<'mutation'> ? DataOf$1<T> : never;
254
+ };
255
+ }
251
256
  interface OperationResultMetadataMap {
252
257
  cache?: {
253
258
  stale: boolean;
@@ -278,7 +283,9 @@ declare const retryExchange: (options?: RetryOptions) => Exchange;
278
283
  //#region src/exchanges/fragment.d.ts
279
284
  declare module '@mearie/core' {
280
285
  interface OperationMetadataMap {
281
- fragmentRef?: unknown;
286
+ fragment?: {
287
+ ref?: unknown;
288
+ };
282
289
  }
283
290
  }
284
291
  declare const fragmentExchange: () => Exchange;
package/dist/index.d.mts CHANGED
@@ -68,8 +68,8 @@ type QueryOptions<T extends Artifact$1<'query'> = Artifact$1<'query'>> = {
68
68
  initialData?: DataOf$1<T>;
69
69
  metadata?: OperationMetadata;
70
70
  };
71
- type MutationOptions = {
72
- metadata?: OperationMetadata;
71
+ type MutationOptions<T extends Artifact$1<'mutation'> = Artifact$1<'mutation'>> = {
72
+ metadata?: OperationMetadata<T>;
73
73
  };
74
74
  type SubscriptionOptions = {
75
75
  metadata?: OperationMetadata;
@@ -93,14 +93,14 @@ declare class Client<TMeta extends SchemaMeta$1 = SchemaMeta$1> {
93
93
  get schema(): TMeta;
94
94
  get scalars(): ScalarsConfig<TMeta> | undefined;
95
95
  private createOperationKey;
96
- createOperation(artifact: Artifact$1, variables?: unknown, metadata?: OperationMetadata): Operation;
96
+ createOperation(artifact: Artifact$1, variables?: unknown, metadata?: Record<string, unknown>): Operation;
97
97
  executeOperation(operation: Operation): Source<OperationResult>;
98
98
  executeQuery<T extends Artifact$1<'query'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, QueryOptions<T>?] : [VariablesOf$1<T>, QueryOptions<T>?]): Source<OperationResult>;
99
- executeMutation<T extends Artifact$1<'mutation'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, MutationOptions?] : [VariablesOf$1<T>, MutationOptions?]): Source<OperationResult>;
99
+ executeMutation<T extends Artifact$1<'mutation'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, MutationOptions<T>?] : [VariablesOf$1<T>, MutationOptions<T>?]): Source<OperationResult>;
100
100
  executeSubscription<T extends Artifact$1<'subscription'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, SubscriptionOptions?] : [VariablesOf$1<T>, SubscriptionOptions?]): Source<OperationResult>;
101
101
  executeFragment<T extends Artifact$1<'fragment'>>(artifact: T, fragmentRef: FragmentRefs$1<T['name']> | FragmentRefs$1<T['name']>[], options?: FragmentOptions): Source<OperationResult>;
102
102
  query<T extends Artifact$1<'query'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, QueryOptions<T>?] : [VariablesOf$1<T>, QueryOptions<T>?]): Promise<DataOf$1<T>>;
103
- mutation<T extends Artifact$1<'mutation'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, MutationOptions?] : [VariablesOf$1<T>, MutationOptions?]): Promise<DataOf$1<T>>;
103
+ mutation<T extends Artifact$1<'mutation'>>(artifact: T, ...[variables, options]: VariablesOf$1<T> extends undefined ? [undefined?, MutationOptions<T>?] : [VariablesOf$1<T>, MutationOptions<T>?]): Promise<DataOf$1<T>>;
104
104
  /**
105
105
  * Returns the extension registered by a named exchange.
106
106
  * @param name - The exchange name.
@@ -115,9 +115,9 @@ declare class Client<TMeta extends SchemaMeta$1 = SchemaMeta$1> {
115
115
  declare const createClient: <T extends SchemaMeta$1>(config: ClientOptions<T>) => Client<T>;
116
116
  //#endregion
117
117
  //#region src/exchange.d.ts
118
- interface OperationMetadataMap {}
118
+ interface OperationMetadataMap<T extends Artifact$1 = Artifact$1> {}
119
119
  interface OperationResultMetadataMap {}
120
- type OperationMetadata = { [K in keyof OperationMetadataMap]?: OperationMetadataMap[K] } & Record<string, unknown>;
120
+ type OperationMetadata<T extends Artifact$1 = Artifact$1> = { [K in keyof OperationMetadataMap<T>]?: OperationMetadataMap<T>[K] } & Record<string, unknown>;
121
121
  type BaseOperation = {
122
122
  key: string;
123
123
  metadata: OperationMetadataMap & Record<string, unknown>;
@@ -248,6 +248,11 @@ declare module '@mearie/core' {
248
248
  interface ExchangeExtensionMap<TMeta extends SchemaMeta$1> {
249
249
  cache: CacheOperations<TMeta>;
250
250
  }
251
+ interface OperationMetadataMap<T extends Artifact$1> {
252
+ cache?: {
253
+ optimisticResponse?: T extends Artifact$1<'mutation'> ? DataOf$1<T> : never;
254
+ };
255
+ }
251
256
  interface OperationResultMetadataMap {
252
257
  cache?: {
253
258
  stale: boolean;
@@ -278,7 +283,9 @@ declare const retryExchange: (options?: RetryOptions) => Exchange;
278
283
  //#region src/exchanges/fragment.d.ts
279
284
  declare module '@mearie/core' {
280
285
  interface OperationMetadataMap {
281
- fragmentRef?: unknown;
286
+ fragment?: {
287
+ ref?: unknown;
288
+ };
282
289
  }
283
290
  }
284
291
  declare const fragmentExchange: () => Exchange;
package/dist/index.mjs CHANGED
@@ -317,6 +317,12 @@ const RootFieldKey = "__root";
317
317
  * @internal
318
318
  */
319
319
  const FragmentRefKey = "__fragmentRef";
320
+ /**
321
+ * Special key used to carry merged variable context (fragment args + operation variables)
322
+ * on fragment references. Used by readFragment to resolve variable-dependent field keys.
323
+ * @internal
324
+ */
325
+ const FragmentVarsKey = "__fragmentVars";
320
326
 
321
327
  //#endregion
322
328
  //#region src/cache/utils.ts
@@ -390,6 +396,14 @@ const isFragmentRef = (value) => {
390
396
  return typeof value === "object" && value !== null && FragmentRefKey in value;
391
397
  };
392
398
  /**
399
+ * Extracts the merged variable context for a specific fragment from a fragment reference.
400
+ * Returns the merged variables (fragment args + operation variables) if present, or an empty object.
401
+ * @internal
402
+ */
403
+ const getFragmentVars = (fragmentRef, fragmentName) => {
404
+ return fragmentRef[FragmentVarsKey]?.[fragmentName] ?? {};
405
+ };
406
+ /**
393
407
  * Type guard to check if a value is an array of fragment references.
394
408
  * @internal
395
409
  * @param value - Value to check.
@@ -503,38 +517,47 @@ const makeFieldKeyFromArgs = (field, args) => {
503
517
 
504
518
  //#endregion
505
519
  //#region src/cache/normalize.ts
506
- const SKIP = Symbol();
520
+ const resolveTypename = (selections, data) => {
521
+ for (const s of selections) if (s.kind === "Field" && s.name === "__typename") return data[s.alias ?? "__typename"];
522
+ return data.__typename;
523
+ };
507
524
  const normalize = (schemaMeta, selections, storage, data, variables, accessor) => {
525
+ const resolveEntityKey = (typename, data) => {
526
+ if (!typename) return null;
527
+ const entityMeta = schemaMeta.entities[typename];
528
+ if (!entityMeta) return null;
529
+ const keys = entityMeta.keyFields.map((field) => data[field]);
530
+ if (keys.every((k) => k !== void 0 && k !== null)) return makeEntityKey(typename, keys);
531
+ return null;
532
+ };
508
533
  const normalizeField = (storageKey, selections, value) => {
509
534
  if (isNullish(value)) return value;
510
535
  if (Array.isArray(value)) return value.map((item) => normalizeField(storageKey, selections, item));
511
536
  const data = value;
512
- const typename = data.__typename;
513
- const entityMeta = schemaMeta.entities[typename];
514
- if (entityMeta) {
515
- const keys = entityMeta.keyFields.map((field) => data[field]);
516
- if (!keys.every((k) => k !== void 0 && k !== null)) return SKIP;
517
- storageKey = makeEntityKey(typename, keys);
518
- }
537
+ const typename = resolveTypename(selections, data);
538
+ const entityKey = resolveEntityKey(typename, data);
539
+ if (entityKey) storageKey = entityKey;
519
540
  const fields = {};
520
541
  for (const selection of selections) if (selection.kind === "Field") {
521
542
  const fieldKey = makeFieldKey(selection, variables);
522
543
  const fieldValue = data[selection.alias ?? selection.name];
544
+ if (storageKey !== null && selection.selections && typeof fieldValue === "object" && fieldValue !== null && !Array.isArray(fieldValue)) {
545
+ const fieldTypename = resolveTypename(selection.selections, fieldValue);
546
+ if (fieldTypename && schemaMeta.entities[fieldTypename] && !resolveEntityKey(fieldTypename, fieldValue) && isEntityLink(storage[storageKey]?.[fieldKey])) continue;
547
+ }
523
548
  const oldValue = storageKey === null ? void 0 : storage[storageKey]?.[fieldKey];
524
549
  if (storageKey !== null && (!selection.selections || isNullish(oldValue) || isNullish(fieldValue))) accessor?.(storageKey, fieldKey, oldValue, fieldValue);
525
- const normalized = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
526
- if (normalized === SKIP) continue;
527
- fields[fieldKey] = normalized;
550
+ fields[fieldKey] = selection.selections ? normalizeField(null, selection.selections, fieldValue) : fieldValue;
528
551
  if (storageKey !== null && selection.selections && !isNullish(oldValue) && !isNullish(fieldValue) && !isEntityLink(fields[fieldKey]) && !isEqual(oldValue, fields[fieldKey])) accessor?.(storageKey, fieldKey, oldValue, fields[fieldKey]);
529
552
  } else if (selection.kind === "FragmentSpread" || selection.kind === "InlineFragment" && selection.on === typename) {
530
553
  const inner = normalizeField(storageKey, selection.selections, value);
531
- if (inner !== SKIP && !isEntityLink(inner)) mergeFields(fields, inner);
554
+ if (!isEntityLink(inner)) mergeFields(fields, inner);
532
555
  }
533
- if (entityMeta && storageKey !== null) {
534
- const existing = storage[storageKey];
556
+ if (entityKey) {
557
+ const existing = storage[entityKey];
535
558
  if (existing) mergeFields(existing, fields);
536
- else storage[storageKey] = fields;
537
- return { [EntityLinkKey]: storageKey };
559
+ else storage[entityKey] = fields;
560
+ return { [EntityLinkKey]: entityKey };
538
561
  }
539
562
  return fields;
540
563
  };
@@ -583,6 +606,17 @@ const denormalize = (selections, storage, value, variables, accessor) => {
583
606
  else fields[name] = value;
584
607
  } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
585
608
  fields[FragmentRefKey] = storageKey;
609
+ if (selection.args) {
610
+ const resolvedArgs = resolveArguments(selection.args, variables);
611
+ const mergedVars = {
612
+ ...variables,
613
+ ...resolvedArgs
614
+ };
615
+ fields[FragmentVarsKey] = {
616
+ ...fields[FragmentVarsKey],
617
+ [selection.name]: mergedVars
618
+ };
619
+ }
586
620
  if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
587
621
  } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
588
622
  else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
@@ -606,9 +640,74 @@ var Cache = class {
606
640
  #subscriptions = /* @__PURE__ */ new Map();
607
641
  #memo = /* @__PURE__ */ new Map();
608
642
  #stale = /* @__PURE__ */ new Set();
643
+ #optimisticKeys = [];
644
+ #optimisticLayers = /* @__PURE__ */ new Map();
645
+ #storageView = null;
609
646
  constructor(schemaMetadata) {
610
647
  this.#schemaMeta = schemaMetadata;
611
648
  }
649
+ #getStorageView() {
650
+ if (this.#optimisticKeys.length === 0) return this.#storage;
651
+ if (this.#storageView) return this.#storageView;
652
+ const merged = { ...this.#storage };
653
+ for (const storageKey of Object.keys(this.#storage)) merged[storageKey] = { ...this.#storage[storageKey] };
654
+ for (const key of this.#optimisticKeys) {
655
+ const layer = this.#optimisticLayers.get(key);
656
+ if (!layer) continue;
657
+ for (const storageKey of Object.keys(layer.storage)) merged[storageKey] = merged[storageKey] ? {
658
+ ...merged[storageKey],
659
+ ...layer.storage[storageKey]
660
+ } : { ...layer.storage[storageKey] };
661
+ }
662
+ this.#storageView = merged;
663
+ return merged;
664
+ }
665
+ /**
666
+ * Writes an optimistic response to a separate cache layer.
667
+ * The optimistic data is immediately visible in reads but does not affect the base storage.
668
+ * @internal
669
+ * @param key - Unique key identifying this optimistic mutation (typically the operation key).
670
+ * @param artifact - GraphQL document artifact.
671
+ * @param variables - Operation variables.
672
+ * @param data - The optimistic response data.
673
+ */
674
+ writeOptimistic(key, artifact, variables, data) {
675
+ const layerStorage = { [RootFieldKey]: {} };
676
+ const dependencies = /* @__PURE__ */ new Set();
677
+ normalize(this.#schemaMeta, artifact.selections, layerStorage, data, variables, (storageKey, fieldKey) => {
678
+ dependencies.add(makeDependencyKey(storageKey, fieldKey));
679
+ });
680
+ this.#optimisticKeys.push(key);
681
+ this.#optimisticLayers.set(key, {
682
+ storage: layerStorage,
683
+ dependencies
684
+ });
685
+ this.#storageView = null;
686
+ const subscriptions = /* @__PURE__ */ new Set();
687
+ for (const depKey of dependencies) {
688
+ const ss = this.#subscriptions.get(depKey);
689
+ if (ss) for (const s of ss) subscriptions.add(s);
690
+ }
691
+ for (const subscription of subscriptions) subscription.listener();
692
+ }
693
+ /**
694
+ * Removes an optimistic layer and notifies affected subscribers.
695
+ * @internal
696
+ * @param key - The key of the optimistic layer to remove.
697
+ */
698
+ removeOptimistic(key) {
699
+ const layer = this.#optimisticLayers.get(key);
700
+ if (!layer) return;
701
+ this.#optimisticLayers.delete(key);
702
+ this.#optimisticKeys = this.#optimisticKeys.filter((k) => k !== key);
703
+ this.#storageView = null;
704
+ const subscriptions = /* @__PURE__ */ new Set();
705
+ for (const depKey of layer.dependencies) {
706
+ const ss = this.#subscriptions.get(depKey);
707
+ if (ss) for (const s of ss) subscriptions.add(s);
708
+ }
709
+ for (const subscription of subscriptions) subscription.listener();
710
+ }
612
711
  /**
613
712
  * Writes a query result to the cache, normalizing entities.
614
713
  * @param artifact - GraphQL document artifact.
@@ -641,7 +740,8 @@ var Cache = class {
641
740
  */
642
741
  readQuery(artifact, variables) {
643
742
  let stale = false;
644
- const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables, (storageKey, fieldKey) => {
743
+ const storage = this.#getStorageView();
744
+ const { data, partial } = denormalize(artifact.selections, storage, storage[RootFieldKey], variables, (storageKey, fieldKey) => {
645
745
  if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
646
746
  });
647
747
  if (partial) return {
@@ -666,7 +766,8 @@ var Cache = class {
666
766
  */
667
767
  subscribeQuery(artifact, variables, listener) {
668
768
  const dependencies = /* @__PURE__ */ new Set();
669
- denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables, (storageKey, fieldKey) => {
769
+ const storageView = this.#getStorageView();
770
+ denormalize(artifact.selections, storageView, storageView[RootFieldKey], variables, (storageKey, fieldKey) => {
670
771
  const dependencyKey = makeDependencyKey(storageKey, fieldKey);
671
772
  dependencies.add(dependencyKey);
672
773
  });
@@ -681,19 +782,22 @@ var Cache = class {
681
782
  */
682
783
  readFragment(artifact, fragmentRef) {
683
784
  const entityKey = fragmentRef[FragmentRefKey];
684
- if (!this.#storage[entityKey]) return {
785
+ const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
786
+ const storageView = this.#getStorageView();
787
+ if (!storageView[entityKey]) return {
685
788
  data: null,
686
789
  stale: false
687
790
  };
688
791
  let stale = false;
689
- const { data, partial } = denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
792
+ const { data, partial } = denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
690
793
  if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
691
794
  });
692
795
  if (partial) return {
693
796
  data: null,
694
797
  stale: false
695
798
  };
696
- const key = makeMemoKey("fragment", artifact.name, entityKey);
799
+ const argsId = Object.keys(fragmentVars).length > 0 ? entityKey + stringify(fragmentVars) : entityKey;
800
+ const key = makeMemoKey("fragment", artifact.name, argsId);
697
801
  const prev = this.#memo.get(key);
698
802
  const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
699
803
  this.#memo.set(key, result);
@@ -704,8 +808,10 @@ var Cache = class {
704
808
  }
705
809
  subscribeFragment(artifact, fragmentRef, listener) {
706
810
  const entityKey = fragmentRef[FragmentRefKey];
811
+ const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
707
812
  const dependencies = /* @__PURE__ */ new Set();
708
- denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
813
+ const storageView = this.#getStorageView();
814
+ denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
709
815
  const dependencyKey = makeDependencyKey(storageKey, fieldKey);
710
816
  dependencies.add(dependencyKey);
711
817
  });
@@ -735,9 +841,11 @@ var Cache = class {
735
841
  }
736
842
  subscribeFragments(artifact, fragmentRefs, listener) {
737
843
  const dependencies = /* @__PURE__ */ new Set();
844
+ const storageView = this.#getStorageView();
738
845
  for (const ref of fragmentRefs) {
739
846
  const entityKey = ref[FragmentRefKey];
740
- denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
847
+ const fragmentVars = getFragmentVars(ref, artifact.name);
848
+ denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
741
849
  dependencies.add(makeDependencyKey(storageKey, fieldKey));
742
850
  });
743
851
  }
@@ -818,6 +926,7 @@ var Cache = class {
818
926
  }
819
927
  /**
820
928
  * Extracts a serializable snapshot of the cache storage and structural sharing state.
929
+ * Optimistic layers are excluded because they represent transient in-flight state.
821
930
  */
822
931
  extract() {
823
932
  return {
@@ -835,6 +944,7 @@ var Cache = class {
835
944
  ...fields
836
945
  };
837
946
  for (const [key, value] of Object.entries(memo)) this.#memo.set(key, value);
947
+ this.#storageView = null;
838
948
  }
839
949
  /**
840
950
  * Clears all cache data.
@@ -844,6 +954,9 @@ var Cache = class {
844
954
  this.#subscriptions.clear();
845
955
  this.#memo.clear();
846
956
  this.#stale.clear();
957
+ this.#optimisticKeys = [];
958
+ this.#optimisticLayers.clear();
959
+ this.#storageView = null;
847
960
  }
848
961
  };
849
962
 
@@ -876,10 +989,10 @@ const cacheExchange = (options = {}) => {
876
989
  },
877
990
  io: (ops$) => {
878
991
  const fragment$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), mergeMap((op) => {
879
- const fragmentRef = op.metadata?.fragmentRef;
992
+ const fragmentRef = op.metadata?.fragment?.ref;
880
993
  if (!fragmentRef) return fromValue({
881
994
  operation: op,
882
- errors: [new ExchangeError("Fragment operation missing fragmentRef in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
995
+ errors: [new ExchangeError("Fragment operation missing fragment.ref in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
883
996
  });
884
997
  if (isFragmentRefArray(fragmentRef)) {
885
998
  const trigger = makeSubject();
@@ -911,7 +1024,9 @@ const cacheExchange = (options = {}) => {
911
1024
  errors: []
912
1025
  })));
913
1026
  }));
914
- const nonCache$ = pipe(ops$, filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "subscription" || op.artifact.kind === "query" && fetchPolicy === "network-only")));
1027
+ const nonCache$ = pipe(ops$, filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "subscription" || op.artifact.kind === "query" && fetchPolicy === "network-only")), tap((op) => {
1028
+ if (op.artifact.kind === "mutation" && op.metadata?.cache?.optimisticResponse) cache.writeOptimistic(op.key, op.artifact, op.variables, op.metadata.cache.optimisticResponse);
1029
+ }));
915
1030
  const query$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), share());
916
1031
  const refetch$ = makeSubject();
917
1032
  return merge(fragment$, pipe(query$, mergeMap((op) => {
@@ -954,9 +1069,18 @@ const cacheExchange = (options = {}) => {
954
1069
  }), filter(() => fetchPolicy === "cache-only" || fetchPolicy === "cache-and-network" || fetchPolicy === "cache-first")), pipe(merge(nonCache$, pipe(query$, filter((op) => {
955
1070
  const { data } = cache.readQuery(op.artifact, op.variables);
956
1071
  return fetchPolicy === "cache-and-network" || data === null;
957
- })), pipe(ops$, filter((op) => op.variant === "teardown")), refetch$.source), forward, tap((result) => {
1072
+ })), pipe(ops$, filter((op) => op.variant === "teardown")), refetch$.source), forward, mergeMap((result) => {
1073
+ if (result.operation.variant === "request" && result.operation.artifact.kind === "mutation" && result.operation.metadata?.cache?.optimisticResponse) cache.removeOptimistic(result.operation.key);
958
1074
  if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
959
- }), filter((result) => result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0))));
1075
+ if (result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0)) return fromValue(result);
1076
+ const { data } = cache.readQuery(result.operation.artifact, result.operation.variables);
1077
+ if (data !== null) return empty();
1078
+ return fromValue({
1079
+ operation: result.operation,
1080
+ data: void 0,
1081
+ errors: [new ExchangeError("Cache failed to denormalize the network response. This is likely a bug in the cache normalizer.", { exchangeName: "cache" })]
1082
+ });
1083
+ })));
960
1084
  }
961
1085
  };
962
1086
  };
@@ -1008,10 +1132,10 @@ const fragmentExchange = () => {
1008
1132
  name: "fragment",
1009
1133
  io: (ops$) => {
1010
1134
  return merge(pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), map((op) => {
1011
- const fragmentRef = op.metadata.fragmentRef;
1135
+ const fragmentRef = op.metadata.fragment?.ref;
1012
1136
  if (!fragmentRef) return {
1013
1137
  operation: op,
1014
- errors: [new ExchangeError("Fragment operation missing fragmentRef in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "fragment" })]
1138
+ errors: [new ExchangeError("Fragment operation missing fragment.ref in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "fragment" })]
1015
1139
  };
1016
1140
  return {
1017
1141
  operation: op,
@@ -1397,7 +1521,7 @@ var Client = class {
1397
1521
  key: this.createOperationKey(),
1398
1522
  metadata: {
1399
1523
  ...options?.metadata,
1400
- fragmentRef
1524
+ fragment: { ref: fragmentRef }
1401
1525
  },
1402
1526
  artifact,
1403
1527
  variables: {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mearie/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Type-safe, zero-overhead GraphQL client",
5
5
  "keywords": [
6
6
  "graphql",
@@ -62,7 +62,7 @@
62
62
  "README.md"
63
63
  ],
64
64
  "dependencies": {
65
- "@mearie/shared": "0.3.0"
65
+ "@mearie/shared": "0.4.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "tsdown": "^0.20.3",