@mearie/core 0.2.4 → 0.4.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
@@ -501,18 +501,6 @@ const mergeFields = (target, source) => {
501
501
  const makeFieldKeyFromArgs = (field, args) => {
502
502
  return `${field}@${args && Object.keys(args).length > 0 ? stringify(args) : "{}"}`;
503
503
  };
504
- /**
505
- * Converts an EntityId to an EntityKey.
506
- * @internal
507
- * @param typename - The GraphQL typename of the entity.
508
- * @param id - The entity identifier (string, number, or composite key record).
509
- * @param keyFields - Optional ordered list of key field names for composite keys.
510
- * @returns An EntityKey.
511
- */
512
- const resolveEntityKey = (typename, id, keyFields) => {
513
- if (typeof id === "string" || typeof id === "number") return makeEntityKey(typename, [id]);
514
- return makeEntityKey(typename, keyFields ? keyFields.map((f) => id[f]) : Object.values(id));
515
- };
516
504
 
517
505
  //#endregion
518
506
  //#region src/cache/normalize.ts
@@ -594,8 +582,10 @@ const denormalize = (selections, storage, value, variables, accessor) => {
594
582
  const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
595
583
  if (name in fields) mergeFields(fields, { [name]: value });
596
584
  else fields[name] = value;
597
- } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) fields[FragmentRefKey] = storageKey;
598
- else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
585
+ } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
586
+ fields[FragmentRefKey] = storageKey;
587
+ if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
588
+ } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
599
589
  else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
600
590
  return fields;
601
591
  };
@@ -616,6 +606,7 @@ var Cache = class {
616
606
  #storage = { [RootFieldKey]: {} };
617
607
  #subscriptions = /* @__PURE__ */ new Map();
618
608
  #memo = /* @__PURE__ */ new Map();
609
+ #stale = /* @__PURE__ */ new Set();
619
610
  constructor(schemaMetadata) {
620
611
  this.#schemaMeta = schemaMetadata;
621
612
  }
@@ -628,17 +619,19 @@ var Cache = class {
628
619
  writeQuery(artifact, variables, data) {
629
620
  const dependencies = /* @__PURE__ */ new Set();
630
621
  const subscriptions = /* @__PURE__ */ new Set();
622
+ const entityStaleCleared = /* @__PURE__ */ new Set();
631
623
  normalize(this.#schemaMeta, artifact.selections, this.#storage, data, variables, (storageKey, fieldKey, oldValue, newValue) => {
632
- if (oldValue !== newValue) {
633
- const dependencyKey = makeDependencyKey(storageKey, fieldKey);
634
- dependencies.add(dependencyKey);
635
- }
624
+ const depKey = makeDependencyKey(storageKey, fieldKey);
625
+ if (this.#stale.delete(depKey)) dependencies.add(depKey);
626
+ if (!entityStaleCleared.has(storageKey) && this.#stale.delete(storageKey)) entityStaleCleared.add(storageKey);
627
+ if (oldValue !== newValue) dependencies.add(depKey);
636
628
  });
629
+ for (const entityKey of entityStaleCleared) this.#collectSubscriptions(entityKey, void 0, subscriptions);
637
630
  for (const dependency of dependencies) {
638
631
  const ss = this.#subscriptions.get(dependency);
639
632
  if (ss) for (const s of ss) subscriptions.add(s);
640
633
  }
641
- for (const subscription of subscriptions) subscription.listener("write");
634
+ for (const subscription of subscriptions) subscription.listener();
642
635
  }
643
636
  /**
644
637
  * Reads a query result from the cache, denormalizing entities if available.
@@ -648,13 +641,22 @@ var Cache = class {
648
641
  * @returns Denormalized query result or null if not found.
649
642
  */
650
643
  readQuery(artifact, variables) {
651
- const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables);
652
- if (partial) return null;
644
+ let stale = false;
645
+ const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables, (storageKey, fieldKey) => {
646
+ if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
647
+ });
648
+ if (partial) return {
649
+ data: null,
650
+ stale: false
651
+ };
653
652
  const key = makeMemoKey("query", artifact.name, stringify(variables));
654
653
  const prev = this.#memo.get(key);
655
654
  const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
656
655
  this.#memo.set(key, result);
657
- return result;
656
+ return {
657
+ data: result,
658
+ stale
659
+ };
658
660
  }
659
661
  /**
660
662
  * Subscribes to cache invalidations for a specific query.
@@ -680,14 +682,26 @@ var Cache = class {
680
682
  */
681
683
  readFragment(artifact, fragmentRef) {
682
684
  const entityKey = fragmentRef[FragmentRefKey];
683
- if (!this.#storage[entityKey]) return null;
684
- const { data, partial } = denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {});
685
- if (partial) return null;
685
+ if (!this.#storage[entityKey]) return {
686
+ data: null,
687
+ stale: false
688
+ };
689
+ let stale = false;
690
+ const { data, partial } = denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
691
+ if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
692
+ });
693
+ if (partial) return {
694
+ data: null,
695
+ stale: false
696
+ };
686
697
  const key = makeMemoKey("fragment", artifact.name, entityKey);
687
698
  const prev = this.#memo.get(key);
688
699
  const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
689
700
  this.#memo.set(key, result);
690
- return result;
701
+ return {
702
+ data: result,
703
+ stale
704
+ };
691
705
  }
692
706
  subscribeFragment(artifact, fragmentRef, listener) {
693
707
  const entityKey = fragmentRef[FragmentRefKey];
@@ -700,17 +714,25 @@ var Cache = class {
700
714
  }
701
715
  readFragments(artifact, fragmentRefs) {
702
716
  const results = [];
717
+ let stale = false;
703
718
  for (const ref of fragmentRefs) {
704
- const data = this.readFragment(artifact, ref);
705
- if (data === null) return null;
706
- results.push(data);
719
+ const result = this.readFragment(artifact, ref);
720
+ if (result.data === null) return {
721
+ data: null,
722
+ stale: false
723
+ };
724
+ if (result.stale) stale = true;
725
+ results.push(result.data);
707
726
  }
708
727
  const entityKeys = fragmentRefs.map((ref) => ref[FragmentRefKey]);
709
728
  const key = makeMemoKey("fragments", artifact.name, entityKeys.join(","));
710
729
  const prev = this.#memo.get(key);
711
730
  const result = prev === void 0 ? results : replaceEqualDeep(prev, results);
712
731
  this.#memo.set(key, result);
713
- return result;
732
+ return {
733
+ data: result,
734
+ stale
735
+ };
714
736
  }
715
737
  subscribeFragments(artifact, fragmentRefs, listener) {
716
738
  const dependencies = /* @__PURE__ */ new Set();
@@ -728,50 +750,47 @@ var Cache = class {
728
750
  */
729
751
  invalidate(...targets) {
730
752
  const subscriptions = /* @__PURE__ */ new Set();
731
- for (const target of targets) if (target.__typename === "Query") if ("field" in target) {
732
- const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
733
- delete this.#storage[RootFieldKey]?.[fieldKey];
753
+ for (const target of targets) if (target.__typename === "Query") if ("$field" in target) {
754
+ const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
755
+ const depKey = makeDependencyKey(RootFieldKey, fieldKey);
756
+ this.#stale.add(depKey);
734
757
  this.#collectSubscriptions(RootFieldKey, fieldKey, subscriptions);
735
758
  } else {
736
- this.#storage[RootFieldKey] = {};
759
+ this.#stale.add(RootFieldKey);
737
760
  this.#collectSubscriptions(RootFieldKey, void 0, subscriptions);
738
761
  }
739
- else if ("id" in target) {
740
- const entityKey = resolveEntityKey(target.__typename, target.id, this.#schemaMeta.entities[target.__typename]?.keyFields);
741
- if ("field" in target) {
742
- const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
743
- delete this.#storage[entityKey]?.[fieldKey];
744
- this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
745
- } else {
746
- delete this.#storage[entityKey];
747
- this.#collectSubscriptions(entityKey, void 0, subscriptions);
748
- }
749
- this.#collectLinkedEntitySubscriptions((linkedEntityKey) => linkedEntityKey === entityKey, subscriptions);
750
- } else {
751
- const prefix = `${target.__typename}:`;
752
- for (const key of Object.keys(this.#storage)) if (key.startsWith(prefix)) {
753
- const entityKey = key;
754
- if ("field" in target) {
755
- const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
756
- delete this.#storage[entityKey]?.[fieldKey];
762
+ else {
763
+ const keyFields = this.#schemaMeta.entities[target.__typename]?.keyFields;
764
+ if (keyFields && this.#hasKeyFields(target, keyFields)) {
765
+ const keyValues = keyFields.map((f) => target[f]);
766
+ const entityKey = makeEntityKey(target.__typename, keyValues);
767
+ if ("$field" in target) {
768
+ const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
769
+ this.#stale.add(makeDependencyKey(entityKey, fieldKey));
757
770
  this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
758
771
  } else {
759
- delete this.#storage[entityKey];
772
+ this.#stale.add(entityKey);
760
773
  this.#collectSubscriptions(entityKey, void 0, subscriptions);
761
774
  }
775
+ } else {
776
+ const prefix = `${target.__typename}:`;
777
+ for (const key of Object.keys(this.#storage)) if (key.startsWith(prefix)) {
778
+ const entityKey = key;
779
+ if ("$field" in target) {
780
+ const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
781
+ this.#stale.add(makeDependencyKey(entityKey, fieldKey));
782
+ this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
783
+ } else {
784
+ this.#stale.add(entityKey);
785
+ this.#collectSubscriptions(entityKey, void 0, subscriptions);
786
+ }
787
+ }
762
788
  }
763
- this.#collectLinkedEntitySubscriptions((linkedEntityKey) => linkedEntityKey.startsWith(prefix), subscriptions);
764
789
  }
765
- for (const subscription of subscriptions) subscription.listener("invalidate");
766
- }
767
- #collectLinkedEntitySubscriptions(matcher, out) {
768
- for (const [storageKey, fields] of Object.entries(this.#storage)) for (const [fieldKey, value] of Object.entries(fields)) if (this.#containsEntityLink(value, matcher)) this.#collectSubscriptions(storageKey, fieldKey, out);
790
+ for (const subscription of subscriptions) subscription.listener();
769
791
  }
770
- #containsEntityLink(value, matcher) {
771
- if (isEntityLink(value)) return matcher(value[EntityLinkKey]);
772
- if (Array.isArray(value)) return value.some((item) => this.#containsEntityLink(item, matcher));
773
- if (value && typeof value === "object") return Object.values(value).some((item) => this.#containsEntityLink(item, matcher));
774
- return false;
792
+ #hasKeyFields(target, keyFields) {
793
+ return keyFields.every((f) => f in target);
775
794
  }
776
795
  #collectSubscriptions(storageKey, fieldKey, out) {
777
796
  if (fieldKey === void 0) {
@@ -825,6 +844,7 @@ var Cache = class {
825
844
  this.#storage = { [RootFieldKey]: {} };
826
845
  this.#subscriptions.clear();
827
846
  this.#memo.clear();
847
+ this.#stale.clear();
828
848
  }
829
849
  };
830
850
 
@@ -864,27 +884,16 @@ const cacheExchange = (options = {}) => {
864
884
  });
865
885
  if (isFragmentRefArray(fragmentRef)) {
866
886
  const trigger = require_make.makeSubject();
867
- let hasData = false;
868
887
  const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => trigger.complete()));
869
888
  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 () => {
870
889
  await Promise.resolve();
871
890
  trigger.next();
872
- }))), require_make.takeUntil(teardown$), require_make.mergeMap((data) => {
873
- if (data !== null) {
874
- hasData = true;
875
- return require_make.fromValue({
876
- operation: op,
877
- data,
878
- errors: []
879
- });
880
- }
881
- if (hasData) return empty();
882
- return require_make.fromValue({
883
- operation: op,
884
- data,
885
- errors: []
886
- });
887
- }));
891
+ }))), require_make.takeUntil(teardown$), require_make.map(({ data, stale }) => ({
892
+ operation: op,
893
+ data,
894
+ ...stale && { metadata: { cache: { stale: true } } },
895
+ errors: []
896
+ })));
888
897
  }
889
898
  if (!isFragmentRef(fragmentRef)) return require_make.fromValue({
890
899
  operation: op,
@@ -892,27 +901,16 @@ const cacheExchange = (options = {}) => {
892
901
  errors: []
893
902
  });
894
903
  const trigger = require_make.makeSubject();
895
- let hasData = false;
896
904
  const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => trigger.complete()));
897
905
  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 () => {
898
906
  await Promise.resolve();
899
907
  trigger.next();
900
- }))), require_make.takeUntil(teardown$), require_make.mergeMap((data) => {
901
- if (data !== null) {
902
- hasData = true;
903
- return require_make.fromValue({
904
- operation: op,
905
- data,
906
- errors: []
907
- });
908
- }
909
- if (hasData) return empty();
910
- return require_make.fromValue({
911
- operation: op,
912
- data,
913
- errors: []
914
- });
915
- }));
908
+ }))), require_make.takeUntil(teardown$), require_make.map(({ data, stale }) => ({
909
+ operation: op,
910
+ data,
911
+ ...stale && { metadata: { cache: { stale: true } } },
912
+ errors: []
913
+ })));
916
914
  }));
917
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")));
918
916
  const query$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), require_make.share());
@@ -920,30 +918,31 @@ const cacheExchange = (options = {}) => {
920
918
  return require_make.merge(fragment$, require_make.pipe(query$, require_make.mergeMap((op) => {
921
919
  const trigger = require_make.makeSubject();
922
920
  let hasData = false;
923
- let invalidated = false;
924
921
  const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => trigger.complete()));
925
- 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 (event) => {
926
- if (event === "invalidate") invalidated = true;
922
+ 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 () => {
927
923
  await Promise.resolve();
928
924
  trigger.next();
929
- }))), require_make.takeUntil(teardown$), require_make.mergeMap((data) => {
930
- if (data !== null) {
931
- if (invalidated && hasData && fetchPolicy !== "cache-only") {
932
- invalidated = false;
933
- refetch$.next(op);
934
- return empty();
935
- }
925
+ }))), require_make.takeUntil(teardown$), require_make.mergeMap(({ data, stale }) => {
926
+ if (data !== null && !stale) {
927
+ hasData = true;
928
+ return require_make.fromValue({
929
+ operation: op,
930
+ data,
931
+ errors: []
932
+ });
933
+ }
934
+ if (data !== null && stale) {
936
935
  hasData = true;
937
- invalidated = false;
936
+ refetch$.next(op);
938
937
  return require_make.fromValue({
939
938
  operation: op,
940
939
  data,
940
+ metadata: { cache: { stale: true } },
941
941
  errors: []
942
942
  });
943
943
  }
944
944
  if (hasData) {
945
- if (fetchPolicy !== "cache-only") refetch$.next(op);
946
- invalidated = false;
945
+ refetch$.next(op);
947
946
  return empty();
948
947
  }
949
948
  if (fetchPolicy === "cache-only") return require_make.fromValue({
@@ -954,8 +953,8 @@ const cacheExchange = (options = {}) => {
954
953
  return empty();
955
954
  }));
956
955
  }), 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) => {
957
- const cached = cache.readQuery(op.artifact, op.variables);
958
- return fetchPolicy === "cache-and-network" || cached === null;
956
+ const { data } = cache.readQuery(op.artifact, op.variables);
957
+ return fetchPolicy === "cache-and-network" || data === null;
959
958
  })), require_make.pipe(ops$, require_make.filter((op) => op.variant === "teardown")), refetch$.source), forward, require_make.tap((result) => {
960
959
  if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
961
960
  }), require_make.filter((result) => result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0))));
package/dist/index.d.cts CHANGED
@@ -106,9 +106,9 @@ declare class Client<TMeta extends SchemaMeta$1 = SchemaMeta$1> {
106
106
  * @param name - The exchange name.
107
107
  * @returns The extension object provided by the exchange.
108
108
  */
109
- extension<TName extends keyof ExchangeExtensionMap>(name: TName): ExchangeExtensionMap[TName];
109
+ extension<TName extends keyof ExchangeExtensionMap<TMeta>>(name: TName): ExchangeExtensionMap<TMeta>[TName];
110
110
  extension(name: string): unknown;
111
- maybeExtension<TName extends keyof ExchangeExtensionMap>(name: TName): ExchangeExtensionMap[TName] | undefined;
111
+ maybeExtension<TName extends keyof ExchangeExtensionMap<TMeta>>(name: TName): ExchangeExtensionMap<TMeta>[TName] | undefined;
112
112
  maybeExtension(name: string): unknown;
113
113
  dispose(): void;
114
114
  }
@@ -116,6 +116,7 @@ declare const createClient: <T extends SchemaMeta$1>(config: ClientOptions<T>) =
116
116
  //#endregion
117
117
  //#region src/exchange.d.ts
118
118
  interface OperationMetadataMap {}
119
+ interface OperationResultMetadataMap {}
119
120
  type OperationMetadata = { [K in keyof OperationMetadataMap]?: OperationMetadataMap[K] } & Record<string, unknown>;
120
121
  type BaseOperation = {
121
122
  key: string;
@@ -135,21 +136,21 @@ type OperationResult = {
135
136
  data?: unknown;
136
137
  errors?: readonly OperationError[];
137
138
  extensions?: Record<string, unknown>;
138
- stale?: boolean;
139
+ metadata?: OperationResultMetadataMap & Record<string, unknown>;
139
140
  };
140
141
  type ExchangeInput<TMeta extends SchemaMeta$1 = SchemaMeta$1> = {
141
142
  forward: ExchangeIO;
142
143
  client: Client<TMeta>;
143
144
  };
144
145
  type ExchangeIO = (operations: Source<Operation>) => Source<OperationResult>;
145
- interface ExchangeExtensionMap {}
146
- type ExchangeResult<TName extends keyof ExchangeExtensionMap | (string & {}) = string> = {
146
+ interface ExchangeExtensionMap<TMeta extends SchemaMeta$1 = SchemaMeta$1> {}
147
+ type ExchangeResult<TName extends keyof ExchangeExtensionMap | (string & {}) = string, TMeta extends SchemaMeta$1 = SchemaMeta$1> = {
147
148
  name: TName;
148
149
  io: ExchangeIO;
149
- } & (TName extends keyof ExchangeExtensionMap ? {
150
- extension: ExchangeExtensionMap[TName];
150
+ } & (TName extends keyof ExchangeExtensionMap<TMeta> ? {
151
+ extension: ExchangeExtensionMap<TMeta>[TName];
151
152
  } : {});
152
- type Exchange<TName extends keyof ExchangeExtensionMap | (string & {}) = string> = <TMeta extends SchemaMeta$1 = SchemaMeta$1>(input: ExchangeInput<TMeta>) => ExchangeResult<TName>;
153
+ type Exchange<TName extends keyof ExchangeExtensionMap | (string & {}) = string> = <TMeta extends SchemaMeta$1 = SchemaMeta$1>(input: ExchangeInput<TMeta>) => ExchangeResult<TName, TMeta>;
153
154
  //#endregion
154
155
  //#region src/exchanges/http.d.ts
155
156
  declare module '@mearie/core' {
@@ -194,34 +195,38 @@ declare module '@mearie/core' {
194
195
  declare const dedupExchange: () => Exchange;
195
196
  //#endregion
196
197
  //#region src/cache/types.d.ts
197
- /**
198
- * Identifier for a single entity, supporting simple or composite keys.
199
- */
200
- type EntityId = string | number | Record<string, string | number>;
201
- /**
202
- * Target specification for cache invalidation operations.
203
- */
204
- type InvalidateTarget = {
205
- __typename: string;
206
- id: EntityId;
207
- } | {
208
- __typename: string;
209
- id: EntityId;
210
- field: string;
211
- args?: Record<string, unknown>;
212
- } | {
198
+ type EntityTypes<TMeta extends SchemaMeta$1> = NonNullable<TMeta[' $entityTypes']>;
199
+ type QueryFields<TMeta extends SchemaMeta$1> = NonNullable<TMeta[' $queryFields']>;
200
+ type KeyFieldsOf<E> = E extends {
201
+ keyFields: infer KF;
202
+ } ? KF : Record<string, unknown>;
203
+ type FieldsOf<E> = E extends {
204
+ fields: infer F extends string;
205
+ } ? F : string;
206
+ type EntityInvalidateTarget<Entities> = { [K in keyof Entities & string]: {
207
+ __typename: K;
208
+ } | ({
209
+ __typename: K;
210
+ } & KeyFieldsOf<Entities[K]>) | {
211
+ __typename: K;
212
+ $field: FieldsOf<Entities[K]>;
213
+ $args?: Record<string, unknown>;
214
+ } | ({
215
+ __typename: K;
216
+ $field: FieldsOf<Entities[K]>;
217
+ $args?: Record<string, unknown>;
218
+ } & KeyFieldsOf<Entities[K]>) }[keyof Entities & string];
219
+ type QueryInvalidateTarget<QF extends string> = {
213
220
  __typename: 'Query';
214
221
  } | {
215
222
  __typename: 'Query';
216
- field: string;
217
- args?: Record<string, unknown>;
218
- } | {
219
- __typename: string;
220
- } | {
221
- __typename: string;
222
- field: string;
223
- args?: Record<string, unknown>;
223
+ $field: QF;
224
+ $args?: Record<string, unknown>;
224
225
  };
226
+ /**
227
+ * Target specification for cache invalidation operations.
228
+ */
229
+ type InvalidateTarget<TMeta extends SchemaMeta$1 = SchemaMeta$1> = EntityInvalidateTarget<EntityTypes<TMeta>> | QueryInvalidateTarget<QueryFields<TMeta>>;
225
230
  /**
226
231
  * Opaque type representing a serializable cache snapshot.
227
232
  */
@@ -231,17 +236,22 @@ type CacheSnapshot = {
231
236
  /**
232
237
  * Operations available for programmatic cache manipulation.
233
238
  */
234
- type CacheOperations = {
239
+ type CacheOperations<TMeta extends SchemaMeta$1 = SchemaMeta$1> = {
235
240
  extract(): CacheSnapshot;
236
241
  hydrate(data: CacheSnapshot): void;
237
- invalidate(...targets: InvalidateTarget[]): void;
242
+ invalidate(...targets: InvalidateTarget<TMeta>[]): void;
238
243
  clear(): void;
239
244
  };
240
245
  //#endregion
241
246
  //#region src/exchanges/cache.d.ts
242
247
  declare module '@mearie/core' {
243
- interface ExchangeExtensionMap {
244
- cache: CacheOperations;
248
+ interface ExchangeExtensionMap<TMeta extends SchemaMeta$1> {
249
+ cache: CacheOperations<TMeta>;
250
+ }
251
+ interface OperationResultMetadataMap {
252
+ cache?: {
253
+ stale: boolean;
254
+ };
245
255
  }
246
256
  }
247
257
  type CacheOptions = {
@@ -345,4 +355,4 @@ declare class RequiredFieldError extends Error {
345
355
  */
346
356
  declare const stringify: (value: unknown) => string;
347
357
  //#endregion
348
- export { AggregatedError, type Artifact, type ArtifactKind, type CacheOptions, type CacheSnapshot, Client, type ClientOptions, type DataOf, type Exchange, ExchangeError, type ExchangeErrorExtensionsMap, type ExchangeExtensionMap, type ExchangeIO, type ExchangeResult, type FragmentOptions, type FragmentRefs, GraphQLError, type HttpOptions, type MutationOptions, type Operation, type OperationError, type OperationMetadata, type OperationMetadataMap, type OperationResult, type QueryOptions, type RequiredAction, RequiredFieldError, type RetryOptions, type SchemaMeta, type SubscriptionClient, type SubscriptionExchangeOptions, type SubscriptionOptions, type VariablesOf, cacheExchange, createClient, dedupExchange, fragmentExchange, httpExchange, isAggregatedError, isExchangeError, isGraphQLError, requiredExchange, retryExchange, stringify, subscriptionExchange };
358
+ export { AggregatedError, type Artifact, type ArtifactKind, type CacheOperations, type CacheOptions, type CacheSnapshot, Client, type ClientOptions, type DataOf, type Exchange, ExchangeError, type ExchangeErrorExtensionsMap, type ExchangeExtensionMap, type ExchangeIO, type ExchangeResult, type FragmentOptions, type FragmentRefs, GraphQLError, type HttpOptions, type InvalidateTarget, type MutationOptions, type Operation, type OperationError, type OperationMetadata, type OperationMetadataMap, type OperationResult, type OperationResultMetadataMap, type QueryOptions, type RequiredAction, RequiredFieldError, type RetryOptions, type SchemaMeta, type SubscriptionClient, type SubscriptionExchangeOptions, type SubscriptionOptions, type VariablesOf, cacheExchange, createClient, dedupExchange, fragmentExchange, httpExchange, isAggregatedError, isExchangeError, isGraphQLError, requiredExchange, retryExchange, stringify, subscriptionExchange };
package/dist/index.d.mts CHANGED
@@ -106,9 +106,9 @@ declare class Client<TMeta extends SchemaMeta$1 = SchemaMeta$1> {
106
106
  * @param name - The exchange name.
107
107
  * @returns The extension object provided by the exchange.
108
108
  */
109
- extension<TName extends keyof ExchangeExtensionMap>(name: TName): ExchangeExtensionMap[TName];
109
+ extension<TName extends keyof ExchangeExtensionMap<TMeta>>(name: TName): ExchangeExtensionMap<TMeta>[TName];
110
110
  extension(name: string): unknown;
111
- maybeExtension<TName extends keyof ExchangeExtensionMap>(name: TName): ExchangeExtensionMap[TName] | undefined;
111
+ maybeExtension<TName extends keyof ExchangeExtensionMap<TMeta>>(name: TName): ExchangeExtensionMap<TMeta>[TName] | undefined;
112
112
  maybeExtension(name: string): unknown;
113
113
  dispose(): void;
114
114
  }
@@ -116,6 +116,7 @@ declare const createClient: <T extends SchemaMeta$1>(config: ClientOptions<T>) =
116
116
  //#endregion
117
117
  //#region src/exchange.d.ts
118
118
  interface OperationMetadataMap {}
119
+ interface OperationResultMetadataMap {}
119
120
  type OperationMetadata = { [K in keyof OperationMetadataMap]?: OperationMetadataMap[K] } & Record<string, unknown>;
120
121
  type BaseOperation = {
121
122
  key: string;
@@ -135,21 +136,21 @@ type OperationResult = {
135
136
  data?: unknown;
136
137
  errors?: readonly OperationError[];
137
138
  extensions?: Record<string, unknown>;
138
- stale?: boolean;
139
+ metadata?: OperationResultMetadataMap & Record<string, unknown>;
139
140
  };
140
141
  type ExchangeInput<TMeta extends SchemaMeta$1 = SchemaMeta$1> = {
141
142
  forward: ExchangeIO;
142
143
  client: Client<TMeta>;
143
144
  };
144
145
  type ExchangeIO = (operations: Source<Operation>) => Source<OperationResult>;
145
- interface ExchangeExtensionMap {}
146
- type ExchangeResult<TName extends keyof ExchangeExtensionMap | (string & {}) = string> = {
146
+ interface ExchangeExtensionMap<TMeta extends SchemaMeta$1 = SchemaMeta$1> {}
147
+ type ExchangeResult<TName extends keyof ExchangeExtensionMap | (string & {}) = string, TMeta extends SchemaMeta$1 = SchemaMeta$1> = {
147
148
  name: TName;
148
149
  io: ExchangeIO;
149
- } & (TName extends keyof ExchangeExtensionMap ? {
150
- extension: ExchangeExtensionMap[TName];
150
+ } & (TName extends keyof ExchangeExtensionMap<TMeta> ? {
151
+ extension: ExchangeExtensionMap<TMeta>[TName];
151
152
  } : {});
152
- type Exchange<TName extends keyof ExchangeExtensionMap | (string & {}) = string> = <TMeta extends SchemaMeta$1 = SchemaMeta$1>(input: ExchangeInput<TMeta>) => ExchangeResult<TName>;
153
+ type Exchange<TName extends keyof ExchangeExtensionMap | (string & {}) = string> = <TMeta extends SchemaMeta$1 = SchemaMeta$1>(input: ExchangeInput<TMeta>) => ExchangeResult<TName, TMeta>;
153
154
  //#endregion
154
155
  //#region src/exchanges/http.d.ts
155
156
  declare module '@mearie/core' {
@@ -194,34 +195,38 @@ declare module '@mearie/core' {
194
195
  declare const dedupExchange: () => Exchange;
195
196
  //#endregion
196
197
  //#region src/cache/types.d.ts
197
- /**
198
- * Identifier for a single entity, supporting simple or composite keys.
199
- */
200
- type EntityId = string | number | Record<string, string | number>;
201
- /**
202
- * Target specification for cache invalidation operations.
203
- */
204
- type InvalidateTarget = {
205
- __typename: string;
206
- id: EntityId;
207
- } | {
208
- __typename: string;
209
- id: EntityId;
210
- field: string;
211
- args?: Record<string, unknown>;
212
- } | {
198
+ type EntityTypes<TMeta extends SchemaMeta$1> = NonNullable<TMeta[' $entityTypes']>;
199
+ type QueryFields<TMeta extends SchemaMeta$1> = NonNullable<TMeta[' $queryFields']>;
200
+ type KeyFieldsOf<E> = E extends {
201
+ keyFields: infer KF;
202
+ } ? KF : Record<string, unknown>;
203
+ type FieldsOf<E> = E extends {
204
+ fields: infer F extends string;
205
+ } ? F : string;
206
+ type EntityInvalidateTarget<Entities> = { [K in keyof Entities & string]: {
207
+ __typename: K;
208
+ } | ({
209
+ __typename: K;
210
+ } & KeyFieldsOf<Entities[K]>) | {
211
+ __typename: K;
212
+ $field: FieldsOf<Entities[K]>;
213
+ $args?: Record<string, unknown>;
214
+ } | ({
215
+ __typename: K;
216
+ $field: FieldsOf<Entities[K]>;
217
+ $args?: Record<string, unknown>;
218
+ } & KeyFieldsOf<Entities[K]>) }[keyof Entities & string];
219
+ type QueryInvalidateTarget<QF extends string> = {
213
220
  __typename: 'Query';
214
221
  } | {
215
222
  __typename: 'Query';
216
- field: string;
217
- args?: Record<string, unknown>;
218
- } | {
219
- __typename: string;
220
- } | {
221
- __typename: string;
222
- field: string;
223
- args?: Record<string, unknown>;
223
+ $field: QF;
224
+ $args?: Record<string, unknown>;
224
225
  };
226
+ /**
227
+ * Target specification for cache invalidation operations.
228
+ */
229
+ type InvalidateTarget<TMeta extends SchemaMeta$1 = SchemaMeta$1> = EntityInvalidateTarget<EntityTypes<TMeta>> | QueryInvalidateTarget<QueryFields<TMeta>>;
225
230
  /**
226
231
  * Opaque type representing a serializable cache snapshot.
227
232
  */
@@ -231,17 +236,22 @@ type CacheSnapshot = {
231
236
  /**
232
237
  * Operations available for programmatic cache manipulation.
233
238
  */
234
- type CacheOperations = {
239
+ type CacheOperations<TMeta extends SchemaMeta$1 = SchemaMeta$1> = {
235
240
  extract(): CacheSnapshot;
236
241
  hydrate(data: CacheSnapshot): void;
237
- invalidate(...targets: InvalidateTarget[]): void;
242
+ invalidate(...targets: InvalidateTarget<TMeta>[]): void;
238
243
  clear(): void;
239
244
  };
240
245
  //#endregion
241
246
  //#region src/exchanges/cache.d.ts
242
247
  declare module '@mearie/core' {
243
- interface ExchangeExtensionMap {
244
- cache: CacheOperations;
248
+ interface ExchangeExtensionMap<TMeta extends SchemaMeta$1> {
249
+ cache: CacheOperations<TMeta>;
250
+ }
251
+ interface OperationResultMetadataMap {
252
+ cache?: {
253
+ stale: boolean;
254
+ };
245
255
  }
246
256
  }
247
257
  type CacheOptions = {
@@ -345,4 +355,4 @@ declare class RequiredFieldError extends Error {
345
355
  */
346
356
  declare const stringify: (value: unknown) => string;
347
357
  //#endregion
348
- export { AggregatedError, type Artifact, type ArtifactKind, type CacheOptions, type CacheSnapshot, Client, type ClientOptions, type DataOf, type Exchange, ExchangeError, type ExchangeErrorExtensionsMap, type ExchangeExtensionMap, type ExchangeIO, type ExchangeResult, type FragmentOptions, type FragmentRefs, GraphQLError, type HttpOptions, type MutationOptions, type Operation, type OperationError, type OperationMetadata, type OperationMetadataMap, type OperationResult, type QueryOptions, type RequiredAction, RequiredFieldError, type RetryOptions, type SchemaMeta, type SubscriptionClient, type SubscriptionExchangeOptions, type SubscriptionOptions, type VariablesOf, cacheExchange, createClient, dedupExchange, fragmentExchange, httpExchange, isAggregatedError, isExchangeError, isGraphQLError, requiredExchange, retryExchange, stringify, subscriptionExchange };
358
+ export { AggregatedError, type Artifact, type ArtifactKind, type CacheOperations, type CacheOptions, type CacheSnapshot, Client, type ClientOptions, type DataOf, type Exchange, ExchangeError, type ExchangeErrorExtensionsMap, type ExchangeExtensionMap, type ExchangeIO, type ExchangeResult, type FragmentOptions, type FragmentRefs, GraphQLError, type HttpOptions, type InvalidateTarget, type MutationOptions, type Operation, type OperationError, type OperationMetadata, type OperationMetadataMap, type OperationResult, type OperationResultMetadataMap, type QueryOptions, type RequiredAction, RequiredFieldError, type RetryOptions, type SchemaMeta, type SubscriptionClient, type SubscriptionExchangeOptions, type SubscriptionOptions, type VariablesOf, cacheExchange, createClient, dedupExchange, fragmentExchange, httpExchange, isAggregatedError, isExchangeError, isGraphQLError, requiredExchange, retryExchange, stringify, subscriptionExchange };
package/dist/index.mjs CHANGED
@@ -500,18 +500,6 @@ const mergeFields = (target, source) => {
500
500
  const makeFieldKeyFromArgs = (field, args) => {
501
501
  return `${field}@${args && Object.keys(args).length > 0 ? stringify(args) : "{}"}`;
502
502
  };
503
- /**
504
- * Converts an EntityId to an EntityKey.
505
- * @internal
506
- * @param typename - The GraphQL typename of the entity.
507
- * @param id - The entity identifier (string, number, or composite key record).
508
- * @param keyFields - Optional ordered list of key field names for composite keys.
509
- * @returns An EntityKey.
510
- */
511
- const resolveEntityKey = (typename, id, keyFields) => {
512
- if (typeof id === "string" || typeof id === "number") return makeEntityKey(typename, [id]);
513
- return makeEntityKey(typename, keyFields ? keyFields.map((f) => id[f]) : Object.values(id));
514
- };
515
503
 
516
504
  //#endregion
517
505
  //#region src/cache/normalize.ts
@@ -593,8 +581,10 @@ const denormalize = (selections, storage, value, variables, accessor) => {
593
581
  const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
594
582
  if (name in fields) mergeFields(fields, { [name]: value });
595
583
  else fields[name] = value;
596
- } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) fields[FragmentRefKey] = storageKey;
597
- else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
584
+ } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
585
+ fields[FragmentRefKey] = storageKey;
586
+ if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
587
+ } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
598
588
  else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
599
589
  return fields;
600
590
  };
@@ -615,6 +605,7 @@ var Cache = class {
615
605
  #storage = { [RootFieldKey]: {} };
616
606
  #subscriptions = /* @__PURE__ */ new Map();
617
607
  #memo = /* @__PURE__ */ new Map();
608
+ #stale = /* @__PURE__ */ new Set();
618
609
  constructor(schemaMetadata) {
619
610
  this.#schemaMeta = schemaMetadata;
620
611
  }
@@ -627,17 +618,19 @@ var Cache = class {
627
618
  writeQuery(artifact, variables, data) {
628
619
  const dependencies = /* @__PURE__ */ new Set();
629
620
  const subscriptions = /* @__PURE__ */ new Set();
621
+ const entityStaleCleared = /* @__PURE__ */ new Set();
630
622
  normalize(this.#schemaMeta, artifact.selections, this.#storage, data, variables, (storageKey, fieldKey, oldValue, newValue) => {
631
- if (oldValue !== newValue) {
632
- const dependencyKey = makeDependencyKey(storageKey, fieldKey);
633
- dependencies.add(dependencyKey);
634
- }
623
+ const depKey = makeDependencyKey(storageKey, fieldKey);
624
+ if (this.#stale.delete(depKey)) dependencies.add(depKey);
625
+ if (!entityStaleCleared.has(storageKey) && this.#stale.delete(storageKey)) entityStaleCleared.add(storageKey);
626
+ if (oldValue !== newValue) dependencies.add(depKey);
635
627
  });
628
+ for (const entityKey of entityStaleCleared) this.#collectSubscriptions(entityKey, void 0, subscriptions);
636
629
  for (const dependency of dependencies) {
637
630
  const ss = this.#subscriptions.get(dependency);
638
631
  if (ss) for (const s of ss) subscriptions.add(s);
639
632
  }
640
- for (const subscription of subscriptions) subscription.listener("write");
633
+ for (const subscription of subscriptions) subscription.listener();
641
634
  }
642
635
  /**
643
636
  * Reads a query result from the cache, denormalizing entities if available.
@@ -647,13 +640,22 @@ var Cache = class {
647
640
  * @returns Denormalized query result or null if not found.
648
641
  */
649
642
  readQuery(artifact, variables) {
650
- const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables);
651
- if (partial) return null;
643
+ let stale = false;
644
+ const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables, (storageKey, fieldKey) => {
645
+ if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
646
+ });
647
+ if (partial) return {
648
+ data: null,
649
+ stale: false
650
+ };
652
651
  const key = makeMemoKey("query", artifact.name, stringify(variables));
653
652
  const prev = this.#memo.get(key);
654
653
  const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
655
654
  this.#memo.set(key, result);
656
- return result;
655
+ return {
656
+ data: result,
657
+ stale
658
+ };
657
659
  }
658
660
  /**
659
661
  * Subscribes to cache invalidations for a specific query.
@@ -679,14 +681,26 @@ var Cache = class {
679
681
  */
680
682
  readFragment(artifact, fragmentRef) {
681
683
  const entityKey = fragmentRef[FragmentRefKey];
682
- if (!this.#storage[entityKey]) return null;
683
- const { data, partial } = denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {});
684
- if (partial) return null;
684
+ if (!this.#storage[entityKey]) return {
685
+ data: null,
686
+ stale: false
687
+ };
688
+ let stale = false;
689
+ const { data, partial } = denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
690
+ if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
691
+ });
692
+ if (partial) return {
693
+ data: null,
694
+ stale: false
695
+ };
685
696
  const key = makeMemoKey("fragment", artifact.name, entityKey);
686
697
  const prev = this.#memo.get(key);
687
698
  const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
688
699
  this.#memo.set(key, result);
689
- return result;
700
+ return {
701
+ data: result,
702
+ stale
703
+ };
690
704
  }
691
705
  subscribeFragment(artifact, fragmentRef, listener) {
692
706
  const entityKey = fragmentRef[FragmentRefKey];
@@ -699,17 +713,25 @@ var Cache = class {
699
713
  }
700
714
  readFragments(artifact, fragmentRefs) {
701
715
  const results = [];
716
+ let stale = false;
702
717
  for (const ref of fragmentRefs) {
703
- const data = this.readFragment(artifact, ref);
704
- if (data === null) return null;
705
- results.push(data);
718
+ const result = this.readFragment(artifact, ref);
719
+ if (result.data === null) return {
720
+ data: null,
721
+ stale: false
722
+ };
723
+ if (result.stale) stale = true;
724
+ results.push(result.data);
706
725
  }
707
726
  const entityKeys = fragmentRefs.map((ref) => ref[FragmentRefKey]);
708
727
  const key = makeMemoKey("fragments", artifact.name, entityKeys.join(","));
709
728
  const prev = this.#memo.get(key);
710
729
  const result = prev === void 0 ? results : replaceEqualDeep(prev, results);
711
730
  this.#memo.set(key, result);
712
- return result;
731
+ return {
732
+ data: result,
733
+ stale
734
+ };
713
735
  }
714
736
  subscribeFragments(artifact, fragmentRefs, listener) {
715
737
  const dependencies = /* @__PURE__ */ new Set();
@@ -727,50 +749,47 @@ var Cache = class {
727
749
  */
728
750
  invalidate(...targets) {
729
751
  const subscriptions = /* @__PURE__ */ new Set();
730
- for (const target of targets) if (target.__typename === "Query") if ("field" in target) {
731
- const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
732
- delete this.#storage[RootFieldKey]?.[fieldKey];
752
+ for (const target of targets) if (target.__typename === "Query") if ("$field" in target) {
753
+ const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
754
+ const depKey = makeDependencyKey(RootFieldKey, fieldKey);
755
+ this.#stale.add(depKey);
733
756
  this.#collectSubscriptions(RootFieldKey, fieldKey, subscriptions);
734
757
  } else {
735
- this.#storage[RootFieldKey] = {};
758
+ this.#stale.add(RootFieldKey);
736
759
  this.#collectSubscriptions(RootFieldKey, void 0, subscriptions);
737
760
  }
738
- else if ("id" in target) {
739
- const entityKey = resolveEntityKey(target.__typename, target.id, this.#schemaMeta.entities[target.__typename]?.keyFields);
740
- if ("field" in target) {
741
- const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
742
- delete this.#storage[entityKey]?.[fieldKey];
743
- this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
744
- } else {
745
- delete this.#storage[entityKey];
746
- this.#collectSubscriptions(entityKey, void 0, subscriptions);
747
- }
748
- this.#collectLinkedEntitySubscriptions((linkedEntityKey) => linkedEntityKey === entityKey, subscriptions);
749
- } else {
750
- const prefix = `${target.__typename}:`;
751
- for (const key of Object.keys(this.#storage)) if (key.startsWith(prefix)) {
752
- const entityKey = key;
753
- if ("field" in target) {
754
- const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
755
- delete this.#storage[entityKey]?.[fieldKey];
761
+ else {
762
+ const keyFields = this.#schemaMeta.entities[target.__typename]?.keyFields;
763
+ if (keyFields && this.#hasKeyFields(target, keyFields)) {
764
+ const keyValues = keyFields.map((f) => target[f]);
765
+ const entityKey = makeEntityKey(target.__typename, keyValues);
766
+ if ("$field" in target) {
767
+ const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
768
+ this.#stale.add(makeDependencyKey(entityKey, fieldKey));
756
769
  this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
757
770
  } else {
758
- delete this.#storage[entityKey];
771
+ this.#stale.add(entityKey);
759
772
  this.#collectSubscriptions(entityKey, void 0, subscriptions);
760
773
  }
774
+ } else {
775
+ const prefix = `${target.__typename}:`;
776
+ for (const key of Object.keys(this.#storage)) if (key.startsWith(prefix)) {
777
+ const entityKey = key;
778
+ if ("$field" in target) {
779
+ const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
780
+ this.#stale.add(makeDependencyKey(entityKey, fieldKey));
781
+ this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
782
+ } else {
783
+ this.#stale.add(entityKey);
784
+ this.#collectSubscriptions(entityKey, void 0, subscriptions);
785
+ }
786
+ }
761
787
  }
762
- this.#collectLinkedEntitySubscriptions((linkedEntityKey) => linkedEntityKey.startsWith(prefix), subscriptions);
763
788
  }
764
- for (const subscription of subscriptions) subscription.listener("invalidate");
765
- }
766
- #collectLinkedEntitySubscriptions(matcher, out) {
767
- for (const [storageKey, fields] of Object.entries(this.#storage)) for (const [fieldKey, value] of Object.entries(fields)) if (this.#containsEntityLink(value, matcher)) this.#collectSubscriptions(storageKey, fieldKey, out);
789
+ for (const subscription of subscriptions) subscription.listener();
768
790
  }
769
- #containsEntityLink(value, matcher) {
770
- if (isEntityLink(value)) return matcher(value[EntityLinkKey]);
771
- if (Array.isArray(value)) return value.some((item) => this.#containsEntityLink(item, matcher));
772
- if (value && typeof value === "object") return Object.values(value).some((item) => this.#containsEntityLink(item, matcher));
773
- return false;
791
+ #hasKeyFields(target, keyFields) {
792
+ return keyFields.every((f) => f in target);
774
793
  }
775
794
  #collectSubscriptions(storageKey, fieldKey, out) {
776
795
  if (fieldKey === void 0) {
@@ -824,6 +843,7 @@ var Cache = class {
824
843
  this.#storage = { [RootFieldKey]: {} };
825
844
  this.#subscriptions.clear();
826
845
  this.#memo.clear();
846
+ this.#stale.clear();
827
847
  }
828
848
  };
829
849
 
@@ -863,27 +883,16 @@ const cacheExchange = (options = {}) => {
863
883
  });
864
884
  if (isFragmentRefArray(fragmentRef)) {
865
885
  const trigger = makeSubject();
866
- let hasData = false;
867
886
  const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
868
887
  return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readFragments(op.artifact, fragmentRef), () => cache.subscribeFragments(op.artifact, fragmentRef, async () => {
869
888
  await Promise.resolve();
870
889
  trigger.next();
871
- }))), takeUntil(teardown$), mergeMap((data) => {
872
- if (data !== null) {
873
- hasData = true;
874
- return fromValue({
875
- operation: op,
876
- data,
877
- errors: []
878
- });
879
- }
880
- if (hasData) return empty();
881
- return fromValue({
882
- operation: op,
883
- data,
884
- errors: []
885
- });
886
- }));
890
+ }))), takeUntil(teardown$), map(({ data, stale }) => ({
891
+ operation: op,
892
+ data,
893
+ ...stale && { metadata: { cache: { stale: true } } },
894
+ errors: []
895
+ })));
887
896
  }
888
897
  if (!isFragmentRef(fragmentRef)) return fromValue({
889
898
  operation: op,
@@ -891,27 +900,16 @@ const cacheExchange = (options = {}) => {
891
900
  errors: []
892
901
  });
893
902
  const trigger = makeSubject();
894
- let hasData = false;
895
903
  const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
896
904
  return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readFragment(op.artifact, fragmentRef), () => cache.subscribeFragment(op.artifact, fragmentRef, async () => {
897
905
  await Promise.resolve();
898
906
  trigger.next();
899
- }))), takeUntil(teardown$), mergeMap((data) => {
900
- if (data !== null) {
901
- hasData = true;
902
- return fromValue({
903
- operation: op,
904
- data,
905
- errors: []
906
- });
907
- }
908
- if (hasData) return empty();
909
- return fromValue({
910
- operation: op,
911
- data,
912
- errors: []
913
- });
914
- }));
907
+ }))), takeUntil(teardown$), map(({ data, stale }) => ({
908
+ operation: op,
909
+ data,
910
+ ...stale && { metadata: { cache: { stale: true } } },
911
+ errors: []
912
+ })));
915
913
  }));
916
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")));
917
915
  const query$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), share());
@@ -919,30 +917,31 @@ const cacheExchange = (options = {}) => {
919
917
  return merge(fragment$, pipe(query$, mergeMap((op) => {
920
918
  const trigger = makeSubject();
921
919
  let hasData = false;
922
- let invalidated = false;
923
920
  const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
924
- return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readQuery(op.artifact, op.variables), () => cache.subscribeQuery(op.artifact, op.variables, async (event) => {
925
- if (event === "invalidate") invalidated = true;
921
+ return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readQuery(op.artifact, op.variables), () => cache.subscribeQuery(op.artifact, op.variables, async () => {
926
922
  await Promise.resolve();
927
923
  trigger.next();
928
- }))), takeUntil(teardown$), mergeMap((data) => {
929
- if (data !== null) {
930
- if (invalidated && hasData && fetchPolicy !== "cache-only") {
931
- invalidated = false;
932
- refetch$.next(op);
933
- return empty();
934
- }
924
+ }))), takeUntil(teardown$), mergeMap(({ data, stale }) => {
925
+ if (data !== null && !stale) {
926
+ hasData = true;
927
+ return fromValue({
928
+ operation: op,
929
+ data,
930
+ errors: []
931
+ });
932
+ }
933
+ if (data !== null && stale) {
935
934
  hasData = true;
936
- invalidated = false;
935
+ refetch$.next(op);
937
936
  return fromValue({
938
937
  operation: op,
939
938
  data,
939
+ metadata: { cache: { stale: true } },
940
940
  errors: []
941
941
  });
942
942
  }
943
943
  if (hasData) {
944
- if (fetchPolicy !== "cache-only") refetch$.next(op);
945
- invalidated = false;
944
+ refetch$.next(op);
946
945
  return empty();
947
946
  }
948
947
  if (fetchPolicy === "cache-only") return fromValue({
@@ -953,8 +952,8 @@ const cacheExchange = (options = {}) => {
953
952
  return empty();
954
953
  }));
955
954
  }), filter(() => fetchPolicy === "cache-only" || fetchPolicy === "cache-and-network" || fetchPolicy === "cache-first")), pipe(merge(nonCache$, pipe(query$, filter((op) => {
956
- const cached = cache.readQuery(op.artifact, op.variables);
957
- return fetchPolicy === "cache-and-network" || cached === null;
955
+ const { data } = cache.readQuery(op.artifact, op.variables);
956
+ return fetchPolicy === "cache-and-network" || data === null;
958
957
  })), pipe(ops$, filter((op) => op.variant === "teardown")), refetch$.source), forward, tap((result) => {
959
958
  if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
960
959
  }), filter((result) => result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0))));
package/package.json CHANGED
@@ -1,15 +1,22 @@
1
1
  {
2
2
  "name": "@mearie/core",
3
- "version": "0.2.4",
3
+ "version": "0.4.0",
4
4
  "description": "Type-safe, zero-overhead GraphQL client",
5
5
  "keywords": [
6
6
  "graphql",
7
7
  "graphql-client",
8
8
  "typescript",
9
+ "type-safe",
9
10
  "codegen",
10
- "cache"
11
+ "cache",
12
+ "normalized-cache",
13
+ "react",
14
+ "vue",
15
+ "svelte",
16
+ "solid",
17
+ "vite"
11
18
  ],
12
- "homepage": "https://github.com/devunt/mearie#readme",
19
+ "homepage": "https://mearie.dev/",
13
20
  "bugs": {
14
21
  "url": "https://github.com/devunt/mearie/issues"
15
22
  },
@@ -55,7 +62,7 @@
55
62
  "README.md"
56
63
  ],
57
64
  "dependencies": {
58
- "@mearie/shared": "0.2.1"
65
+ "@mearie/shared": "0.3.0"
59
66
  },
60
67
  "devDependencies": {
61
68
  "tsdown": "^0.20.3",