@mearie/core 0.2.4 → 0.3.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
@@ -594,8 +594,10 @@ const denormalize = (selections, storage, value, variables, accessor) => {
594
594
  const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
595
595
  if (name in fields) mergeFields(fields, { [name]: value });
596
596
  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));
597
+ } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
598
+ fields[FragmentRefKey] = storageKey;
599
+ if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
600
+ } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
599
601
  else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
600
602
  return fields;
601
603
  };
@@ -616,6 +618,7 @@ var Cache = class {
616
618
  #storage = { [RootFieldKey]: {} };
617
619
  #subscriptions = /* @__PURE__ */ new Map();
618
620
  #memo = /* @__PURE__ */ new Map();
621
+ #stale = /* @__PURE__ */ new Set();
619
622
  constructor(schemaMetadata) {
620
623
  this.#schemaMeta = schemaMetadata;
621
624
  }
@@ -628,17 +631,19 @@ var Cache = class {
628
631
  writeQuery(artifact, variables, data) {
629
632
  const dependencies = /* @__PURE__ */ new Set();
630
633
  const subscriptions = /* @__PURE__ */ new Set();
634
+ const entityStaleCleared = /* @__PURE__ */ new Set();
631
635
  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
- }
636
+ const depKey = makeDependencyKey(storageKey, fieldKey);
637
+ if (this.#stale.delete(depKey)) dependencies.add(depKey);
638
+ if (!entityStaleCleared.has(storageKey) && this.#stale.delete(storageKey)) entityStaleCleared.add(storageKey);
639
+ if (oldValue !== newValue) dependencies.add(depKey);
636
640
  });
641
+ for (const entityKey of entityStaleCleared) this.#collectSubscriptions(entityKey, void 0, subscriptions);
637
642
  for (const dependency of dependencies) {
638
643
  const ss = this.#subscriptions.get(dependency);
639
644
  if (ss) for (const s of ss) subscriptions.add(s);
640
645
  }
641
- for (const subscription of subscriptions) subscription.listener("write");
646
+ for (const subscription of subscriptions) subscription.listener();
642
647
  }
643
648
  /**
644
649
  * Reads a query result from the cache, denormalizing entities if available.
@@ -648,13 +653,22 @@ var Cache = class {
648
653
  * @returns Denormalized query result or null if not found.
649
654
  */
650
655
  readQuery(artifact, variables) {
651
- const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables);
652
- if (partial) return null;
656
+ let stale = false;
657
+ const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables, (storageKey, fieldKey) => {
658
+ if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
659
+ });
660
+ if (partial) return {
661
+ data: null,
662
+ stale: false
663
+ };
653
664
  const key = makeMemoKey("query", artifact.name, stringify(variables));
654
665
  const prev = this.#memo.get(key);
655
666
  const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
656
667
  this.#memo.set(key, result);
657
- return result;
668
+ return {
669
+ data: result,
670
+ stale
671
+ };
658
672
  }
659
673
  /**
660
674
  * Subscribes to cache invalidations for a specific query.
@@ -680,14 +694,26 @@ var Cache = class {
680
694
  */
681
695
  readFragment(artifact, fragmentRef) {
682
696
  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;
697
+ if (!this.#storage[entityKey]) return {
698
+ data: null,
699
+ stale: false
700
+ };
701
+ let stale = false;
702
+ const { data, partial } = denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
703
+ if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
704
+ });
705
+ if (partial) return {
706
+ data: null,
707
+ stale: false
708
+ };
686
709
  const key = makeMemoKey("fragment", artifact.name, entityKey);
687
710
  const prev = this.#memo.get(key);
688
711
  const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
689
712
  this.#memo.set(key, result);
690
- return result;
713
+ return {
714
+ data: result,
715
+ stale
716
+ };
691
717
  }
692
718
  subscribeFragment(artifact, fragmentRef, listener) {
693
719
  const entityKey = fragmentRef[FragmentRefKey];
@@ -700,17 +726,25 @@ var Cache = class {
700
726
  }
701
727
  readFragments(artifact, fragmentRefs) {
702
728
  const results = [];
729
+ let stale = false;
703
730
  for (const ref of fragmentRefs) {
704
- const data = this.readFragment(artifact, ref);
705
- if (data === null) return null;
706
- results.push(data);
731
+ const result = this.readFragment(artifact, ref);
732
+ if (result.data === null) return {
733
+ data: null,
734
+ stale: false
735
+ };
736
+ if (result.stale) stale = true;
737
+ results.push(result.data);
707
738
  }
708
739
  const entityKeys = fragmentRefs.map((ref) => ref[FragmentRefKey]);
709
740
  const key = makeMemoKey("fragments", artifact.name, entityKeys.join(","));
710
741
  const prev = this.#memo.get(key);
711
742
  const result = prev === void 0 ? results : replaceEqualDeep(prev, results);
712
743
  this.#memo.set(key, result);
713
- return result;
744
+ return {
745
+ data: result,
746
+ stale
747
+ };
714
748
  }
715
749
  subscribeFragments(artifact, fragmentRefs, listener) {
716
750
  const dependencies = /* @__PURE__ */ new Set();
@@ -730,48 +764,38 @@ var Cache = class {
730
764
  const subscriptions = /* @__PURE__ */ new Set();
731
765
  for (const target of targets) if (target.__typename === "Query") if ("field" in target) {
732
766
  const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
733
- delete this.#storage[RootFieldKey]?.[fieldKey];
767
+ const depKey = makeDependencyKey(RootFieldKey, fieldKey);
768
+ this.#stale.add(depKey);
734
769
  this.#collectSubscriptions(RootFieldKey, fieldKey, subscriptions);
735
770
  } else {
736
- this.#storage[RootFieldKey] = {};
771
+ this.#stale.add(RootFieldKey);
737
772
  this.#collectSubscriptions(RootFieldKey, void 0, subscriptions);
738
773
  }
739
774
  else if ("id" in target) {
740
775
  const entityKey = resolveEntityKey(target.__typename, target.id, this.#schemaMeta.entities[target.__typename]?.keyFields);
741
776
  if ("field" in target) {
742
777
  const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
743
- delete this.#storage[entityKey]?.[fieldKey];
778
+ this.#stale.add(makeDependencyKey(entityKey, fieldKey));
744
779
  this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
745
780
  } else {
746
- delete this.#storage[entityKey];
781
+ this.#stale.add(entityKey);
747
782
  this.#collectSubscriptions(entityKey, void 0, subscriptions);
748
783
  }
749
- this.#collectLinkedEntitySubscriptions((linkedEntityKey) => linkedEntityKey === entityKey, subscriptions);
750
784
  } else {
751
785
  const prefix = `${target.__typename}:`;
752
786
  for (const key of Object.keys(this.#storage)) if (key.startsWith(prefix)) {
753
787
  const entityKey = key;
754
788
  if ("field" in target) {
755
789
  const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
756
- delete this.#storage[entityKey]?.[fieldKey];
790
+ this.#stale.add(makeDependencyKey(entityKey, fieldKey));
757
791
  this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
758
792
  } else {
759
- delete this.#storage[entityKey];
793
+ this.#stale.add(entityKey);
760
794
  this.#collectSubscriptions(entityKey, void 0, subscriptions);
761
795
  }
762
796
  }
763
- this.#collectLinkedEntitySubscriptions((linkedEntityKey) => linkedEntityKey.startsWith(prefix), subscriptions);
764
797
  }
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);
769
- }
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;
798
+ for (const subscription of subscriptions) subscription.listener();
775
799
  }
776
800
  #collectSubscriptions(storageKey, fieldKey, out) {
777
801
  if (fieldKey === void 0) {
@@ -825,6 +849,7 @@ var Cache = class {
825
849
  this.#storage = { [RootFieldKey]: {} };
826
850
  this.#subscriptions.clear();
827
851
  this.#memo.clear();
852
+ this.#stale.clear();
828
853
  }
829
854
  };
830
855
 
@@ -864,27 +889,16 @@ const cacheExchange = (options = {}) => {
864
889
  });
865
890
  if (isFragmentRefArray(fragmentRef)) {
866
891
  const trigger = require_make.makeSubject();
867
- let hasData = false;
868
892
  const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => trigger.complete()));
869
893
  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
894
  await Promise.resolve();
871
895
  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
- }));
896
+ }))), require_make.takeUntil(teardown$), require_make.map(({ data, stale }) => ({
897
+ operation: op,
898
+ data,
899
+ ...stale && { metadata: { cache: { stale: true } } },
900
+ errors: []
901
+ })));
888
902
  }
889
903
  if (!isFragmentRef(fragmentRef)) return require_make.fromValue({
890
904
  operation: op,
@@ -892,27 +906,16 @@ const cacheExchange = (options = {}) => {
892
906
  errors: []
893
907
  });
894
908
  const trigger = require_make.makeSubject();
895
- let hasData = false;
896
909
  const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => trigger.complete()));
897
910
  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
911
  await Promise.resolve();
899
912
  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
- }));
913
+ }))), require_make.takeUntil(teardown$), require_make.map(({ data, stale }) => ({
914
+ operation: op,
915
+ data,
916
+ ...stale && { metadata: { cache: { stale: true } } },
917
+ errors: []
918
+ })));
916
919
  }));
917
920
  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
921
  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 +923,31 @@ const cacheExchange = (options = {}) => {
920
923
  return require_make.merge(fragment$, require_make.pipe(query$, require_make.mergeMap((op) => {
921
924
  const trigger = require_make.makeSubject();
922
925
  let hasData = false;
923
- let invalidated = false;
924
926
  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;
927
+ 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
928
  await Promise.resolve();
928
929
  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
- }
930
+ }))), require_make.takeUntil(teardown$), require_make.mergeMap(({ data, stale }) => {
931
+ if (data !== null && !stale) {
932
+ hasData = true;
933
+ return require_make.fromValue({
934
+ operation: op,
935
+ data,
936
+ errors: []
937
+ });
938
+ }
939
+ if (data !== null && stale) {
936
940
  hasData = true;
937
- invalidated = false;
941
+ refetch$.next(op);
938
942
  return require_make.fromValue({
939
943
  operation: op,
940
944
  data,
945
+ metadata: { cache: { stale: true } },
941
946
  errors: []
942
947
  });
943
948
  }
944
949
  if (hasData) {
945
- if (fetchPolicy !== "cache-only") refetch$.next(op);
946
- invalidated = false;
950
+ refetch$.next(op);
947
951
  return empty();
948
952
  }
949
953
  if (fetchPolicy === "cache-only") return require_make.fromValue({
@@ -954,8 +958,8 @@ const cacheExchange = (options = {}) => {
954
958
  return empty();
955
959
  }));
956
960
  }), 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;
961
+ const { data } = cache.readQuery(op.artifact, op.variables);
962
+ return fetchPolicy === "cache-and-network" || data === null;
959
963
  })), require_make.pipe(ops$, require_make.filter((op) => op.variant === "teardown")), refetch$.source), forward, require_make.tap((result) => {
960
964
  if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
961
965
  }), 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
@@ -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,7 +136,7 @@ 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;
@@ -243,6 +244,11 @@ declare module '@mearie/core' {
243
244
  interface ExchangeExtensionMap {
244
245
  cache: CacheOperations;
245
246
  }
247
+ interface OperationResultMetadataMap {
248
+ cache?: {
249
+ stale: boolean;
250
+ };
251
+ }
246
252
  }
247
253
  type CacheOptions = {
248
254
  fetchPolicy?: 'cache-first' | 'cache-and-network' | 'network-only' | 'cache-only';
@@ -345,4 +351,4 @@ declare class RequiredFieldError extends Error {
345
351
  */
346
352
  declare const stringify: (value: unknown) => string;
347
353
  //#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 };
354
+ 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 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
@@ -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,7 +136,7 @@ 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;
@@ -243,6 +244,11 @@ declare module '@mearie/core' {
243
244
  interface ExchangeExtensionMap {
244
245
  cache: CacheOperations;
245
246
  }
247
+ interface OperationResultMetadataMap {
248
+ cache?: {
249
+ stale: boolean;
250
+ };
251
+ }
246
252
  }
247
253
  type CacheOptions = {
248
254
  fetchPolicy?: 'cache-first' | 'cache-and-network' | 'network-only' | 'cache-only';
@@ -345,4 +351,4 @@ declare class RequiredFieldError extends Error {
345
351
  */
346
352
  declare const stringify: (value: unknown) => string;
347
353
  //#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 };
354
+ 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 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
@@ -593,8 +593,10 @@ const denormalize = (selections, storage, value, variables, accessor) => {
593
593
  const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
594
594
  if (name in fields) mergeFields(fields, { [name]: value });
595
595
  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));
596
+ } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
597
+ fields[FragmentRefKey] = storageKey;
598
+ if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
599
+ } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
598
600
  else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value));
599
601
  return fields;
600
602
  };
@@ -615,6 +617,7 @@ var Cache = class {
615
617
  #storage = { [RootFieldKey]: {} };
616
618
  #subscriptions = /* @__PURE__ */ new Map();
617
619
  #memo = /* @__PURE__ */ new Map();
620
+ #stale = /* @__PURE__ */ new Set();
618
621
  constructor(schemaMetadata) {
619
622
  this.#schemaMeta = schemaMetadata;
620
623
  }
@@ -627,17 +630,19 @@ var Cache = class {
627
630
  writeQuery(artifact, variables, data) {
628
631
  const dependencies = /* @__PURE__ */ new Set();
629
632
  const subscriptions = /* @__PURE__ */ new Set();
633
+ const entityStaleCleared = /* @__PURE__ */ new Set();
630
634
  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
- }
635
+ const depKey = makeDependencyKey(storageKey, fieldKey);
636
+ if (this.#stale.delete(depKey)) dependencies.add(depKey);
637
+ if (!entityStaleCleared.has(storageKey) && this.#stale.delete(storageKey)) entityStaleCleared.add(storageKey);
638
+ if (oldValue !== newValue) dependencies.add(depKey);
635
639
  });
640
+ for (const entityKey of entityStaleCleared) this.#collectSubscriptions(entityKey, void 0, subscriptions);
636
641
  for (const dependency of dependencies) {
637
642
  const ss = this.#subscriptions.get(dependency);
638
643
  if (ss) for (const s of ss) subscriptions.add(s);
639
644
  }
640
- for (const subscription of subscriptions) subscription.listener("write");
645
+ for (const subscription of subscriptions) subscription.listener();
641
646
  }
642
647
  /**
643
648
  * Reads a query result from the cache, denormalizing entities if available.
@@ -647,13 +652,22 @@ var Cache = class {
647
652
  * @returns Denormalized query result or null if not found.
648
653
  */
649
654
  readQuery(artifact, variables) {
650
- const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables);
651
- if (partial) return null;
655
+ let stale = false;
656
+ const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables, (storageKey, fieldKey) => {
657
+ if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
658
+ });
659
+ if (partial) return {
660
+ data: null,
661
+ stale: false
662
+ };
652
663
  const key = makeMemoKey("query", artifact.name, stringify(variables));
653
664
  const prev = this.#memo.get(key);
654
665
  const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
655
666
  this.#memo.set(key, result);
656
- return result;
667
+ return {
668
+ data: result,
669
+ stale
670
+ };
657
671
  }
658
672
  /**
659
673
  * Subscribes to cache invalidations for a specific query.
@@ -679,14 +693,26 @@ var Cache = class {
679
693
  */
680
694
  readFragment(artifact, fragmentRef) {
681
695
  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;
696
+ if (!this.#storage[entityKey]) return {
697
+ data: null,
698
+ stale: false
699
+ };
700
+ let stale = false;
701
+ const { data, partial } = denormalize(artifact.selections, this.#storage, { [EntityLinkKey]: entityKey }, {}, (storageKey, fieldKey) => {
702
+ if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
703
+ });
704
+ if (partial) return {
705
+ data: null,
706
+ stale: false
707
+ };
685
708
  const key = makeMemoKey("fragment", artifact.name, entityKey);
686
709
  const prev = this.#memo.get(key);
687
710
  const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
688
711
  this.#memo.set(key, result);
689
- return result;
712
+ return {
713
+ data: result,
714
+ stale
715
+ };
690
716
  }
691
717
  subscribeFragment(artifact, fragmentRef, listener) {
692
718
  const entityKey = fragmentRef[FragmentRefKey];
@@ -699,17 +725,25 @@ var Cache = class {
699
725
  }
700
726
  readFragments(artifact, fragmentRefs) {
701
727
  const results = [];
728
+ let stale = false;
702
729
  for (const ref of fragmentRefs) {
703
- const data = this.readFragment(artifact, ref);
704
- if (data === null) return null;
705
- results.push(data);
730
+ const result = this.readFragment(artifact, ref);
731
+ if (result.data === null) return {
732
+ data: null,
733
+ stale: false
734
+ };
735
+ if (result.stale) stale = true;
736
+ results.push(result.data);
706
737
  }
707
738
  const entityKeys = fragmentRefs.map((ref) => ref[FragmentRefKey]);
708
739
  const key = makeMemoKey("fragments", artifact.name, entityKeys.join(","));
709
740
  const prev = this.#memo.get(key);
710
741
  const result = prev === void 0 ? results : replaceEqualDeep(prev, results);
711
742
  this.#memo.set(key, result);
712
- return result;
743
+ return {
744
+ data: result,
745
+ stale
746
+ };
713
747
  }
714
748
  subscribeFragments(artifact, fragmentRefs, listener) {
715
749
  const dependencies = /* @__PURE__ */ new Set();
@@ -729,48 +763,38 @@ var Cache = class {
729
763
  const subscriptions = /* @__PURE__ */ new Set();
730
764
  for (const target of targets) if (target.__typename === "Query") if ("field" in target) {
731
765
  const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
732
- delete this.#storage[RootFieldKey]?.[fieldKey];
766
+ const depKey = makeDependencyKey(RootFieldKey, fieldKey);
767
+ this.#stale.add(depKey);
733
768
  this.#collectSubscriptions(RootFieldKey, fieldKey, subscriptions);
734
769
  } else {
735
- this.#storage[RootFieldKey] = {};
770
+ this.#stale.add(RootFieldKey);
736
771
  this.#collectSubscriptions(RootFieldKey, void 0, subscriptions);
737
772
  }
738
773
  else if ("id" in target) {
739
774
  const entityKey = resolveEntityKey(target.__typename, target.id, this.#schemaMeta.entities[target.__typename]?.keyFields);
740
775
  if ("field" in target) {
741
776
  const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
742
- delete this.#storage[entityKey]?.[fieldKey];
777
+ this.#stale.add(makeDependencyKey(entityKey, fieldKey));
743
778
  this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
744
779
  } else {
745
- delete this.#storage[entityKey];
780
+ this.#stale.add(entityKey);
746
781
  this.#collectSubscriptions(entityKey, void 0, subscriptions);
747
782
  }
748
- this.#collectLinkedEntitySubscriptions((linkedEntityKey) => linkedEntityKey === entityKey, subscriptions);
749
783
  } else {
750
784
  const prefix = `${target.__typename}:`;
751
785
  for (const key of Object.keys(this.#storage)) if (key.startsWith(prefix)) {
752
786
  const entityKey = key;
753
787
  if ("field" in target) {
754
788
  const fieldKey = makeFieldKeyFromArgs(target.field, target.args);
755
- delete this.#storage[entityKey]?.[fieldKey];
789
+ this.#stale.add(makeDependencyKey(entityKey, fieldKey));
756
790
  this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
757
791
  } else {
758
- delete this.#storage[entityKey];
792
+ this.#stale.add(entityKey);
759
793
  this.#collectSubscriptions(entityKey, void 0, subscriptions);
760
794
  }
761
795
  }
762
- this.#collectLinkedEntitySubscriptions((linkedEntityKey) => linkedEntityKey.startsWith(prefix), subscriptions);
763
796
  }
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);
768
- }
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;
797
+ for (const subscription of subscriptions) subscription.listener();
774
798
  }
775
799
  #collectSubscriptions(storageKey, fieldKey, out) {
776
800
  if (fieldKey === void 0) {
@@ -824,6 +848,7 @@ var Cache = class {
824
848
  this.#storage = { [RootFieldKey]: {} };
825
849
  this.#subscriptions.clear();
826
850
  this.#memo.clear();
851
+ this.#stale.clear();
827
852
  }
828
853
  };
829
854
 
@@ -863,27 +888,16 @@ const cacheExchange = (options = {}) => {
863
888
  });
864
889
  if (isFragmentRefArray(fragmentRef)) {
865
890
  const trigger = makeSubject();
866
- let hasData = false;
867
891
  const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
868
892
  return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readFragments(op.artifact, fragmentRef), () => cache.subscribeFragments(op.artifact, fragmentRef, async () => {
869
893
  await Promise.resolve();
870
894
  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
- }));
895
+ }))), takeUntil(teardown$), map(({ data, stale }) => ({
896
+ operation: op,
897
+ data,
898
+ ...stale && { metadata: { cache: { stale: true } } },
899
+ errors: []
900
+ })));
887
901
  }
888
902
  if (!isFragmentRef(fragmentRef)) return fromValue({
889
903
  operation: op,
@@ -891,27 +905,16 @@ const cacheExchange = (options = {}) => {
891
905
  errors: []
892
906
  });
893
907
  const trigger = makeSubject();
894
- let hasData = false;
895
908
  const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
896
909
  return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readFragment(op.artifact, fragmentRef), () => cache.subscribeFragment(op.artifact, fragmentRef, async () => {
897
910
  await Promise.resolve();
898
911
  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
- }));
912
+ }))), takeUntil(teardown$), map(({ data, stale }) => ({
913
+ operation: op,
914
+ data,
915
+ ...stale && { metadata: { cache: { stale: true } } },
916
+ errors: []
917
+ })));
915
918
  }));
916
919
  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
920
  const query$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), share());
@@ -919,30 +922,31 @@ const cacheExchange = (options = {}) => {
919
922
  return merge(fragment$, pipe(query$, mergeMap((op) => {
920
923
  const trigger = makeSubject();
921
924
  let hasData = false;
922
- let invalidated = false;
923
925
  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;
926
+ return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readQuery(op.artifact, op.variables), () => cache.subscribeQuery(op.artifact, op.variables, async () => {
926
927
  await Promise.resolve();
927
928
  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
- }
929
+ }))), takeUntil(teardown$), mergeMap(({ data, stale }) => {
930
+ if (data !== null && !stale) {
931
+ hasData = true;
932
+ return fromValue({
933
+ operation: op,
934
+ data,
935
+ errors: []
936
+ });
937
+ }
938
+ if (data !== null && stale) {
935
939
  hasData = true;
936
- invalidated = false;
940
+ refetch$.next(op);
937
941
  return fromValue({
938
942
  operation: op,
939
943
  data,
944
+ metadata: { cache: { stale: true } },
940
945
  errors: []
941
946
  });
942
947
  }
943
948
  if (hasData) {
944
- if (fetchPolicy !== "cache-only") refetch$.next(op);
945
- invalidated = false;
949
+ refetch$.next(op);
946
950
  return empty();
947
951
  }
948
952
  if (fetchPolicy === "cache-only") return fromValue({
@@ -953,8 +957,8 @@ const cacheExchange = (options = {}) => {
953
957
  return empty();
954
958
  }));
955
959
  }), 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;
960
+ const { data } = cache.readQuery(op.artifact, op.variables);
961
+ return fetchPolicy === "cache-and-network" || data === null;
958
962
  })), pipe(ops$, filter((op) => op.variant === "teardown")), refetch$.source), forward, tap((result) => {
959
963
  if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
960
964
  }), 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.3.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.2.2"
59
66
  },
60
67
  "devDependencies": {
61
68
  "tsdown": "^0.20.3",