@mearie/core 0.6.1 → 0.6.3

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