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