@mearie/core 0.6.0 → 0.6.2

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.
Files changed (3) hide show
  1. package/dist/index.cjs +759 -749
  2. package/dist/index.mjs +759 -749
  3. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -588,7 +588,7 @@ const normalize = (schemaMeta, selections, storage, data, variables, accessor) =
588
588
 
589
589
  //#endregion
590
590
  //#region src/cache/denormalize.ts
591
- const typenameFieldKey = makeFieldKey({
591
+ const typenameFieldKey$1 = makeFieldKey({
592
592
  kind: "Field",
593
593
  name: "__typename",
594
594
  type: "String"
@@ -603,7 +603,7 @@ const denormalize = (selections, storage, value, variables, accessor, options) =
603
603
  const entityKey = data[EntityLinkKey];
604
604
  const entity = storage[entityKey];
605
605
  if (!entity) {
606
- accessor?.(entityKey, typenameFieldKey, path);
606
+ accessor?.(entityKey, typenameFieldKey$1, path);
607
607
  partial = true;
608
608
  return null;
609
609
  }
@@ -650,7 +650,7 @@ const denormalize = (selections, storage, value, variables, accessor, options) =
650
650
  if (denormalize(selection.selections, storage, storage[RootFieldKey], variables, options?.trackFragmentDeps === false ? void 0 : accessor, options).partial) partial = true;
651
651
  }
652
652
  } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value, path), true);
653
- else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value, path), true);
653
+ else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey$1]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value, path), true);
654
654
  return fields;
655
655
  };
656
656
  return {
@@ -660,159 +660,146 @@ const denormalize = (selections, storage, value, variables, accessor, options) =
660
660
  };
661
661
 
662
662
  //#endregion
663
- //#region src/cache/tree.ts
663
+ //#region src/cache/cursor.ts
664
664
  /**
665
+ * Reverse index mapping dependency keys to cursor entries.
665
666
  * @internal
666
667
  */
667
- const buildEntryTree = (tuples, rootDepKey) => {
668
- const root = {
669
- depKey: rootDepKey ?? "__root",
670
- children: /* @__PURE__ */ new Map()
671
- };
672
- for (const { storageKey, fieldKey, path, selections } of tuples) {
673
- let current = root;
674
- for (const element of path) {
675
- const key = String(element);
676
- let child = current.children.get(key);
677
- if (!child) {
678
- child = {
679
- depKey: "",
680
- children: /* @__PURE__ */ new Map()
681
- };
682
- current.children.set(key, child);
683
- }
684
- current = child;
668
+ var CursorRegistry = class {
669
+ #index = /* @__PURE__ */ new Map();
670
+ add(depKey, entry) {
671
+ let set = this.#index.get(depKey);
672
+ if (!set) {
673
+ set = /* @__PURE__ */ new Set();
674
+ this.#index.set(depKey, set);
685
675
  }
686
- current.depKey = makeDependencyKey(storageKey, fieldKey);
687
- if (selections) current.selections = selections;
676
+ set.add(entry);
688
677
  }
689
- return root;
690
- };
691
- /**
692
- * @internal
693
- */
694
- const findEntryTreeNode = (root, path) => {
695
- let current = root;
696
- for (const segment of path) {
697
- if (!current) return void 0;
698
- current = current.children.get(String(segment));
678
+ get(depKey) {
679
+ return this.#index.get(depKey);
699
680
  }
700
- return current;
701
- };
702
- /**
703
- * Removes all subscription entries for a given subscription from the subtree rooted at {@link node},
704
- * and clears the node's children map. Both the subscription entries and the tree structure
705
- * are cleaned up atomically to avoid stale references.
706
- * @internal
707
- */
708
- const removeSubtreeEntries = (node, subscription, subscriptions) => {
709
- const entries = subscriptions.get(node.depKey);
710
- if (entries) {
711
- for (const entry of entries) if (entry.subscription === subscription) {
712
- entries.delete(entry);
713
- break;
681
+ remove(depKey, entry) {
682
+ const set = this.#index.get(depKey);
683
+ if (set) {
684
+ set.delete(entry);
685
+ if (set.size === 0) this.#index.delete(depKey);
714
686
  }
715
- if (entries.size === 0) subscriptions.delete(node.depKey);
716
687
  }
717
- for (const child of node.children.values()) removeSubtreeEntries(child, subscription, subscriptions);
718
- node.children.clear();
719
- };
720
- /**
721
- * @internal
722
- */
723
- const snapshotFields = (node, storage) => {
724
- const result = /* @__PURE__ */ new Map();
725
- for (const [fieldName, child] of node.children) {
726
- const { storageKey, fieldKey } = parseDependencyKey(child.depKey);
727
- const fields = storage[storageKey];
728
- if (fields) result.set(fieldName, fields[fieldKey]);
688
+ removeAll(cursors) {
689
+ for (const [depKey, set] of this.#index) {
690
+ for (const cursor of cursors) if (set.has(cursor)) set.delete(cursor);
691
+ if (set.size === 0) this.#index.delete(depKey);
692
+ }
693
+ }
694
+ forEachByPrefix(prefix, callback) {
695
+ for (const [depKey, set] of this.#index) if (depKey.startsWith(prefix)) for (const entry of set) callback(entry);
696
+ }
697
+ clear() {
698
+ this.#index.clear();
729
699
  }
730
- return result;
731
700
  };
701
+ const typenameFieldKey = makeFieldKey({
702
+ kind: "Field",
703
+ name: "__typename",
704
+ type: "String"
705
+ }, {});
732
706
  /**
707
+ * Walks selections against storage to produce cursor entries, check completeness,
708
+ * and build denormalized data.
733
709
  * @internal
734
710
  */
735
- const partialDenormalize = (node, entity, basePath, rebuiltDepKeys, storage, subscriptions, subscription) => {
736
- if (!node.selections) return {
737
- data: null,
738
- fieldValues: /* @__PURE__ */ new Map()
739
- };
740
- const tuples = [];
741
- const { data } = denormalize(node.selections, storage, entity, subscription.variables, (storageKey, fieldKey, path, sels) => {
742
- tuples.push({
743
- storageKey,
744
- fieldKey,
745
- path: [...basePath, ...path],
746
- selections: sels
747
- });
748
- }, { trackFragmentDeps: false });
749
- node.children.clear();
750
- const fieldValues = /* @__PURE__ */ new Map();
751
- for (const tuple of tuples) {
752
- const depKey = makeDependencyKey(tuple.storageKey, tuple.fieldKey);
753
- rebuiltDepKeys.add(depKey);
754
- const relativePath = tuple.path.slice(basePath.length);
755
- let current = node;
756
- for (const element of relativePath) {
757
- const key = String(element);
758
- let child = current.children.get(key);
759
- if (!child) {
760
- child = {
761
- depKey: "",
762
- children: /* @__PURE__ */ new Map()
763
- };
764
- current.children.set(key, child);
711
+ const traceSelections = (selections, storage, value, variables, storageKey, basePath, subscriptionId) => {
712
+ const cursors = [];
713
+ const missingDeps = /* @__PURE__ */ new Set();
714
+ let complete = true;
715
+ const traceField = (sk, sels, val, path, trackCursors) => {
716
+ if (isNullish(val)) return val;
717
+ if (Array.isArray(val)) return val.map((item, i) => traceField(sk, sels, item, [...path, i], trackCursors));
718
+ const data = val;
719
+ if (isEntityLink(data)) {
720
+ const entityKey = data[EntityLinkKey];
721
+ const entity = storage[entityKey];
722
+ if (!entity) {
723
+ if (trackCursors) {
724
+ const depKey = makeDependencyKey(entityKey, typenameFieldKey);
725
+ missingDeps.add(depKey);
726
+ }
727
+ complete = false;
728
+ return null;
765
729
  }
766
- current = child;
730
+ return traceField(entityKey, sels, entity, path, trackCursors);
767
731
  }
768
- current.depKey = depKey;
769
- if (tuple.selections) current.selections = tuple.selections;
770
- const entry = {
771
- path: tuple.path,
772
- subscription
773
- };
774
- let entrySet = subscriptions.get(depKey);
775
- if (!entrySet) {
776
- entrySet = /* @__PURE__ */ new Set();
777
- subscriptions.set(depKey, entrySet);
778
- }
779
- entrySet.add(entry);
780
- if (relativePath.length === 1) {
781
- const fieldName = String(relativePath[0]);
782
- if (data && typeof data === "object") fieldValues.set(fieldName, data[fieldName]);
783
- }
784
- }
732
+ const fields = {};
733
+ for (const selection of sels) if (selection.kind === "Field") {
734
+ const fieldKey = makeFieldKey(selection, variables);
735
+ const fieldValue = data[fieldKey];
736
+ const fieldPath = [...path, selection.alias ?? selection.name];
737
+ if (sk !== null && trackCursors) {
738
+ const depKey = makeDependencyKey(sk, fieldKey);
739
+ const entry = {
740
+ subscriptionId,
741
+ path: fieldPath,
742
+ ...selection.selections && { selections: selection.selections }
743
+ };
744
+ cursors.push({
745
+ depKey,
746
+ entry
747
+ });
748
+ }
749
+ if (fieldValue === void 0) {
750
+ if (sk !== null) {
751
+ const depKey = makeDependencyKey(sk, fieldKey);
752
+ missingDeps.add(depKey);
753
+ }
754
+ complete = false;
755
+ continue;
756
+ }
757
+ const name = selection.alias ?? selection.name;
758
+ const resolvedValue = selection.selections ? traceField(null, selection.selections, fieldValue, fieldPath, trackCursors) : fieldValue;
759
+ if (name in fields) mergeFields(fields, { [name]: resolvedValue }, true);
760
+ else fields[name] = resolvedValue;
761
+ } else if (selection.kind === "FragmentSpread") if (sk !== null && sk !== RootFieldKey) {
762
+ fields[FragmentRefKey] = sk;
763
+ const merged = selection.args ? {
764
+ ...variables,
765
+ ...resolveArguments(selection.args, variables)
766
+ } : { ...variables };
767
+ fields[FragmentVarsKey] = {
768
+ ...fields[FragmentVarsKey],
769
+ [selection.name]: merged
770
+ };
771
+ const inner = traceSelections(selection.selections, storage, storage[sk], variables, sk, path, subscriptionId);
772
+ if (!inner.complete) {
773
+ complete = false;
774
+ for (const dep of inner.missingDeps) missingDeps.add(dep);
775
+ }
776
+ } else if (sk === RootFieldKey) {
777
+ fields[FragmentRefKey] = RootFieldKey;
778
+ const merged = selection.args ? {
779
+ ...variables,
780
+ ...resolveArguments(selection.args, variables)
781
+ } : { ...variables };
782
+ fields[FragmentVarsKey] = {
783
+ ...fields[FragmentVarsKey],
784
+ [selection.name]: merged
785
+ };
786
+ const inner = traceSelections(selection.selections, storage, storage[RootFieldKey], variables, RootFieldKey, path, subscriptionId);
787
+ if (!inner.complete) {
788
+ complete = false;
789
+ for (const dep of inner.missingDeps) missingDeps.add(dep);
790
+ }
791
+ } else mergeFields(fields, traceField(sk, selection.selections, val, path, trackCursors), true);
792
+ else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, traceField(sk, selection.selections, val, path, trackCursors), true);
793
+ return fields;
794
+ };
795
+ const data = traceField(storageKey, selections, value, basePath, true);
785
796
  return {
786
- data,
787
- fieldValues
797
+ complete,
798
+ cursors,
799
+ missingDeps,
800
+ data
788
801
  };
789
802
  };
790
- const updateSubtreePaths = (node, basePath, newIndex, baseLen, subscription, subscriptions) => {
791
- const entries = subscriptions.get(node.depKey);
792
- if (entries) {
793
- for (const entry of entries) if (entry.subscription === subscription && entry.path.length > baseLen) entry.path = [
794
- ...basePath,
795
- newIndex,
796
- ...entry.path.slice(baseLen + 1)
797
- ];
798
- }
799
- for (const child of node.children.values()) updateSubtreePaths(child, basePath, newIndex, baseLen, subscription, subscriptions);
800
- };
801
- /**
802
- * @internal
803
- */
804
- const rebuildArrayIndices = (node, entry, subscriptions) => {
805
- const basePath = entry.path;
806
- const baseLen = basePath.length;
807
- const children = [...node.children.entries()].toSorted(([a], [b]) => Number(a) - Number(b));
808
- node.children.clear();
809
- for (const [newIdx, child_] of children.entries()) {
810
- const [, child] = child_;
811
- const newKey = String(newIdx);
812
- node.children.set(newKey, child);
813
- updateSubtreePaths(child, basePath, newIdx, baseLen, entry.subscription, subscriptions);
814
- }
815
- };
816
803
 
817
804
  //#endregion
818
805
  //#region src/cache/diff.ts
@@ -854,27 +841,139 @@ const computeSwaps = (oldKeys, newKeys) => {
854
841
  }
855
842
  return swaps;
856
843
  };
844
+ /**
845
+ * @internal
846
+ */
847
+ const extractEntityKey = (item) => {
848
+ if (item !== null && item !== void 0 && typeof item === "object" && EntityLinkKey in item) return String(item[EntityLinkKey]);
849
+ return null;
850
+ };
851
+ /**
852
+ * @internal
853
+ */
854
+ const computeEntityArrayPatches = (oldValue, newValue, path, denormalizedArray) => {
855
+ const patches = [];
856
+ const oldKeys = oldValue.map((item) => extractEntityKey(item));
857
+ const newKeys = newValue.map((item) => extractEntityKey(item));
858
+ const { start, oldEnd, newEnd } = findCommonBounds(oldKeys, newKeys);
859
+ const oldMiddle = oldKeys.slice(start, oldEnd);
860
+ const newMiddle = newKeys.slice(start, newEnd);
861
+ const oldMiddleSet = new Set(oldMiddle.filter((k) => k !== null));
862
+ const newMiddleSet = new Set(newMiddle.filter((k) => k !== null));
863
+ for (let i = oldMiddle.length - 1; i >= 0; i--) if (oldMiddle[i] !== null && !newMiddleSet.has(oldMiddle[i])) patches.push({
864
+ type: "splice",
865
+ path,
866
+ index: start + i,
867
+ deleteCount: 1,
868
+ items: []
869
+ });
870
+ const retainedOld = oldMiddle.filter((k) => k !== null && newMiddleSet.has(k));
871
+ const retainedNew = newMiddle.filter((k) => k !== null && oldMiddleSet.has(k));
872
+ if (retainedOld.length > 0) {
873
+ const swaps = computeSwaps(retainedOld, retainedNew);
874
+ for (const { i, j } of swaps) patches.push({
875
+ type: "swap",
876
+ path,
877
+ i: start + i,
878
+ j: start + j
879
+ });
880
+ }
881
+ const addedKeys = newMiddle.filter((k) => k !== null && !oldMiddleSet.has(k));
882
+ for (const key of addedKeys) {
883
+ const idx = start + newMiddle.indexOf(key);
884
+ const data = denormalizedArray[idx] ?? null;
885
+ patches.push({
886
+ type: "splice",
887
+ path,
888
+ index: idx,
889
+ deleteCount: 0,
890
+ items: [data]
891
+ });
892
+ }
893
+ return patches;
894
+ };
895
+ const pathToKey = (path) => path.map(String).join("\0");
896
+ /**
897
+ * Diffs two denormalized data snapshots to produce patches.
898
+ * Handles entity link arrays with identity-aware splice/swap patches.
899
+ * @internal
900
+ */
901
+ const diffSnapshots = (oldData, newData, entityArrayChanges) => {
902
+ const patches = [];
903
+ const diff = (old, cur, path) => {
904
+ if (isEqual(old, cur)) return;
905
+ if (cur === null || cur === void 0 || old === null || old === void 0 || typeof cur !== "object" || typeof old !== "object") {
906
+ patches.push({
907
+ type: "set",
908
+ path,
909
+ value: cur
910
+ });
911
+ return;
912
+ }
913
+ if (Array.isArray(cur)) {
914
+ if (entityArrayChanges && Array.isArray(old)) {
915
+ const key = pathToKey(path);
916
+ const change = entityArrayChanges.get(key);
917
+ if (change) {
918
+ diffEntityArray(old, cur, path, change);
919
+ return;
920
+ }
921
+ }
922
+ patches.push({
923
+ type: "set",
924
+ path,
925
+ value: cur
926
+ });
927
+ return;
928
+ }
929
+ if (Array.isArray(old)) {
930
+ patches.push({
931
+ type: "set",
932
+ path,
933
+ value: cur
934
+ });
935
+ return;
936
+ }
937
+ const oldObj = old;
938
+ const curObj = cur;
939
+ for (const key of Object.keys(curObj)) {
940
+ if (key === "__fragmentRef" || key === "__fragmentVars") continue;
941
+ diff(oldObj[key], curObj[key], [...path, key]);
942
+ }
943
+ };
944
+ const diffEntityArray = (old, cur, path, change) => {
945
+ const { oldKeys, newKeys } = change;
946
+ const oldByKey = /* @__PURE__ */ new Map();
947
+ for (const [i, key] of oldKeys.entries()) if (key) oldByKey.set(key, old[i]);
948
+ const arrayPatches = computeEntityArrayPatches(oldKeys.map((k) => k ? { [EntityLinkKey]: k } : null), newKeys.map((k) => k ? { [EntityLinkKey]: k } : null), path, cur);
949
+ patches.push(...arrayPatches);
950
+ for (const [i, item] of cur.entries()) {
951
+ const entityKey = newKeys[i];
952
+ diff(entityKey ? oldByKey.get(entityKey) : void 0, item, [...path, i]);
953
+ }
954
+ };
955
+ diff(oldData, newData, []);
956
+ return patches;
957
+ };
958
+ /**
959
+ * @internal
960
+ */
961
+ const pathToKeyFn = pathToKey;
857
962
 
858
963
  //#endregion
859
964
  //#region src/cache/change.ts
860
965
  /**
861
966
  * @internal
862
967
  */
863
- const classifyChanges = (changedKeys) => {
968
+ const classifyChanges = (changes) => {
864
969
  const structural = [];
865
970
  const scalar = [];
866
- for (const [depKey, { oldValue, newValue }] of changedKeys) {
971
+ for (const change of changes) {
972
+ const { oldValue, newValue } = change;
867
973
  if (isEntityLink(oldValue) && isEntityLink(newValue) && oldValue[EntityLinkKey] === newValue[EntityLinkKey]) continue;
868
974
  if (isEntityLinkArray(oldValue) && isEntityLinkArray(newValue) && isEntityLinkArrayEqual(oldValue, newValue)) continue;
869
- if (isEntityLink(oldValue) || isEntityLink(newValue) || isEntityLinkArray(oldValue) || isEntityLinkArray(newValue)) structural.push({
870
- depKey,
871
- oldValue,
872
- newValue
873
- });
874
- else scalar.push({
875
- depKey,
876
- newValue
877
- });
975
+ if (isEntityLink(oldValue) || isEntityLink(newValue) || isEntityLinkArray(oldValue) || isEntityLinkArray(newValue)) structural.push(change);
976
+ else scalar.push(change);
878
977
  }
879
978
  return {
880
979
  structural,
@@ -884,201 +983,229 @@ const classifyChanges = (changedKeys) => {
884
983
  /**
885
984
  * @internal
886
985
  */
887
- const processStructuralChange = (entry, node, oldValue, newValue, rebuiltDepKeys, storage, subscriptions) => {
888
- const patches = [];
889
- if (isEntityLink(oldValue) || isEntityLink(newValue)) {
890
- if (isNullish(newValue)) {
891
- removeSubtreeEntries(node, entry.subscription, subscriptions);
986
+ const processScalarChanges = (changes, registry, subscriptions) => {
987
+ const result = /* @__PURE__ */ new Map();
988
+ for (const change of changes) {
989
+ const entries = registry.get(change.depKey);
990
+ if (!entries) continue;
991
+ for (const entry of entries) {
992
+ const sub = subscriptions.get(entry.subscriptionId);
993
+ if (!sub) continue;
994
+ let patchValue = change.newValue;
995
+ if (entry.selections && isNormalizedRecord(change.newValue)) {
996
+ const { data } = denormalize(entry.selections, {}, change.newValue, sub.variables);
997
+ patchValue = data;
998
+ }
999
+ const patches = result.get(entry.subscriptionId) ?? [];
892
1000
  patches.push({
893
1001
  type: "set",
894
1002
  path: entry.path,
895
- value: null
1003
+ value: patchValue
896
1004
  });
897
- return patches;
1005
+ result.set(entry.subscriptionId, patches);
898
1006
  }
899
- if (isNullish(oldValue)) {
900
- const entity = storage[newValue[EntityLinkKey]];
901
- if (entity) {
902
- const { data } = partialDenormalize(node, entity, entry.path, rebuiltDepKeys, storage, subscriptions, entry.subscription);
903
- patches.push({
904
- type: "set",
905
- path: entry.path,
906
- value: data
907
- });
908
- } else patches.push({
909
- type: "set",
910
- path: entry.path,
911
- value: null
1007
+ }
1008
+ return result;
1009
+ };
1010
+ /**
1011
+ * @internal
1012
+ */
1013
+ const buildEntityArrayContext = (changes, cursors) => {
1014
+ const result = /* @__PURE__ */ new Map();
1015
+ for (const change of changes) {
1016
+ if (!isEntityLinkArray(change.oldValue) && !isEntityLinkArray(change.newValue)) continue;
1017
+ for (const { depKey, entry } of cursors) if (depKey === change.depKey) {
1018
+ const oldArr = Array.isArray(change.oldValue) ? change.oldValue : [];
1019
+ const newArr = Array.isArray(change.newValue) ? change.newValue : [];
1020
+ const key = pathToKeyFn(entry.path);
1021
+ result.set(key, {
1022
+ oldKeys: oldArr.map((item) => extractEntityKey(item)),
1023
+ newKeys: newArr.map((item) => extractEntityKey(item))
912
1024
  });
913
- return patches;
1025
+ break;
914
1026
  }
915
- const oldFields = snapshotFields(node, storage);
916
- removeSubtreeEntries(node, entry.subscription, subscriptions);
917
- const newEntity = storage[newValue[EntityLinkKey]];
918
- if (!newEntity) {
919
- patches.push({
920
- type: "set",
921
- path: entry.path,
922
- value: null
1027
+ }
1028
+ return result.size > 0 ? result : void 0;
1029
+ };
1030
+ /**
1031
+ * @internal
1032
+ */
1033
+ const processStructuralChanges = (changes, registry, subscriptions, storage, stalled) => {
1034
+ const result = /* @__PURE__ */ new Map();
1035
+ const processedSubs = /* @__PURE__ */ new Set();
1036
+ for (const change of changes) {
1037
+ const entries = registry.get(change.depKey);
1038
+ if (!entries) continue;
1039
+ for (const entry of entries) {
1040
+ const subId = entry.subscriptionId;
1041
+ if (processedSubs.has(subId)) continue;
1042
+ const sub = subscriptions.get(subId);
1043
+ if (!sub) continue;
1044
+ processedSubs.add(subId);
1045
+ registry.removeAll(sub.cursors);
1046
+ const rootStorageKey = sub.entityKey ?? "__root";
1047
+ const rootValue = storage[rootStorageKey];
1048
+ if (!rootValue) continue;
1049
+ const traceResult = traceSelections(sub.artifact.selections, storage, rootValue, sub.variables, rootStorageKey, [], sub.id);
1050
+ sub.cursors = new Set(traceResult.cursors.map((c) => c.entry));
1051
+ for (const { depKey, entry: cursorEntry } of traceResult.cursors) registry.add(depKey, cursorEntry);
1052
+ if (traceResult.complete) {
1053
+ stalled.delete(subId);
1054
+ const entityArrayChanges = buildEntityArrayContext(changes, traceResult.cursors);
1055
+ const patches = diffSnapshots(sub.data, traceResult.data, entityArrayChanges);
1056
+ sub.data = traceResult.data;
1057
+ if (patches.length > 0) result.set(subId, patches);
1058
+ } else stalled.set(subId, {
1059
+ subscription: sub,
1060
+ missingDeps: traceResult.missingDeps
923
1061
  });
924
- return patches;
925
1062
  }
926
- const { fieldValues: newFields } = partialDenormalize(node, newEntity, entry.path, rebuiltDepKeys, storage, subscriptions, entry.subscription);
927
- for (const [fieldName, newVal] of newFields) if (!isEqual(oldFields.get(fieldName), newVal)) patches.push({
928
- type: "set",
929
- path: [...entry.path, fieldName],
930
- value: newVal
931
- });
932
- for (const [fieldName] of oldFields) if (!newFields.has(fieldName)) patches.push({
933
- type: "set",
934
- path: [...entry.path, fieldName],
935
- value: null
1063
+ }
1064
+ return result;
1065
+ };
1066
+
1067
+ //#endregion
1068
+ //#region src/cache/optimistic.ts
1069
+ /**
1070
+ * CoW optimistic stack that tracks field-level changes for rollback.
1071
+ * @internal
1072
+ */
1073
+ var OptimisticStack = class {
1074
+ #stack = [];
1075
+ push(key, changes) {
1076
+ this.#stack.push({
1077
+ key,
1078
+ changes
936
1079
  });
937
- return patches;
938
- }
939
- if (isEntityLinkArray(oldValue) || isEntityLinkArray(newValue)) {
940
- const oldArr = Array.isArray(oldValue) ? oldValue : [];
941
- const newArr = Array.isArray(newValue) ? newValue : [];
942
- const oldKeys = oldArr.map((item) => item !== null && item !== void 0 && typeof item === "object" && EntityLinkKey in item ? item[EntityLinkKey] : null);
943
- const newKeys = newArr.map((item) => item !== null && item !== void 0 && typeof item === "object" && EntityLinkKey in item ? item[EntityLinkKey] : null);
944
- const { start, oldEnd, newEnd } = findCommonBounds(oldKeys, newKeys);
945
- const oldMiddle = oldKeys.slice(start, oldEnd);
946
- const newMiddle = newKeys.slice(start, newEnd);
947
- const newMiddleSet = new Set(newMiddle.filter((k) => k !== null));
948
- const oldMiddleSet = new Set(oldMiddle.filter((k) => k !== null));
949
- const removedIndices = [];
950
- for (let i = oldMiddle.length - 1; i >= 0; i--) {
951
- const key = oldMiddle[i];
952
- if (key !== null && !newMiddleSet.has(key)) removedIndices.push(start + i);
953
- }
954
- for (const idx of removedIndices) {
955
- const childKey = String(idx);
956
- const child = node.children.get(childKey);
957
- if (child) {
958
- removeSubtreeEntries(child, entry.subscription, subscriptions);
959
- node.children.delete(childKey);
1080
+ }
1081
+ has(key) {
1082
+ return this.#stack.some((e) => e.key === key);
1083
+ }
1084
+ rollback(key) {
1085
+ const idx = this.#stack.findIndex((e) => e.key === key);
1086
+ if (idx === -1) return [];
1087
+ const entry = this.#stack[idx];
1088
+ this.#stack.splice(idx, 1);
1089
+ const restorations = [];
1090
+ for (const [depKey, { old: oldVal, new: newVal }] of entry.changes) {
1091
+ const laterIdx = this.#stack.slice(idx).findIndex((later) => later.changes.has(depKey));
1092
+ if (laterIdx !== -1) {
1093
+ const laterChange = this.#stack[idx + laterIdx].changes.get(depKey);
1094
+ laterChange.old = oldVal;
1095
+ continue;
960
1096
  }
961
- patches.push({
962
- type: "splice",
963
- path: entry.path,
964
- index: idx,
965
- deleteCount: 1,
966
- items: []
1097
+ const earlierEntry = this.#findClosestEarlier(depKey, idx);
1098
+ const restoreValue = earlierEntry === void 0 ? oldVal : earlierEntry;
1099
+ const { storageKey, fieldKey } = parseDependencyKey(depKey);
1100
+ restorations.push({
1101
+ depKey,
1102
+ storageKey,
1103
+ fieldKey,
1104
+ oldValue: newVal,
1105
+ newValue: restoreValue
967
1106
  });
968
1107
  }
969
- compactChildren(node);
970
- const retainedOld = oldMiddle.filter((k) => k !== null && newMiddleSet.has(k));
971
- const retainedNew = newMiddle.filter((k) => k !== null && oldMiddleSet.has(k));
972
- if (retainedOld.length > 0) {
973
- const swaps = computeSwaps(retainedOld, retainedNew);
974
- for (const { i, j } of swaps) {
975
- const absI = start + i;
976
- const absJ = start + j;
977
- patches.push({
978
- type: "swap",
979
- path: entry.path,
980
- i: absI,
981
- j: absJ
982
- });
983
- const childI = node.children.get(String(absI));
984
- const childJ = node.children.get(String(absJ));
985
- if (childI && childJ) {
986
- node.children.set(String(absI), childJ);
987
- node.children.set(String(absJ), childI);
988
- }
989
- }
990
- }
991
- const siblingSelections = findSiblingSelections(node);
992
- const addedKeys = newMiddle.filter((k) => k !== null && !oldMiddleSet.has(k));
993
- for (const key of addedKeys) {
994
- const idx = start + newMiddle.indexOf(key);
995
- shiftChildrenRight(node, idx);
996
- const entity = storage[key];
997
- const insertNode = {
998
- depKey: "",
999
- children: /* @__PURE__ */ new Map(),
1000
- ...siblingSelections && { selections: siblingSelections }
1001
- };
1002
- if (entity) {
1003
- const { data } = partialDenormalize(insertNode, entity, [...entry.path, idx], rebuiltDepKeys, storage, subscriptions, entry.subscription);
1004
- node.children.set(String(idx), insertNode);
1005
- patches.push({
1006
- type: "splice",
1007
- path: entry.path,
1008
- index: idx,
1009
- deleteCount: 0,
1010
- items: [data]
1011
- });
1012
- } else {
1013
- node.children.set(String(idx), insertNode);
1014
- patches.push({
1015
- type: "splice",
1016
- path: entry.path,
1017
- index: idx,
1018
- deleteCount: 0,
1019
- items: [null]
1020
- });
1021
- }
1108
+ return restorations;
1109
+ }
1110
+ #findClosestEarlier(depKey, beforeIdx) {
1111
+ for (let i = beforeIdx - 1; i >= 0; i--) {
1112
+ const entry = this.#stack[i];
1113
+ if (entry.changes.has(depKey)) return entry.changes.get(depKey).new;
1022
1114
  }
1023
- rebuildArrayIndices(node, entry, subscriptions);
1024
- return patches;
1025
1115
  }
1026
- return patches;
1027
1116
  };
1028
- const compactChildren = (node) => {
1029
- const sorted = [...node.children.entries()].toSorted(([a], [b]) => Number(a) - Number(b));
1030
- node.children.clear();
1031
- for (const [i, element] of sorted.entries()) node.children.set(String(i), element[1]);
1117
+
1118
+ //#endregion
1119
+ //#region src/cache/patch.ts
1120
+ const copyNode = (node) => Array.isArray(node) ? [...node] : { ...node };
1121
+ const shallowCopyPath = (root, path) => {
1122
+ if (path.length === 0) return root;
1123
+ let result = copyNode(root);
1124
+ const top = result;
1125
+ for (let i = 0; i < path.length - 1; i++) {
1126
+ const key = path[i];
1127
+ result[key] = copyNode(result[key]);
1128
+ result = result[key];
1129
+ }
1130
+ return top;
1032
1131
  };
1033
- const findSiblingSelections = (node) => {
1034
- for (const child of node.children.values()) if (child.selections) return child.selections;
1035
- return node.selections;
1132
+ /**
1133
+ * Sets a value at a nested path within an object.
1134
+ * @param obj - The object to modify.
1135
+ * @param path - The path to the target location.
1136
+ * @param value - The value to set.
1137
+ */
1138
+ const setPath = (obj, path, value) => {
1139
+ let current = obj;
1140
+ for (let i = 0; i < path.length - 1; i++) current = current[path[i]];
1141
+ current[path.at(-1)] = value;
1036
1142
  };
1037
- const shiftChildrenRight = (node, fromIndex) => {
1038
- const entries = [...node.children.entries()].toSorted(([a], [b]) => Number(a) - Number(b));
1039
- node.children.clear();
1040
- for (const [key, child] of entries) {
1041
- const idx = Number(key);
1042
- if (idx >= fromIndex) node.children.set(String(idx + 1), child);
1043
- else node.children.set(key, child);
1143
+ /**
1144
+ * Gets a value at a nested path within an object.
1145
+ * @param obj - The object to read from.
1146
+ * @param path - The path to the target location.
1147
+ * @returns The value at the path, or the object itself if path is empty.
1148
+ */
1149
+ const getPath = (obj, path) => {
1150
+ let current = obj;
1151
+ for (const segment of path) {
1152
+ if (current === void 0 || current === null) return void 0;
1153
+ current = current[segment];
1044
1154
  }
1155
+ return current;
1045
1156
  };
1046
1157
  /**
1047
- * @internal
1158
+ * Applies cache patches to data immutably, shallow-copying only along changed paths.
1048
1159
  */
1049
- const generatePatches = (changedKeys, subscriptions, storage) => {
1050
- const patchesBySubscription = /* @__PURE__ */ new Map();
1051
- const rebuiltDepKeys = /* @__PURE__ */ new Set();
1052
- const { structural, scalar } = classifyChanges(changedKeys);
1053
- for (const { depKey, oldValue, newValue } of structural) {
1054
- const entries = subscriptions.get(depKey);
1055
- if (!entries) continue;
1056
- for (const entry of entries) {
1057
- const node = findEntryTreeNode(entry.subscription.entryTree, entry.path);
1058
- if (!node) continue;
1059
- const patches = processStructuralChange(entry, node, oldValue, newValue, rebuiltDepKeys, storage, subscriptions);
1060
- if (patches.length > 0) {
1061
- const existing = patchesBySubscription.get(entry.subscription) ?? [];
1062
- existing.push(...patches);
1063
- patchesBySubscription.set(entry.subscription, existing);
1064
- }
1160
+ const applyPatchesImmutable = (data, patches) => {
1161
+ if (patches.length === 0) return data;
1162
+ let result = data;
1163
+ for (const patch of patches) if (patch.type === "set") {
1164
+ if (patch.path.length === 0) {
1165
+ result = patch.value;
1166
+ continue;
1065
1167
  }
1168
+ result = shallowCopyPath(result, patch.path);
1169
+ let target = result;
1170
+ for (let i = 0; i < patch.path.length - 1; i++) target = target[patch.path[i]];
1171
+ target[patch.path.at(-1)] = patch.value;
1172
+ } else if (patch.type === "splice") {
1173
+ result = shallowCopyPath(result, patch.path);
1174
+ let target = result;
1175
+ for (const segment of patch.path) target = target[segment];
1176
+ const arr = [...target];
1177
+ arr.splice(patch.index, patch.deleteCount, ...patch.items);
1178
+ let parent = result;
1179
+ for (let i = 0; i < patch.path.length - 1; i++) parent = parent[patch.path[i]];
1180
+ parent[patch.path.at(-1)] = arr;
1181
+ } else if (patch.type === "swap") {
1182
+ result = shallowCopyPath(result, patch.path);
1183
+ let target = result;
1184
+ for (const segment of patch.path) target = target[segment];
1185
+ const arr = [...target];
1186
+ [arr[patch.i], arr[patch.j]] = [arr[patch.j], arr[patch.i]];
1187
+ let parent = result;
1188
+ for (let i = 0; i < patch.path.length - 1; i++) parent = parent[patch.path[i]];
1189
+ parent[patch.path.at(-1)] = arr;
1066
1190
  }
1067
- for (const { depKey, newValue } of scalar) {
1068
- if (rebuiltDepKeys.has(depKey)) continue;
1069
- const entries = subscriptions.get(depKey);
1070
- if (!entries) continue;
1071
- for (const entry of entries) {
1072
- const existing = patchesBySubscription.get(entry.subscription) ?? [];
1073
- existing.push({
1074
- type: "set",
1075
- path: entry.path,
1076
- value: newValue
1077
- });
1078
- patchesBySubscription.set(entry.subscription, existing);
1079
- }
1191
+ return result;
1192
+ };
1193
+ /**
1194
+ * Applies cache patches to a mutable target object in place.
1195
+ * @param target - The mutable object to apply patches to.
1196
+ * @param patches - The patches to apply.
1197
+ * @returns The new root value if a root-level set patch was applied, otherwise undefined.
1198
+ */
1199
+ const applyPatchesMutable = (target, patches) => {
1200
+ let root;
1201
+ for (const patch of patches) if (patch.type === "set") if (patch.path.length === 0) root = patch.value;
1202
+ else setPath(target, patch.path, patch.value);
1203
+ else if (patch.type === "splice") getPath(target, patch.path).splice(patch.index, patch.deleteCount, ...patch.items);
1204
+ else if (patch.type === "swap") {
1205
+ const arr = getPath(target, patch.path);
1206
+ [arr[patch.i], arr[patch.j]] = [arr[patch.j], arr[patch.i]];
1080
1207
  }
1081
- return patchesBySubscription;
1208
+ return root;
1082
1209
  };
1083
1210
 
1084
1211
  //#endregion
@@ -1090,141 +1217,39 @@ const generatePatches = (changedKeys, subscriptions, storage) => {
1090
1217
  var Cache = class {
1091
1218
  #schemaMeta;
1092
1219
  #storage = { [RootFieldKey]: {} };
1220
+ #registry = new CursorRegistry();
1093
1221
  #subscriptions = /* @__PURE__ */ new Map();
1222
+ #stalled = /* @__PURE__ */ new Map();
1223
+ #optimistic = new OptimisticStack();
1224
+ #nextId = 1;
1094
1225
  #stale = /* @__PURE__ */ new Set();
1095
- #optimisticKeys = [];
1096
- #optimisticLayers = /* @__PURE__ */ new Map();
1097
- #storageView = null;
1098
1226
  constructor(schemaMetadata) {
1099
1227
  this.#schemaMeta = schemaMetadata;
1100
1228
  }
1101
- #getStorageView() {
1102
- if (this.#optimisticKeys.length === 0) return this.#storage;
1103
- if (this.#storageView) return this.#storageView;
1104
- const merged = { ...this.#storage };
1105
- for (const storageKey of Object.keys(this.#storage)) merged[storageKey] = { ...this.#storage[storageKey] };
1106
- for (const key of this.#optimisticKeys) {
1107
- const layer = this.#optimisticLayers.get(key);
1108
- if (!layer) continue;
1109
- for (const storageKey of Object.keys(layer.storage)) merged[storageKey] = merged[storageKey] ? {
1110
- ...merged[storageKey],
1111
- ...layer.storage[storageKey]
1112
- } : { ...layer.storage[storageKey] };
1113
- }
1114
- this.#storageView = merged;
1115
- return merged;
1116
- }
1117
- /**
1118
- * Writes an optimistic response to a separate cache layer.
1119
- * The optimistic data is immediately visible in reads but does not affect the base storage.
1120
- * @internal
1121
- * @param key - Unique key identifying this optimistic mutation (typically the operation key).
1122
- * @param artifact - GraphQL document artifact.
1123
- * @param variables - Operation variables.
1124
- * @param data - The optimistic response data.
1125
- */
1126
- writeOptimistic(key, artifact, variables, data) {
1127
- const layerStorage = { [RootFieldKey]: {} };
1128
- const layerDependencies = /* @__PURE__ */ new Set();
1129
- normalize(this.#schemaMeta, artifact.selections, layerStorage, data, variables, (storageKey, fieldKey) => {
1130
- layerDependencies.add(makeDependencyKey(storageKey, fieldKey));
1131
- });
1132
- const oldValues = /* @__PURE__ */ new Map();
1133
- const currentView = this.#getStorageView();
1134
- for (const depKey of layerDependencies) {
1135
- const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
1136
- oldValues.set(depKey, currentView[sk]?.[fk]);
1137
- }
1138
- this.#optimisticKeys.push(key);
1139
- this.#optimisticLayers.set(key, {
1140
- storage: layerStorage,
1141
- dependencies: layerDependencies
1142
- });
1143
- this.#storageView = null;
1144
- const newView = this.#getStorageView();
1145
- const changedKeys = /* @__PURE__ */ new Map();
1146
- for (const depKey of layerDependencies) {
1147
- const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
1148
- const newVal = newView[sk]?.[fk];
1149
- const oldVal = oldValues.get(depKey);
1150
- if (oldVal !== newVal) changedKeys.set(depKey, {
1151
- oldValue: oldVal,
1152
- newValue: newVal
1153
- });
1154
- }
1155
- const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, newView);
1156
- for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
1157
- }
1158
- /**
1159
- * Removes an optimistic layer and notifies affected subscribers.
1160
- * @internal
1161
- * @param key - The key of the optimistic layer to remove.
1162
- */
1163
- removeOptimistic(key) {
1164
- const layer = this.#optimisticLayers.get(key);
1165
- if (!layer) return;
1166
- const currentView = this.#getStorageView();
1167
- const oldValues = /* @__PURE__ */ new Map();
1168
- for (const depKey of layer.dependencies) {
1169
- const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
1170
- oldValues.set(depKey, currentView[sk]?.[fk]);
1171
- }
1172
- this.#optimisticLayers.delete(key);
1173
- this.#optimisticKeys = this.#optimisticKeys.filter((k) => k !== key);
1174
- this.#storageView = null;
1175
- const newView = this.#getStorageView();
1176
- const changedKeys = /* @__PURE__ */ new Map();
1177
- for (const depKey of layer.dependencies) {
1178
- const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
1179
- const newVal = newView[sk]?.[fk];
1180
- const oldVal = oldValues.get(depKey);
1181
- if (oldVal !== newVal) changedKeys.set(depKey, {
1182
- oldValue: oldVal,
1183
- newValue: newVal
1184
- });
1185
- }
1186
- const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, newView);
1187
- for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
1188
- }
1189
1229
  /**
1190
1230
  * Writes a query result to the cache, normalizing entities.
1191
- * In addition to field-level stale clearing, this also clears entity-level stale entries
1192
- * (e.g., `"User:1"`) when any field of that entity is written, because {@link invalidate}
1193
- * supports entity-level invalidation without specifying a field.
1194
1231
  * @param artifact - GraphQL document artifact.
1195
1232
  * @param variables - Query variables.
1196
1233
  * @param data - Query result data.
1197
1234
  */
1198
1235
  writeQuery(artifact, variables, data) {
1199
- const changedKeys = /* @__PURE__ */ new Map();
1236
+ const changes = [];
1200
1237
  const staleClearedKeys = /* @__PURE__ */ new Set();
1201
1238
  const entityStaleCleared = /* @__PURE__ */ new Set();
1202
1239
  normalize(this.#schemaMeta, artifact.selections, this.#storage, data, variables, (storageKey, fieldKey, oldValue, newValue) => {
1203
1240
  const depKey = makeDependencyKey(storageKey, fieldKey);
1204
1241
  if (this.#stale.delete(depKey)) staleClearedKeys.add(depKey);
1205
1242
  if (!entityStaleCleared.has(storageKey) && this.#stale.delete(storageKey)) entityStaleCleared.add(storageKey);
1206
- if (oldValue !== newValue) changedKeys.set(depKey, {
1243
+ if (!isEqual(oldValue, newValue)) changes.push({
1244
+ depKey,
1245
+ storageKey,
1246
+ fieldKey,
1207
1247
  oldValue,
1208
1248
  newValue
1209
1249
  });
1210
1250
  });
1211
- const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, this.#storage);
1212
- for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
1213
- const staleOnlySubscriptions = /* @__PURE__ */ new Set();
1214
- for (const depKey of staleClearedKeys) {
1215
- if (changedKeys.has(depKey)) continue;
1216
- const entries = this.#subscriptions.get(depKey);
1217
- if (entries) {
1218
- for (const entry of entries) if (!patchesBySubscription.has(entry.subscription)) staleOnlySubscriptions.add(entry.subscription);
1219
- }
1220
- }
1221
- for (const entityKey of entityStaleCleared) {
1222
- const prefix = `${entityKey}.`;
1223
- for (const [depKey, entries] of this.#subscriptions) if (depKey.startsWith(prefix)) {
1224
- for (const entry of entries) if (!patchesBySubscription.has(entry.subscription)) staleOnlySubscriptions.add(entry.subscription);
1225
- }
1226
- }
1227
- for (const subscription of staleOnlySubscriptions) subscription.listener(null);
1251
+ this.#notifySubscribers(changes);
1252
+ this.#notifyStaleCleared(staleClearedKeys, entityStaleCleared, changes);
1228
1253
  }
1229
1254
  /**
1230
1255
  * Reads a query result from the cache, denormalizing entities if available.
@@ -1234,8 +1259,7 @@ var Cache = class {
1234
1259
  */
1235
1260
  readQuery(artifact, variables) {
1236
1261
  let stale = false;
1237
- const storage = this.#getStorageView();
1238
- const { data, partial } = denormalize(artifact.selections, storage, storage[RootFieldKey], variables, (storageKey, fieldKey) => {
1262
+ const { data, partial } = denormalize(artifact.selections, this.#storage, this.#storage[RootFieldKey], variables, (storageKey, fieldKey) => {
1239
1263
  if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
1240
1264
  });
1241
1265
  if (partial) return {
@@ -1252,49 +1276,46 @@ var Cache = class {
1252
1276
  * @param artifact - GraphQL document artifact.
1253
1277
  * @param variables - Query variables.
1254
1278
  * @param listener - Callback function to invoke on cache changes.
1255
- * @returns Object containing initial data, stale status, unsubscribe function, and subscription.
1279
+ * @returns Object containing initial data, stale status, and unsubscribe function.
1256
1280
  */
1257
1281
  subscribeQuery(artifact, variables, listener) {
1282
+ const id = this.#nextId++;
1283
+ const vars = variables;
1258
1284
  let stale = false;
1259
- const tuples = [];
1260
- const storageView = this.#getStorageView();
1261
- const { data, partial } = denormalize(artifact.selections, storageView, storageView[RootFieldKey], variables, (storageKey, fieldKey, path, selections) => {
1262
- tuples.push({
1263
- storageKey,
1264
- fieldKey,
1265
- path,
1266
- selections
1267
- });
1268
- if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
1269
- }, { trackFragmentDeps: false });
1270
- const entryTree = buildEntryTree(tuples);
1285
+ const traceResult = traceSelections(artifact.selections, this.#storage, this.#storage[RootFieldKey], vars, RootFieldKey, [], id);
1286
+ for (const { depKey } of traceResult.cursors) {
1287
+ const { storageKey, fieldKey } = parseDependencyKey(depKey);
1288
+ if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) {
1289
+ stale = true;
1290
+ break;
1291
+ }
1292
+ }
1271
1293
  const subscription = {
1294
+ id,
1295
+ kind: "query",
1296
+ artifact,
1297
+ variables: vars,
1272
1298
  listener,
1273
- selections: artifact.selections,
1274
- variables,
1275
- entryTree
1299
+ data: traceResult.complete ? traceResult.data : null,
1300
+ stale,
1301
+ cursors: new Set(traceResult.cursors.map((c) => c.entry))
1276
1302
  };
1277
- for (const tuple of tuples) {
1278
- const depKey = makeDependencyKey(tuple.storageKey, tuple.fieldKey);
1279
- const entry = {
1280
- path: tuple.path,
1281
- subscription
1282
- };
1283
- let entrySet = this.#subscriptions.get(depKey);
1284
- if (!entrySet) {
1285
- entrySet = /* @__PURE__ */ new Set();
1286
- this.#subscriptions.set(depKey, entrySet);
1287
- }
1288
- entrySet.add(entry);
1289
- }
1303
+ this.#subscriptions.set(id, subscription);
1304
+ for (const { depKey, entry } of traceResult.cursors) this.#registry.add(depKey, entry);
1305
+ if (!traceResult.complete) this.#stalled.set(id, {
1306
+ subscription,
1307
+ missingDeps: traceResult.missingDeps
1308
+ });
1290
1309
  const unsubscribe = () => {
1291
- this.#removeSubscriptionFromTree(entryTree, subscription);
1310
+ this.#registry.removeAll(subscription.cursors);
1311
+ this.#subscriptions.delete(id);
1312
+ this.#stalled.delete(id);
1292
1313
  };
1293
1314
  return {
1294
- data: partial ? null : data,
1315
+ data: subscription.data,
1295
1316
  stale,
1296
- unsubscribe,
1297
- subscription
1317
+ subId: id,
1318
+ unsubscribe
1298
1319
  };
1299
1320
  }
1300
1321
  /**
@@ -1306,14 +1327,13 @@ var Cache = class {
1306
1327
  readFragment(artifact, fragmentRef) {
1307
1328
  const storageKey = fragmentRef[FragmentRefKey];
1308
1329
  const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
1309
- const storageView = this.#getStorageView();
1310
1330
  let stale = false;
1311
- const value = storageView[storageKey];
1331
+ const value = this.#storage[storageKey];
1312
1332
  if (!value) return {
1313
1333
  data: null,
1314
1334
  stale: false
1315
1335
  };
1316
- const { data, partial } = denormalize(artifact.selections, storageView, storageKey === RootFieldKey ? value : { [EntityLinkKey]: storageKey }, fragmentVars, (sk, fieldKey) => {
1336
+ const { data, partial } = denormalize(artifact.selections, this.#storage, storageKey === RootFieldKey ? value : { [EntityLinkKey]: storageKey }, fragmentVars, (sk, fieldKey) => {
1317
1337
  if (this.#stale.has(sk) || this.#stale.has(makeDependencyKey(sk, fieldKey))) stale = true;
1318
1338
  });
1319
1339
  if (partial) return {
@@ -1330,81 +1350,86 @@ var Cache = class {
1330
1350
  * @param artifact - GraphQL fragment artifact.
1331
1351
  * @param fragmentRef - Fragment reference containing entity key.
1332
1352
  * @param listener - Callback function to invoke on cache changes.
1333
- * @returns Object containing initial data, stale status, unsubscribe function, and subscription.
1353
+ * @returns Object containing initial data, stale status, and unsubscribe function.
1334
1354
  */
1335
1355
  subscribeFragment(artifact, fragmentRef, listener) {
1336
1356
  const storageKey = fragmentRef[FragmentRefKey];
1337
1357
  const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
1338
- const storageView = this.#getStorageView();
1339
- const value = storageKey === RootFieldKey ? storageView[RootFieldKey] : storageView[storageKey];
1358
+ const id = this.#nextId++;
1359
+ const value = storageKey === RootFieldKey ? this.#storage[RootFieldKey] : this.#storage[storageKey];
1340
1360
  if (!value) {
1341
- const entryTree = buildEntryTree([]);
1361
+ const subscription = {
1362
+ id,
1363
+ kind: "fragment",
1364
+ artifact,
1365
+ variables: fragmentVars,
1366
+ listener,
1367
+ entityKey: storageKey,
1368
+ data: null,
1369
+ stale: false,
1370
+ cursors: /* @__PURE__ */ new Set()
1371
+ };
1372
+ this.#subscriptions.set(id, subscription);
1342
1373
  return {
1343
1374
  data: null,
1344
1375
  stale: false,
1345
- unsubscribe: () => {},
1346
- subscription: {
1347
- listener,
1348
- selections: artifact.selections,
1349
- variables: fragmentVars,
1350
- entryTree
1351
- }
1376
+ subId: id,
1377
+ unsubscribe: () => this.#subscriptions.delete(id)
1352
1378
  };
1353
1379
  }
1354
1380
  let stale = false;
1355
- const tuples = [];
1356
- const denormalizeValue = storageKey === RootFieldKey ? value : { [EntityLinkKey]: storageKey };
1357
- const { data, partial } = denormalize(artifact.selections, storageView, denormalizeValue, fragmentVars, (sk, fieldKey, path, selections) => {
1358
- tuples.push({
1359
- storageKey: sk,
1360
- fieldKey,
1361
- path,
1362
- selections
1363
- });
1364
- if (this.#stale.has(sk) || this.#stale.has(makeDependencyKey(sk, fieldKey))) stale = true;
1365
- }, { trackFragmentDeps: false });
1366
- if (partial) {
1367
- const entryTree = buildEntryTree([]);
1381
+ const denormalizeValue = storageKey === RootFieldKey ? value : value;
1382
+ const traceResult = traceSelections(artifact.selections, this.#storage, denormalizeValue, fragmentVars, storageKey, [], id);
1383
+ for (const { depKey } of traceResult.cursors) {
1384
+ const { storageKey: sk, fieldKey } = parseDependencyKey(depKey);
1385
+ if (this.#stale.has(sk) || this.#stale.has(makeDependencyKey(sk, fieldKey))) {
1386
+ stale = true;
1387
+ break;
1388
+ }
1389
+ }
1390
+ if (!traceResult.complete) {
1391
+ const subscription = {
1392
+ id,
1393
+ kind: "fragment",
1394
+ artifact,
1395
+ variables: fragmentVars,
1396
+ listener,
1397
+ entityKey: storageKey,
1398
+ data: null,
1399
+ stale: false,
1400
+ cursors: /* @__PURE__ */ new Set()
1401
+ };
1402
+ this.#subscriptions.set(id, subscription);
1368
1403
  return {
1369
1404
  data: null,
1370
1405
  stale: false,
1371
- unsubscribe: () => {},
1372
- subscription: {
1373
- listener,
1374
- selections: artifact.selections,
1375
- variables: fragmentVars,
1376
- entryTree
1377
- }
1406
+ subId: id,
1407
+ unsubscribe: () => this.#subscriptions.delete(id)
1378
1408
  };
1379
1409
  }
1380
- const entryTree = buildEntryTree(tuples, storageKey === RootFieldKey ? void 0 : storageKey);
1381
1410
  const subscription = {
1382
- listener,
1383
- selections: artifact.selections,
1411
+ id,
1412
+ kind: "fragment",
1413
+ artifact,
1384
1414
  variables: fragmentVars,
1385
- entryTree
1415
+ listener,
1416
+ entityKey: storageKey,
1417
+ data: traceResult.data,
1418
+ stale,
1419
+ cursors: new Set(traceResult.cursors.map((c) => c.entry))
1386
1420
  };
1387
- for (const tuple of tuples) {
1388
- const depKey = makeDependencyKey(tuple.storageKey, tuple.fieldKey);
1389
- const entry = {
1390
- path: tuple.path,
1391
- subscription
1392
- };
1393
- let entrySet = this.#subscriptions.get(depKey);
1394
- if (!entrySet) {
1395
- entrySet = /* @__PURE__ */ new Set();
1396
- this.#subscriptions.set(depKey, entrySet);
1397
- }
1398
- entrySet.add(entry);
1399
- }
1421
+ this.#subscriptions.set(id, subscription);
1422
+ for (const { depKey, entry } of traceResult.cursors) this.#registry.add(depKey, entry);
1400
1423
  const unsubscribe = () => {
1401
- this.#removeSubscriptionFromTree(entryTree, subscription);
1424
+ this.#registry.removeAll(subscription.cursors);
1425
+ this.#subscriptions.delete(id);
1426
+ this.#stalled.delete(id);
1402
1427
  };
1403
1428
  return {
1404
- data: partial ? null : data,
1429
+ data: traceResult.data,
1405
1430
  stale,
1406
- unsubscribe,
1407
- subscription
1431
+ subId: id,
1432
+ unsubscribe
1408
1433
  };
1409
1434
  }
1410
1435
  readFragments(artifact, fragmentRefs) {
@@ -1435,6 +1460,45 @@ var Cache = class {
1435
1460
  };
1436
1461
  }
1437
1462
  /**
1463
+ * Writes an optimistic response to the cache.
1464
+ * @internal
1465
+ */
1466
+ writeOptimistic(key, artifact, variables, data) {
1467
+ const changes = [];
1468
+ const optimisticChanges = /* @__PURE__ */ new Map();
1469
+ normalize(this.#schemaMeta, artifact.selections, this.#storage, data, variables, (storageKey, fieldKey, oldValue, newValue) => {
1470
+ const depKey = makeDependencyKey(storageKey, fieldKey);
1471
+ if (!isEqual(oldValue, newValue)) {
1472
+ changes.push({
1473
+ depKey,
1474
+ storageKey,
1475
+ fieldKey,
1476
+ oldValue,
1477
+ newValue
1478
+ });
1479
+ optimisticChanges.set(depKey, {
1480
+ old: oldValue,
1481
+ new: newValue
1482
+ });
1483
+ }
1484
+ });
1485
+ this.#optimistic.push(key, optimisticChanges);
1486
+ this.#notifySubscribers(changes);
1487
+ }
1488
+ /**
1489
+ * Removes an optimistic layer and notifies affected subscribers.
1490
+ * @internal
1491
+ */
1492
+ removeOptimistic(key) {
1493
+ const restorations = this.#optimistic.rollback(key);
1494
+ for (const restoration of restorations) {
1495
+ const { storageKey, fieldKey, newValue } = restoration;
1496
+ const fields = this.#storage[storageKey];
1497
+ if (fields) fields[fieldKey] = newValue;
1498
+ }
1499
+ this.#notifySubscribers(restorations);
1500
+ }
1501
+ /**
1438
1502
  * Invalidates one or more cache entries and notifies affected subscribers.
1439
1503
  * @param targets - Cache entries to invalidate.
1440
1504
  */
@@ -1444,10 +1508,10 @@ var Cache = class {
1444
1508
  const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
1445
1509
  const depKey = makeDependencyKey(RootFieldKey, fieldKey);
1446
1510
  this.#stale.add(depKey);
1447
- this.#collectSubscriptions(RootFieldKey, fieldKey, affectedSubscriptions);
1511
+ this.#collectAffectedSubscriptions(RootFieldKey, fieldKey, affectedSubscriptions);
1448
1512
  } else {
1449
1513
  this.#stale.add(RootFieldKey);
1450
- this.#collectSubscriptions(RootFieldKey, void 0, affectedSubscriptions);
1514
+ this.#collectAffectedSubscriptions(RootFieldKey, void 0, affectedSubscriptions);
1451
1515
  }
1452
1516
  else {
1453
1517
  const keyFields = this.#schemaMeta.entities[target.__typename]?.keyFields;
@@ -1457,10 +1521,10 @@ var Cache = class {
1457
1521
  if ("$field" in target) {
1458
1522
  const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
1459
1523
  this.#stale.add(makeDependencyKey(entityKey, fieldKey));
1460
- this.#collectSubscriptions(entityKey, fieldKey, affectedSubscriptions);
1524
+ this.#collectAffectedSubscriptions(entityKey, fieldKey, affectedSubscriptions);
1461
1525
  } else {
1462
1526
  this.#stale.add(entityKey);
1463
- this.#collectSubscriptions(entityKey, void 0, affectedSubscriptions);
1527
+ this.#collectAffectedSubscriptions(entityKey, void 0, affectedSubscriptions);
1464
1528
  }
1465
1529
  } else {
1466
1530
  const prefix = `${target.__typename}:`;
@@ -1469,61 +1533,33 @@ var Cache = class {
1469
1533
  if ("$field" in target) {
1470
1534
  const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
1471
1535
  this.#stale.add(makeDependencyKey(entityKey, fieldKey));
1472
- this.#collectSubscriptions(entityKey, fieldKey, affectedSubscriptions);
1536
+ this.#collectAffectedSubscriptions(entityKey, fieldKey, affectedSubscriptions);
1473
1537
  } else {
1474
1538
  this.#stale.add(entityKey);
1475
- this.#collectSubscriptions(entityKey, void 0, affectedSubscriptions);
1539
+ this.#collectAffectedSubscriptions(entityKey, void 0, affectedSubscriptions);
1476
1540
  }
1477
1541
  }
1478
1542
  }
1479
1543
  }
1480
- for (const subscription of affectedSubscriptions) subscription.listener(null);
1544
+ const subsToNotify = [];
1545
+ for (const subId of affectedSubscriptions) {
1546
+ const sub = this.#subscriptions.get(subId);
1547
+ if (sub) {
1548
+ sub.stale = true;
1549
+ subsToNotify.push(sub);
1550
+ }
1551
+ }
1552
+ for (const sub of subsToNotify) if (sub.stale) sub.listener({ type: "stale" });
1481
1553
  }
1482
1554
  /**
1483
1555
  * Checks if a subscription has stale data.
1484
1556
  * @internal
1485
1557
  */
1486
- isStale(subscription) {
1487
- const check = (node) => {
1488
- if (node.depKey.includes("@")) {
1489
- const { storageKey } = parseDependencyKey(node.depKey);
1490
- if (this.#stale.has(storageKey) || this.#stale.has(node.depKey)) return true;
1491
- }
1492
- for (const child of node.children.values()) if (check(child)) return true;
1493
- return false;
1494
- };
1495
- return check(subscription.entryTree);
1496
- }
1497
- #hasKeyFields(target, keyFields) {
1498
- return keyFields.every((f) => f in target);
1499
- }
1500
- #collectSubscriptions(storageKey, fieldKey, out) {
1501
- if (fieldKey === void 0) {
1502
- const prefix = `${storageKey}.`;
1503
- for (const [depKey, entries] of this.#subscriptions) if (depKey.startsWith(prefix)) for (const entry of entries) out.add(entry.subscription);
1504
- } else {
1505
- const depKey = makeDependencyKey(storageKey, fieldKey);
1506
- const entries = this.#subscriptions.get(depKey);
1507
- if (entries) for (const entry of entries) out.add(entry.subscription);
1508
- }
1509
- }
1510
- #removeSubscriptionFromTree(node, subscription) {
1511
- const entries = this.#subscriptions.get(node.depKey);
1512
- if (entries) {
1513
- for (const entry of entries) if (entry.subscription === subscription) {
1514
- entries.delete(entry);
1515
- break;
1516
- }
1517
- if (entries.size === 0) this.#subscriptions.delete(node.depKey);
1518
- }
1519
- for (const child of node.children.values()) this.#removeSubscriptionFromTree(child, subscription);
1520
- }
1521
- #parseDepKey(depKey) {
1522
- return parseDependencyKey(depKey);
1558
+ isStale(subId) {
1559
+ return this.#subscriptions.get(subId)?.stale ?? false;
1523
1560
  }
1524
1561
  /**
1525
1562
  * Extracts a serializable snapshot of the cache storage.
1526
- * Optimistic layers are excluded because they represent transient in-flight state.
1527
1563
  */
1528
1564
  extract() {
1529
1565
  return { storage: structuredClone(this.#storage) };
@@ -1537,18 +1573,100 @@ var Cache = class {
1537
1573
  ...this.#storage[key],
1538
1574
  ...fields
1539
1575
  };
1540
- this.#storageView = null;
1541
1576
  }
1542
1577
  /**
1543
1578
  * Clears all cache data.
1544
1579
  */
1545
1580
  clear() {
1546
1581
  this.#storage = { [RootFieldKey]: {} };
1582
+ this.#registry.clear();
1547
1583
  this.#subscriptions.clear();
1584
+ this.#stalled.clear();
1548
1585
  this.#stale.clear();
1549
- this.#optimisticKeys = [];
1550
- this.#optimisticLayers.clear();
1551
- this.#storageView = null;
1586
+ }
1587
+ #notifySubscribers(changes) {
1588
+ if (changes.length === 0) return;
1589
+ const unstalledPatches = this.#checkStalled(changes);
1590
+ const { scalar, structural } = classifyChanges(changes);
1591
+ const scalarPatches = processScalarChanges(scalar, this.#registry, this.#subscriptions);
1592
+ const structuralPatches = processStructuralChanges(structural, this.#registry, this.#subscriptions, this.#storage, this.#stalled);
1593
+ const allPatches = /* @__PURE__ */ new Map();
1594
+ for (const [subId, patches] of unstalledPatches) allPatches.set(subId, patches);
1595
+ for (const [subId, patches] of scalarPatches) {
1596
+ if (unstalledPatches.has(subId)) continue;
1597
+ allPatches.set(subId, [...allPatches.get(subId) ?? [], ...patches]);
1598
+ }
1599
+ for (const [subId, patches] of structuralPatches) {
1600
+ if (unstalledPatches.has(subId)) continue;
1601
+ allPatches.set(subId, [...allPatches.get(subId) ?? [], ...patches]);
1602
+ }
1603
+ for (const [subId, patches] of allPatches) {
1604
+ const sub = this.#subscriptions.get(subId);
1605
+ if (sub && patches.length > 0) {
1606
+ if (!structuralPatches.has(subId) && !unstalledPatches.has(subId)) sub.data = applyPatchesImmutable(sub.data, patches);
1607
+ sub.listener({
1608
+ type: "patch",
1609
+ patches
1610
+ });
1611
+ }
1612
+ }
1613
+ }
1614
+ #checkStalled(changes) {
1615
+ const result = /* @__PURE__ */ new Map();
1616
+ const writtenDepKeys = new Set(changes.map((c) => c.depKey));
1617
+ for (const [subId, info] of this.#stalled) {
1618
+ if (![...info.missingDeps].some((dep) => writtenDepKeys.has(dep))) continue;
1619
+ const sub = info.subscription;
1620
+ const rootStorageKey = sub.entityKey ?? RootFieldKey;
1621
+ const rootValue = this.#storage[rootStorageKey];
1622
+ if (!rootValue) continue;
1623
+ const traceResult = traceSelections(sub.artifact.selections, this.#storage, rootValue, sub.variables, rootStorageKey, [], sub.id);
1624
+ if (traceResult.complete) {
1625
+ this.#registry.removeAll(sub.cursors);
1626
+ sub.cursors = new Set(traceResult.cursors.map((c) => c.entry));
1627
+ for (const { depKey, entry } of traceResult.cursors) this.#registry.add(depKey, entry);
1628
+ this.#stalled.delete(subId);
1629
+ const entityArrayChanges = buildEntityArrayContext(changes, traceResult.cursors);
1630
+ const patches = diffSnapshots(sub.data, traceResult.data, entityArrayChanges);
1631
+ if (patches.length > 0) {
1632
+ sub.data = traceResult.data;
1633
+ result.set(subId, patches);
1634
+ }
1635
+ } else info.missingDeps = traceResult.missingDeps;
1636
+ }
1637
+ return result;
1638
+ }
1639
+ #notifyStaleCleared(staleClearedKeys, entityStaleCleared, changes) {
1640
+ const changedDepKeys = new Set(changes.map((c) => c.depKey));
1641
+ const notifiedSubs = /* @__PURE__ */ new Set();
1642
+ for (const depKey of staleClearedKeys) {
1643
+ if (changedDepKeys.has(depKey)) continue;
1644
+ const entries = this.#registry.get(depKey);
1645
+ if (entries) for (const entry of entries) notifiedSubs.add(entry.subscriptionId);
1646
+ }
1647
+ for (const entityKey of entityStaleCleared) this.#registry.forEachByPrefix(`${entityKey}.`, (entry) => {
1648
+ notifiedSubs.add(entry.subscriptionId);
1649
+ });
1650
+ for (const subId of notifiedSubs) {
1651
+ const sub = this.#subscriptions.get(subId);
1652
+ if (sub?.stale) {
1653
+ sub.stale = false;
1654
+ sub.listener({ type: "stale" });
1655
+ }
1656
+ }
1657
+ }
1658
+ #hasKeyFields(target, keyFields) {
1659
+ return keyFields.every((f) => f in target);
1660
+ }
1661
+ #collectAffectedSubscriptions(storageKey, fieldKey, out) {
1662
+ if (fieldKey === void 0) this.#registry.forEachByPrefix(`${storageKey}.`, (entry) => {
1663
+ out.add(entry.subscriptionId);
1664
+ });
1665
+ else {
1666
+ const depKey = makeDependencyKey(storageKey, fieldKey);
1667
+ const entries = this.#registry.get(depKey);
1668
+ if (entries) for (const entry of entries) out.add(entry.subscriptionId);
1669
+ }
1552
1670
  }
1553
1671
  };
1554
1672
 
@@ -1581,7 +1699,6 @@ const cacheExchange = (options = {}) => {
1581
1699
  },
1582
1700
  io: (ops$) => {
1583
1701
  const subscriptionHasData = /* @__PURE__ */ new Map();
1584
- const resubscribe$ = makeSubject();
1585
1702
  const refetch$ = makeSubject();
1586
1703
  const fragment$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), mergeMap((op) => {
1587
1704
  const fragmentRef = op.metadata?.fragment?.ref;
@@ -1592,11 +1709,10 @@ const cacheExchange = (options = {}) => {
1592
1709
  if (isFragmentRefArray(fragmentRef)) {
1593
1710
  const results = makeSubject();
1594
1711
  const unsubscribes = [];
1595
- const fragmentSubscriptions = [];
1596
1712
  for (const [index, ref] of fragmentRef.entries()) {
1597
- const patchListener = (patches) => {
1598
- if (patches) {
1599
- const indexedPatches = patches.map((patch) => ({
1713
+ const listener = (notification) => {
1714
+ if (notification.type === "patch") {
1715
+ const indexedPatches = notification.patches.map((patch) => ({
1600
1716
  ...patch,
1601
1717
  path: [index, ...patch.path]
1602
1718
  }));
@@ -1606,21 +1722,17 @@ const cacheExchange = (options = {}) => {
1606
1722
  errors: []
1607
1723
  });
1608
1724
  } else {
1609
- const sub = fragmentSubscriptions[index];
1610
- if (sub && cache.isStale(sub)) {
1611
- const { data, stale } = cache.readFragments(op.artifact, fragmentRef);
1612
- if (data !== null) results.next({
1613
- operation: op,
1614
- data,
1615
- ...stale && { metadata: { cache: { stale: true } } },
1616
- errors: []
1617
- });
1618
- }
1725
+ const { data, stale } = cache.readFragments(op.artifact, fragmentRef);
1726
+ if (data !== null && stale) results.next({
1727
+ operation: op,
1728
+ data,
1729
+ metadata: { cache: { stale: true } },
1730
+ errors: []
1731
+ });
1619
1732
  }
1620
1733
  };
1621
- const { unsubscribe, subscription } = cache.subscribeFragment(op.artifact, ref, patchListener);
1734
+ const { unsubscribe } = cache.subscribeFragment(op.artifact, ref, listener);
1622
1735
  unsubscribes.push(unsubscribe);
1623
- fragmentSubscriptions.push(subscription);
1624
1736
  }
1625
1737
  const { data: initialData, stale: initialStale } = cache.readFragments(op.artifact, fragmentRef);
1626
1738
  const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => {
@@ -1640,31 +1752,25 @@ const cacheExchange = (options = {}) => {
1640
1752
  errors: []
1641
1753
  });
1642
1754
  const results = makeSubject();
1643
- let currentUnsubscribe = null;
1644
- let currentSubscription = null;
1645
- const patchListener = (patches) => {
1646
- if (patches) results.next({
1755
+ const listener = (notification) => {
1756
+ if (notification.type === "patch") results.next({
1647
1757
  operation: op,
1648
- metadata: { cache: { patches } },
1758
+ metadata: { cache: { patches: notification.patches } },
1649
1759
  errors: []
1650
1760
  });
1651
- else if (currentSubscription) {
1652
- if (cache.isStale(currentSubscription)) {
1653
- const { data: staleData } = cache.readFragment(op.artifact, fragmentRef);
1654
- if (staleData !== null) results.next({
1655
- operation: op,
1656
- data: staleData,
1657
- metadata: { cache: { stale: true } },
1658
- errors: []
1659
- });
1660
- }
1761
+ else {
1762
+ const { data: staleData, stale: isStale } = cache.readFragment(op.artifact, fragmentRef);
1763
+ if (staleData !== null && isStale) results.next({
1764
+ operation: op,
1765
+ data: staleData,
1766
+ metadata: { cache: { stale: true } },
1767
+ errors: []
1768
+ });
1661
1769
  }
1662
1770
  };
1663
- const { data, stale, unsubscribe, subscription } = cache.subscribeFragment(op.artifact, fragmentRef, patchListener);
1664
- currentUnsubscribe = unsubscribe;
1665
- currentSubscription = subscription;
1771
+ const { data, stale, unsubscribe } = cache.subscribeFragment(op.artifact, fragmentRef, listener);
1666
1772
  const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => {
1667
- if (currentUnsubscribe) currentUnsubscribe();
1773
+ unsubscribe();
1668
1774
  results.complete();
1669
1775
  }));
1670
1776
  return pipe(merge(data === null ? empty() : fromValue({
@@ -1680,50 +1786,34 @@ const cacheExchange = (options = {}) => {
1680
1786
  const query$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), share());
1681
1787
  return merge(fragment$, pipe(query$, mergeMap((op) => {
1682
1788
  const results = makeSubject();
1683
- let currentUnsubscribe = null;
1684
- let currentSubscription = null;
1685
- let initialized = false;
1686
- const doSubscribe = () => {
1687
- if (currentUnsubscribe) currentUnsubscribe();
1688
- const patchListener = (patches) => {
1689
- if (patches) {
1690
- if (!initialized) return;
1691
- results.next({
1789
+ const listener = (notification) => {
1790
+ if (notification.type === "patch") {
1791
+ if (!subscriptionHasData.get(op.key)) return;
1792
+ results.next({
1793
+ operation: op,
1794
+ metadata: { cache: { patches: notification.patches } },
1795
+ errors: []
1796
+ });
1797
+ } else {
1798
+ const { data: staleData, stale: isStale } = cache.readQuery(op.artifact, op.variables);
1799
+ if (isStale) {
1800
+ if (staleData !== null) results.next({
1692
1801
  operation: op,
1693
- metadata: { cache: { patches } },
1802
+ data: staleData,
1803
+ metadata: { cache: { stale: true } },
1694
1804
  errors: []
1695
1805
  });
1696
- } else if (currentSubscription) {
1697
- if (cache.isStale(currentSubscription)) {
1698
- const { data: staleData } = cache.readQuery(op.artifact, op.variables);
1699
- if (staleData !== null) results.next({
1700
- operation: op,
1701
- data: staleData,
1702
- metadata: { cache: { stale: true } },
1703
- errors: []
1704
- });
1705
- refetch$.next(op);
1706
- }
1806
+ refetch$.next(op);
1707
1807
  }
1708
- };
1709
- const result = cache.subscribeQuery(op.artifact, op.variables, patchListener);
1710
- currentUnsubscribe = result.unsubscribe;
1711
- currentSubscription = result.subscription;
1712
- return result;
1808
+ }
1713
1809
  };
1714
- const { data, stale } = doSubscribe();
1810
+ const { data, stale, unsubscribe } = cache.subscribeQuery(op.artifact, op.variables, listener);
1715
1811
  subscriptionHasData.set(op.key, data !== null);
1716
- if (data !== null) initialized = true;
1717
1812
  const teardown$ = pipe(ops$, filter((o) => o.variant === "teardown" && o.key === op.key), tap(() => {
1718
- if (currentUnsubscribe) currentUnsubscribe();
1813
+ unsubscribe();
1719
1814
  subscriptionHasData.delete(op.key);
1720
1815
  results.complete();
1721
1816
  }));
1722
- const resubStream$ = pipe(resubscribe$.source, filter((key) => key === op.key), mergeMap(() => {
1723
- doSubscribe();
1724
- initialized = true;
1725
- return empty();
1726
- }));
1727
1817
  const stream$ = pipe(merge(data === null ? fetchPolicy === "cache-only" ? fromValue({
1728
1818
  operation: op,
1729
1819
  data: null,
@@ -1733,7 +1823,7 @@ const cacheExchange = (options = {}) => {
1733
1823
  data,
1734
1824
  ...stale && { metadata: { cache: { stale: true } } },
1735
1825
  errors: []
1736
- }), results.source, resubStream$), takeUntil(teardown$));
1826
+ }), results.source), takeUntil(teardown$));
1737
1827
  if (stale) refetch$.next(op);
1738
1828
  return stream$;
1739
1829
  }), filter(() => fetchPolicy === "cache-only" || fetchPolicy === "cache-and-network" || fetchPolicy === "cache-first")), pipe(merge(nonCache$, pipe(query$, filter((op) => {
@@ -1753,7 +1843,6 @@ const cacheExchange = (options = {}) => {
1753
1843
  });
1754
1844
  }
1755
1845
  subscriptionHasData.set(result.operation.key, true);
1756
- resubscribe$.next(result.operation.key);
1757
1846
  const { data } = cache.readQuery(result.operation.artifact, result.operation.variables);
1758
1847
  if (data !== null) return fromValue({
1759
1848
  ...result,
@@ -1770,99 +1859,6 @@ const cacheExchange = (options = {}) => {
1770
1859
  };
1771
1860
  };
1772
1861
 
1773
- //#endregion
1774
- //#region src/cache/patch.ts
1775
- const copyNode = (node) => Array.isArray(node) ? [...node] : { ...node };
1776
- const shallowCopyPath = (root, path) => {
1777
- if (path.length === 0) return root;
1778
- let result = copyNode(root);
1779
- const top = result;
1780
- for (let i = 0; i < path.length - 1; i++) {
1781
- const key = path[i];
1782
- result[key] = copyNode(result[key]);
1783
- result = result[key];
1784
- }
1785
- return top;
1786
- };
1787
- /**
1788
- * Sets a value at a nested path within an object.
1789
- * @param obj - The object to modify.
1790
- * @param path - The path to the target location.
1791
- * @param value - The value to set.
1792
- */
1793
- const setPath = (obj, path, value) => {
1794
- let current = obj;
1795
- for (let i = 0; i < path.length - 1; i++) current = current[path[i]];
1796
- current[path.at(-1)] = value;
1797
- };
1798
- /**
1799
- * Gets a value at a nested path within an object.
1800
- * @param obj - The object to read from.
1801
- * @param path - The path to the target location.
1802
- * @returns The value at the path, or the object itself if path is empty.
1803
- */
1804
- const getPath = (obj, path) => {
1805
- let current = obj;
1806
- for (const segment of path) {
1807
- if (current === void 0 || current === null) return void 0;
1808
- current = current[segment];
1809
- }
1810
- return current;
1811
- };
1812
- /**
1813
- * Applies cache patches to data immutably, shallow-copying only along changed paths.
1814
- */
1815
- const applyPatchesImmutable = (data, patches) => {
1816
- if (patches.length === 0) return data;
1817
- let result = data;
1818
- for (const patch of patches) if (patch.type === "set") {
1819
- if (patch.path.length === 0) {
1820
- result = patch.value;
1821
- continue;
1822
- }
1823
- result = shallowCopyPath(result, patch.path);
1824
- let target = result;
1825
- for (let i = 0; i < patch.path.length - 1; i++) target = target[patch.path[i]];
1826
- target[patch.path.at(-1)] = patch.value;
1827
- } else if (patch.type === "splice") {
1828
- result = shallowCopyPath(result, patch.path);
1829
- let target = result;
1830
- for (const segment of patch.path) target = target[segment];
1831
- const arr = [...target];
1832
- arr.splice(patch.index, patch.deleteCount, ...patch.items);
1833
- let parent = result;
1834
- for (let i = 0; i < patch.path.length - 1; i++) parent = parent[patch.path[i]];
1835
- parent[patch.path.at(-1)] = arr;
1836
- } else if (patch.type === "swap") {
1837
- result = shallowCopyPath(result, patch.path);
1838
- let target = result;
1839
- for (const segment of patch.path) target = target[segment];
1840
- const arr = [...target];
1841
- [arr[patch.i], arr[patch.j]] = [arr[patch.j], arr[patch.i]];
1842
- let parent = result;
1843
- for (let i = 0; i < patch.path.length - 1; i++) parent = parent[patch.path[i]];
1844
- parent[patch.path.at(-1)] = arr;
1845
- }
1846
- return result;
1847
- };
1848
- /**
1849
- * Applies cache patches to a mutable target object in place.
1850
- * @param target - The mutable object to apply patches to.
1851
- * @param patches - The patches to apply.
1852
- * @returns The new root value if a root-level set patch was applied, otherwise undefined.
1853
- */
1854
- const applyPatchesMutable = (target, patches) => {
1855
- let root;
1856
- for (const patch of patches) if (patch.type === "set") if (patch.path.length === 0) root = patch.value;
1857
- else setPath(target, patch.path, patch.value);
1858
- else if (patch.type === "splice") getPath(target, patch.path).splice(patch.index, patch.deleteCount, ...patch.items);
1859
- else if (patch.type === "swap") {
1860
- const arr = getPath(target, patch.path);
1861
- [arr[patch.i], arr[patch.j]] = [arr[patch.j], arr[patch.i]];
1862
- }
1863
- return root;
1864
- };
1865
-
1866
1862
  //#endregion
1867
1863
  //#region src/exchanges/retry.ts
1868
1864
  const defaultShouldRetry = (error) => isExchangeError(error, "http") && error.extensions?.statusCode !== void 0 && error.extensions.statusCode >= 500;
@@ -2053,7 +2049,7 @@ const subscriptionExchange = (options) => {
2053
2049
  return pipe(make((observer) => {
2054
2050
  let unsubscribe;
2055
2051
  let completed = false;
2056
- Promise.resolve().then(() => {
2052
+ const doSubscribe = () => {
2057
2053
  if (completed) return;
2058
2054
  unsubscribe = client.subscribe({
2059
2055
  operationName: op.artifact.name,
@@ -2062,16 +2058,28 @@ const subscriptionExchange = (options) => {
2062
2058
  }, {
2063
2059
  next: (result) => {
2064
2060
  const response = result;
2065
- observer.next({
2066
- operation: op,
2067
- data: response.data,
2068
- errors: response.errors?.map((err) => new GraphQLError(err.message, {
2069
- path: err.path,
2070
- locations: err.locations,
2071
- extensions: err.extensions
2072
- })),
2073
- extensions: response.extensions
2074
- });
2061
+ try {
2062
+ observer.next({
2063
+ operation: op,
2064
+ data: response.data,
2065
+ errors: response.errors?.map((err) => new GraphQLError(err.message, {
2066
+ path: err.path,
2067
+ locations: err.locations,
2068
+ extensions: err.extensions
2069
+ })),
2070
+ extensions: response.extensions
2071
+ });
2072
+ } catch (error) {
2073
+ try {
2074
+ observer.next({
2075
+ operation: op,
2076
+ errors: [new ExchangeError(error instanceof Error ? error.message : String(error), {
2077
+ exchangeName: "subscription",
2078
+ cause: error
2079
+ })]
2080
+ });
2081
+ } catch {}
2082
+ }
2075
2083
  },
2076
2084
  error: (error) => {
2077
2085
  observer.next({
@@ -2081,11 +2089,13 @@ const subscriptionExchange = (options) => {
2081
2089
  cause: error
2082
2090
  })]
2083
2091
  });
2084
- observer.complete();
2092
+ unsubscribe = void 0;
2093
+ Promise.resolve().then(doSubscribe);
2085
2094
  },
2086
2095
  complete: observer.complete
2087
2096
  });
2088
- });
2097
+ };
2098
+ Promise.resolve().then(doSubscribe);
2089
2099
  return () => {
2090
2100
  completed = true;
2091
2101
  unsubscribe?.();