@mearie/core 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.cjs +759 -749
  2. package/dist/index.mjs +759 -749
  3. package/package.json +1 -1
package/dist/index.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,201 +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
- const existing = patchesBySubscription.get(entry.subscription) ?? [];
1074
- existing.push({
1075
- type: "set",
1076
- path: entry.path,
1077
- value: newValue
1078
- });
1079
- patchesBySubscription.set(entry.subscription, existing);
1080
- }
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]];
1081
1208
  }
1082
- return patchesBySubscription;
1209
+ return root;
1083
1210
  };
1084
1211
 
1085
1212
  //#endregion
@@ -1091,141 +1218,39 @@ const generatePatches = (changedKeys, subscriptions, storage) => {
1091
1218
  var Cache = class {
1092
1219
  #schemaMeta;
1093
1220
  #storage = { [RootFieldKey]: {} };
1221
+ #registry = new CursorRegistry();
1094
1222
  #subscriptions = /* @__PURE__ */ new Map();
1223
+ #stalled = /* @__PURE__ */ new Map();
1224
+ #optimistic = new OptimisticStack();
1225
+ #nextId = 1;
1095
1226
  #stale = /* @__PURE__ */ new Set();
1096
- #optimisticKeys = [];
1097
- #optimisticLayers = /* @__PURE__ */ new Map();
1098
- #storageView = null;
1099
1227
  constructor(schemaMetadata) {
1100
1228
  this.#schemaMeta = schemaMetadata;
1101
1229
  }
1102
- #getStorageView() {
1103
- if (this.#optimisticKeys.length === 0) return this.#storage;
1104
- if (this.#storageView) return this.#storageView;
1105
- const merged = { ...this.#storage };
1106
- for (const storageKey of Object.keys(this.#storage)) merged[storageKey] = { ...this.#storage[storageKey] };
1107
- for (const key of this.#optimisticKeys) {
1108
- const layer = this.#optimisticLayers.get(key);
1109
- if (!layer) continue;
1110
- for (const storageKey of Object.keys(layer.storage)) merged[storageKey] = merged[storageKey] ? {
1111
- ...merged[storageKey],
1112
- ...layer.storage[storageKey]
1113
- } : { ...layer.storage[storageKey] };
1114
- }
1115
- this.#storageView = merged;
1116
- return merged;
1117
- }
1118
- /**
1119
- * Writes an optimistic response to a separate cache layer.
1120
- * The optimistic data is immediately visible in reads but does not affect the base storage.
1121
- * @internal
1122
- * @param key - Unique key identifying this optimistic mutation (typically the operation key).
1123
- * @param artifact - GraphQL document artifact.
1124
- * @param variables - Operation variables.
1125
- * @param data - The optimistic response data.
1126
- */
1127
- writeOptimistic(key, artifact, variables, data) {
1128
- const layerStorage = { [RootFieldKey]: {} };
1129
- const layerDependencies = /* @__PURE__ */ new Set();
1130
- normalize(this.#schemaMeta, artifact.selections, layerStorage, data, variables, (storageKey, fieldKey) => {
1131
- layerDependencies.add(makeDependencyKey(storageKey, fieldKey));
1132
- });
1133
- const oldValues = /* @__PURE__ */ new Map();
1134
- const currentView = this.#getStorageView();
1135
- for (const depKey of layerDependencies) {
1136
- const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
1137
- oldValues.set(depKey, currentView[sk]?.[fk]);
1138
- }
1139
- this.#optimisticKeys.push(key);
1140
- this.#optimisticLayers.set(key, {
1141
- storage: layerStorage,
1142
- dependencies: layerDependencies
1143
- });
1144
- this.#storageView = null;
1145
- const newView = this.#getStorageView();
1146
- const changedKeys = /* @__PURE__ */ new Map();
1147
- for (const depKey of layerDependencies) {
1148
- const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
1149
- const newVal = newView[sk]?.[fk];
1150
- const oldVal = oldValues.get(depKey);
1151
- if (oldVal !== newVal) changedKeys.set(depKey, {
1152
- oldValue: oldVal,
1153
- newValue: newVal
1154
- });
1155
- }
1156
- const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, newView);
1157
- for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
1158
- }
1159
- /**
1160
- * Removes an optimistic layer and notifies affected subscribers.
1161
- * @internal
1162
- * @param key - The key of the optimistic layer to remove.
1163
- */
1164
- removeOptimistic(key) {
1165
- const layer = this.#optimisticLayers.get(key);
1166
- if (!layer) return;
1167
- const currentView = this.#getStorageView();
1168
- const oldValues = /* @__PURE__ */ new Map();
1169
- for (const depKey of layer.dependencies) {
1170
- const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
1171
- oldValues.set(depKey, currentView[sk]?.[fk]);
1172
- }
1173
- this.#optimisticLayers.delete(key);
1174
- this.#optimisticKeys = this.#optimisticKeys.filter((k) => k !== key);
1175
- this.#storageView = null;
1176
- const newView = this.#getStorageView();
1177
- const changedKeys = /* @__PURE__ */ new Map();
1178
- for (const depKey of layer.dependencies) {
1179
- const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
1180
- const newVal = newView[sk]?.[fk];
1181
- const oldVal = oldValues.get(depKey);
1182
- if (oldVal !== newVal) changedKeys.set(depKey, {
1183
- oldValue: oldVal,
1184
- newValue: newVal
1185
- });
1186
- }
1187
- const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, newView);
1188
- for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
1189
- }
1190
1230
  /**
1191
1231
  * Writes a query result to the cache, normalizing entities.
1192
- * In addition to field-level stale clearing, this also clears entity-level stale entries
1193
- * (e.g., `"User:1"`) when any field of that entity is written, because {@link invalidate}
1194
- * supports entity-level invalidation without specifying a field.
1195
1232
  * @param artifact - GraphQL document artifact.
1196
1233
  * @param variables - Query variables.
1197
1234
  * @param data - Query result data.
1198
1235
  */
1199
1236
  writeQuery(artifact, variables, data) {
1200
- const changedKeys = /* @__PURE__ */ new Map();
1237
+ const changes = [];
1201
1238
  const staleClearedKeys = /* @__PURE__ */ new Set();
1202
1239
  const entityStaleCleared = /* @__PURE__ */ new Set();
1203
1240
  normalize(this.#schemaMeta, artifact.selections, this.#storage, data, variables, (storageKey, fieldKey, oldValue, newValue) => {
1204
1241
  const depKey = makeDependencyKey(storageKey, fieldKey);
1205
1242
  if (this.#stale.delete(depKey)) staleClearedKeys.add(depKey);
1206
1243
  if (!entityStaleCleared.has(storageKey) && this.#stale.delete(storageKey)) entityStaleCleared.add(storageKey);
1207
- if (oldValue !== newValue) changedKeys.set(depKey, {
1244
+ if (!isEqual(oldValue, newValue)) changes.push({
1245
+ depKey,
1246
+ storageKey,
1247
+ fieldKey,
1208
1248
  oldValue,
1209
1249
  newValue
1210
1250
  });
1211
1251
  });
1212
- const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, this.#storage);
1213
- for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
1214
- const staleOnlySubscriptions = /* @__PURE__ */ new Set();
1215
- for (const depKey of staleClearedKeys) {
1216
- if (changedKeys.has(depKey)) continue;
1217
- const entries = this.#subscriptions.get(depKey);
1218
- if (entries) {
1219
- for (const entry of entries) if (!patchesBySubscription.has(entry.subscription)) staleOnlySubscriptions.add(entry.subscription);
1220
- }
1221
- }
1222
- for (const entityKey of entityStaleCleared) {
1223
- const prefix = `${entityKey}.`;
1224
- for (const [depKey, entries] of this.#subscriptions) if (depKey.startsWith(prefix)) {
1225
- for (const entry of entries) if (!patchesBySubscription.has(entry.subscription)) staleOnlySubscriptions.add(entry.subscription);
1226
- }
1227
- }
1228
- for (const subscription of staleOnlySubscriptions) subscription.listener(null);
1252
+ this.#notifySubscribers(changes);
1253
+ this.#notifyStaleCleared(staleClearedKeys, entityStaleCleared, changes);
1229
1254
  }
1230
1255
  /**
1231
1256
  * Reads a query result from the cache, denormalizing entities if available.
@@ -1235,8 +1260,7 @@ var Cache = class {
1235
1260
  */
1236
1261
  readQuery(artifact, variables) {
1237
1262
  let stale = false;
1238
- const storage = this.#getStorageView();
1239
- 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) => {
1240
1264
  if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
1241
1265
  });
1242
1266
  if (partial) return {
@@ -1253,49 +1277,46 @@ var Cache = class {
1253
1277
  * @param artifact - GraphQL document artifact.
1254
1278
  * @param variables - Query variables.
1255
1279
  * @param listener - Callback function to invoke on cache changes.
1256
- * @returns Object containing initial data, stale status, unsubscribe function, and subscription.
1280
+ * @returns Object containing initial data, stale status, and unsubscribe function.
1257
1281
  */
1258
1282
  subscribeQuery(artifact, variables, listener) {
1283
+ const id = this.#nextId++;
1284
+ const vars = variables;
1259
1285
  let stale = false;
1260
- const tuples = [];
1261
- const storageView = this.#getStorageView();
1262
- const { data, partial } = denormalize(artifact.selections, storageView, storageView[RootFieldKey], variables, (storageKey, fieldKey, path, selections) => {
1263
- tuples.push({
1264
- storageKey,
1265
- fieldKey,
1266
- path,
1267
- selections
1268
- });
1269
- if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
1270
- }, { trackFragmentDeps: false });
1271
- 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
+ }
1272
1294
  const subscription = {
1295
+ id,
1296
+ kind: "query",
1297
+ artifact,
1298
+ variables: vars,
1273
1299
  listener,
1274
- selections: artifact.selections,
1275
- variables,
1276
- entryTree
1300
+ data: traceResult.complete ? traceResult.data : null,
1301
+ stale,
1302
+ cursors: new Set(traceResult.cursors.map((c) => c.entry))
1277
1303
  };
1278
- for (const tuple of tuples) {
1279
- const depKey = makeDependencyKey(tuple.storageKey, tuple.fieldKey);
1280
- const entry = {
1281
- path: tuple.path,
1282
- subscription
1283
- };
1284
- let entrySet = this.#subscriptions.get(depKey);
1285
- if (!entrySet) {
1286
- entrySet = /* @__PURE__ */ new Set();
1287
- this.#subscriptions.set(depKey, entrySet);
1288
- }
1289
- entrySet.add(entry);
1290
- }
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
+ });
1291
1310
  const unsubscribe = () => {
1292
- this.#removeSubscriptionFromTree(entryTree, subscription);
1311
+ this.#registry.removeAll(subscription.cursors);
1312
+ this.#subscriptions.delete(id);
1313
+ this.#stalled.delete(id);
1293
1314
  };
1294
1315
  return {
1295
- data: partial ? null : data,
1316
+ data: subscription.data,
1296
1317
  stale,
1297
- unsubscribe,
1298
- subscription
1318
+ subId: id,
1319
+ unsubscribe
1299
1320
  };
1300
1321
  }
1301
1322
  /**
@@ -1307,14 +1328,13 @@ var Cache = class {
1307
1328
  readFragment(artifact, fragmentRef) {
1308
1329
  const storageKey = fragmentRef[FragmentRefKey];
1309
1330
  const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
1310
- const storageView = this.#getStorageView();
1311
1331
  let stale = false;
1312
- const value = storageView[storageKey];
1332
+ const value = this.#storage[storageKey];
1313
1333
  if (!value) return {
1314
1334
  data: null,
1315
1335
  stale: false
1316
1336
  };
1317
- 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) => {
1318
1338
  if (this.#stale.has(sk) || this.#stale.has(makeDependencyKey(sk, fieldKey))) stale = true;
1319
1339
  });
1320
1340
  if (partial) return {
@@ -1331,81 +1351,86 @@ var Cache = class {
1331
1351
  * @param artifact - GraphQL fragment artifact.
1332
1352
  * @param fragmentRef - Fragment reference containing entity key.
1333
1353
  * @param listener - Callback function to invoke on cache changes.
1334
- * @returns Object containing initial data, stale status, unsubscribe function, and subscription.
1354
+ * @returns Object containing initial data, stale status, and unsubscribe function.
1335
1355
  */
1336
1356
  subscribeFragment(artifact, fragmentRef, listener) {
1337
1357
  const storageKey = fragmentRef[FragmentRefKey];
1338
1358
  const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
1339
- const storageView = this.#getStorageView();
1340
- const value = storageKey === RootFieldKey ? storageView[RootFieldKey] : storageView[storageKey];
1359
+ const id = this.#nextId++;
1360
+ const value = storageKey === RootFieldKey ? this.#storage[RootFieldKey] : this.#storage[storageKey];
1341
1361
  if (!value) {
1342
- 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);
1343
1374
  return {
1344
1375
  data: null,
1345
1376
  stale: false,
1346
- unsubscribe: () => {},
1347
- subscription: {
1348
- listener,
1349
- selections: artifact.selections,
1350
- variables: fragmentVars,
1351
- entryTree
1352
- }
1377
+ subId: id,
1378
+ unsubscribe: () => this.#subscriptions.delete(id)
1353
1379
  };
1354
1380
  }
1355
1381
  let stale = false;
1356
- const tuples = [];
1357
- const denormalizeValue = storageKey === RootFieldKey ? value : { [EntityLinkKey]: storageKey };
1358
- const { data, partial } = denormalize(artifact.selections, storageView, denormalizeValue, fragmentVars, (sk, fieldKey, path, selections) => {
1359
- tuples.push({
1360
- storageKey: sk,
1361
- fieldKey,
1362
- path,
1363
- selections
1364
- });
1365
- if (this.#stale.has(sk) || this.#stale.has(makeDependencyKey(sk, fieldKey))) stale = true;
1366
- }, { trackFragmentDeps: false });
1367
- if (partial) {
1368
- 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);
1369
1404
  return {
1370
1405
  data: null,
1371
1406
  stale: false,
1372
- unsubscribe: () => {},
1373
- subscription: {
1374
- listener,
1375
- selections: artifact.selections,
1376
- variables: fragmentVars,
1377
- entryTree
1378
- }
1407
+ subId: id,
1408
+ unsubscribe: () => this.#subscriptions.delete(id)
1379
1409
  };
1380
1410
  }
1381
- const entryTree = buildEntryTree(tuples, storageKey === RootFieldKey ? void 0 : storageKey);
1382
1411
  const subscription = {
1383
- listener,
1384
- selections: artifact.selections,
1412
+ id,
1413
+ kind: "fragment",
1414
+ artifact,
1385
1415
  variables: fragmentVars,
1386
- entryTree
1416
+ listener,
1417
+ entityKey: storageKey,
1418
+ data: traceResult.data,
1419
+ stale,
1420
+ cursors: new Set(traceResult.cursors.map((c) => c.entry))
1387
1421
  };
1388
- for (const tuple of tuples) {
1389
- const depKey = makeDependencyKey(tuple.storageKey, tuple.fieldKey);
1390
- const entry = {
1391
- path: tuple.path,
1392
- subscription
1393
- };
1394
- let entrySet = this.#subscriptions.get(depKey);
1395
- if (!entrySet) {
1396
- entrySet = /* @__PURE__ */ new Set();
1397
- this.#subscriptions.set(depKey, entrySet);
1398
- }
1399
- entrySet.add(entry);
1400
- }
1422
+ this.#subscriptions.set(id, subscription);
1423
+ for (const { depKey, entry } of traceResult.cursors) this.#registry.add(depKey, entry);
1401
1424
  const unsubscribe = () => {
1402
- this.#removeSubscriptionFromTree(entryTree, subscription);
1425
+ this.#registry.removeAll(subscription.cursors);
1426
+ this.#subscriptions.delete(id);
1427
+ this.#stalled.delete(id);
1403
1428
  };
1404
1429
  return {
1405
- data: partial ? null : data,
1430
+ data: traceResult.data,
1406
1431
  stale,
1407
- unsubscribe,
1408
- subscription
1432
+ subId: id,
1433
+ unsubscribe
1409
1434
  };
1410
1435
  }
1411
1436
  readFragments(artifact, fragmentRefs) {
@@ -1436,6 +1461,45 @@ var Cache = class {
1436
1461
  };
1437
1462
  }
1438
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
+ /**
1439
1503
  * Invalidates one or more cache entries and notifies affected subscribers.
1440
1504
  * @param targets - Cache entries to invalidate.
1441
1505
  */
@@ -1445,10 +1509,10 @@ var Cache = class {
1445
1509
  const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
1446
1510
  const depKey = makeDependencyKey(RootFieldKey, fieldKey);
1447
1511
  this.#stale.add(depKey);
1448
- this.#collectSubscriptions(RootFieldKey, fieldKey, affectedSubscriptions);
1512
+ this.#collectAffectedSubscriptions(RootFieldKey, fieldKey, affectedSubscriptions);
1449
1513
  } else {
1450
1514
  this.#stale.add(RootFieldKey);
1451
- this.#collectSubscriptions(RootFieldKey, void 0, affectedSubscriptions);
1515
+ this.#collectAffectedSubscriptions(RootFieldKey, void 0, affectedSubscriptions);
1452
1516
  }
1453
1517
  else {
1454
1518
  const keyFields = this.#schemaMeta.entities[target.__typename]?.keyFields;
@@ -1458,10 +1522,10 @@ var Cache = class {
1458
1522
  if ("$field" in target) {
1459
1523
  const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
1460
1524
  this.#stale.add(makeDependencyKey(entityKey, fieldKey));
1461
- this.#collectSubscriptions(entityKey, fieldKey, affectedSubscriptions);
1525
+ this.#collectAffectedSubscriptions(entityKey, fieldKey, affectedSubscriptions);
1462
1526
  } else {
1463
1527
  this.#stale.add(entityKey);
1464
- this.#collectSubscriptions(entityKey, void 0, affectedSubscriptions);
1528
+ this.#collectAffectedSubscriptions(entityKey, void 0, affectedSubscriptions);
1465
1529
  }
1466
1530
  } else {
1467
1531
  const prefix = `${target.__typename}:`;
@@ -1470,61 +1534,33 @@ var Cache = class {
1470
1534
  if ("$field" in target) {
1471
1535
  const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
1472
1536
  this.#stale.add(makeDependencyKey(entityKey, fieldKey));
1473
- this.#collectSubscriptions(entityKey, fieldKey, affectedSubscriptions);
1537
+ this.#collectAffectedSubscriptions(entityKey, fieldKey, affectedSubscriptions);
1474
1538
  } else {
1475
1539
  this.#stale.add(entityKey);
1476
- this.#collectSubscriptions(entityKey, void 0, affectedSubscriptions);
1540
+ this.#collectAffectedSubscriptions(entityKey, void 0, affectedSubscriptions);
1477
1541
  }
1478
1542
  }
1479
1543
  }
1480
1544
  }
1481
- 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" });
1482
1554
  }
1483
1555
  /**
1484
1556
  * Checks if a subscription has stale data.
1485
1557
  * @internal
1486
1558
  */
1487
- isStale(subscription) {
1488
- const check = (node) => {
1489
- if (node.depKey.includes("@")) {
1490
- const { storageKey } = parseDependencyKey(node.depKey);
1491
- if (this.#stale.has(storageKey) || this.#stale.has(node.depKey)) return true;
1492
- }
1493
- for (const child of node.children.values()) if (check(child)) return true;
1494
- return false;
1495
- };
1496
- return check(subscription.entryTree);
1497
- }
1498
- #hasKeyFields(target, keyFields) {
1499
- return keyFields.every((f) => f in target);
1500
- }
1501
- #collectSubscriptions(storageKey, fieldKey, out) {
1502
- if (fieldKey === void 0) {
1503
- const prefix = `${storageKey}.`;
1504
- for (const [depKey, entries] of this.#subscriptions) if (depKey.startsWith(prefix)) for (const entry of entries) out.add(entry.subscription);
1505
- } else {
1506
- const depKey = makeDependencyKey(storageKey, fieldKey);
1507
- const entries = this.#subscriptions.get(depKey);
1508
- if (entries) for (const entry of entries) out.add(entry.subscription);
1509
- }
1510
- }
1511
- #removeSubscriptionFromTree(node, subscription) {
1512
- const entries = this.#subscriptions.get(node.depKey);
1513
- if (entries) {
1514
- for (const entry of entries) if (entry.subscription === subscription) {
1515
- entries.delete(entry);
1516
- break;
1517
- }
1518
- if (entries.size === 0) this.#subscriptions.delete(node.depKey);
1519
- }
1520
- for (const child of node.children.values()) this.#removeSubscriptionFromTree(child, subscription);
1521
- }
1522
- #parseDepKey(depKey) {
1523
- return parseDependencyKey(depKey);
1559
+ isStale(subId) {
1560
+ return this.#subscriptions.get(subId)?.stale ?? false;
1524
1561
  }
1525
1562
  /**
1526
1563
  * Extracts a serializable snapshot of the cache storage.
1527
- * Optimistic layers are excluded because they represent transient in-flight state.
1528
1564
  */
1529
1565
  extract() {
1530
1566
  return { storage: structuredClone(this.#storage) };
@@ -1538,18 +1574,100 @@ var Cache = class {
1538
1574
  ...this.#storage[key],
1539
1575
  ...fields
1540
1576
  };
1541
- this.#storageView = null;
1542
1577
  }
1543
1578
  /**
1544
1579
  * Clears all cache data.
1545
1580
  */
1546
1581
  clear() {
1547
1582
  this.#storage = { [RootFieldKey]: {} };
1583
+ this.#registry.clear();
1548
1584
  this.#subscriptions.clear();
1585
+ this.#stalled.clear();
1549
1586
  this.#stale.clear();
1550
- this.#optimisticKeys = [];
1551
- this.#optimisticLayers.clear();
1552
- 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
+ }
1553
1671
  }
1554
1672
  };
1555
1673
 
@@ -1582,7 +1700,6 @@ const cacheExchange = (options = {}) => {
1582
1700
  },
1583
1701
  io: (ops$) => {
1584
1702
  const subscriptionHasData = /* @__PURE__ */ new Map();
1585
- const resubscribe$ = require_make.makeSubject();
1586
1703
  const refetch$ = require_make.makeSubject();
1587
1704
  const fragment$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), require_make.mergeMap((op) => {
1588
1705
  const fragmentRef = op.metadata?.fragment?.ref;
@@ -1593,11 +1710,10 @@ const cacheExchange = (options = {}) => {
1593
1710
  if (isFragmentRefArray(fragmentRef)) {
1594
1711
  const results = require_make.makeSubject();
1595
1712
  const unsubscribes = [];
1596
- const fragmentSubscriptions = [];
1597
1713
  for (const [index, ref] of fragmentRef.entries()) {
1598
- const patchListener = (patches) => {
1599
- if (patches) {
1600
- const indexedPatches = patches.map((patch) => ({
1714
+ const listener = (notification) => {
1715
+ if (notification.type === "patch") {
1716
+ const indexedPatches = notification.patches.map((patch) => ({
1601
1717
  ...patch,
1602
1718
  path: [index, ...patch.path]
1603
1719
  }));
@@ -1607,21 +1723,17 @@ const cacheExchange = (options = {}) => {
1607
1723
  errors: []
1608
1724
  });
1609
1725
  } else {
1610
- const sub = fragmentSubscriptions[index];
1611
- if (sub && cache.isStale(sub)) {
1612
- const { data, stale } = cache.readFragments(op.artifact, fragmentRef);
1613
- if (data !== null) results.next({
1614
- operation: op,
1615
- data,
1616
- ...stale && { metadata: { cache: { stale: true } } },
1617
- errors: []
1618
- });
1619
- }
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
+ });
1620
1733
  }
1621
1734
  };
1622
- const { unsubscribe, subscription } = cache.subscribeFragment(op.artifact, ref, patchListener);
1735
+ const { unsubscribe } = cache.subscribeFragment(op.artifact, ref, listener);
1623
1736
  unsubscribes.push(unsubscribe);
1624
- fragmentSubscriptions.push(subscription);
1625
1737
  }
1626
1738
  const { data: initialData, stale: initialStale } = cache.readFragments(op.artifact, fragmentRef);
1627
1739
  const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => {
@@ -1641,31 +1753,25 @@ const cacheExchange = (options = {}) => {
1641
1753
  errors: []
1642
1754
  });
1643
1755
  const results = require_make.makeSubject();
1644
- let currentUnsubscribe = null;
1645
- let currentSubscription = null;
1646
- const patchListener = (patches) => {
1647
- if (patches) results.next({
1756
+ const listener = (notification) => {
1757
+ if (notification.type === "patch") results.next({
1648
1758
  operation: op,
1649
- metadata: { cache: { patches } },
1759
+ metadata: { cache: { patches: notification.patches } },
1650
1760
  errors: []
1651
1761
  });
1652
- else if (currentSubscription) {
1653
- if (cache.isStale(currentSubscription)) {
1654
- const { data: staleData } = cache.readFragment(op.artifact, fragmentRef);
1655
- if (staleData !== null) results.next({
1656
- operation: op,
1657
- data: staleData,
1658
- metadata: { cache: { stale: true } },
1659
- errors: []
1660
- });
1661
- }
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
+ });
1662
1770
  }
1663
1771
  };
1664
- const { data, stale, unsubscribe, subscription } = cache.subscribeFragment(op.artifact, fragmentRef, patchListener);
1665
- currentUnsubscribe = unsubscribe;
1666
- currentSubscription = subscription;
1772
+ const { data, stale, unsubscribe } = cache.subscribeFragment(op.artifact, fragmentRef, listener);
1667
1773
  const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => {
1668
- if (currentUnsubscribe) currentUnsubscribe();
1774
+ unsubscribe();
1669
1775
  results.complete();
1670
1776
  }));
1671
1777
  return require_make.pipe(require_make.merge(data === null ? empty() : require_make.fromValue({
@@ -1681,50 +1787,34 @@ const cacheExchange = (options = {}) => {
1681
1787
  const query$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), require_make.share());
1682
1788
  return require_make.merge(fragment$, require_make.pipe(query$, require_make.mergeMap((op) => {
1683
1789
  const results = require_make.makeSubject();
1684
- let currentUnsubscribe = null;
1685
- let currentSubscription = null;
1686
- let initialized = false;
1687
- const doSubscribe = () => {
1688
- if (currentUnsubscribe) currentUnsubscribe();
1689
- const patchListener = (patches) => {
1690
- if (patches) {
1691
- if (!initialized) return;
1692
- 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({
1693
1802
  operation: op,
1694
- metadata: { cache: { patches } },
1803
+ data: staleData,
1804
+ metadata: { cache: { stale: true } },
1695
1805
  errors: []
1696
1806
  });
1697
- } else if (currentSubscription) {
1698
- if (cache.isStale(currentSubscription)) {
1699
- const { data: staleData } = cache.readQuery(op.artifact, op.variables);
1700
- if (staleData !== null) results.next({
1701
- operation: op,
1702
- data: staleData,
1703
- metadata: { cache: { stale: true } },
1704
- errors: []
1705
- });
1706
- refetch$.next(op);
1707
- }
1807
+ refetch$.next(op);
1708
1808
  }
1709
- };
1710
- const result = cache.subscribeQuery(op.artifact, op.variables, patchListener);
1711
- currentUnsubscribe = result.unsubscribe;
1712
- currentSubscription = result.subscription;
1713
- return result;
1809
+ }
1714
1810
  };
1715
- const { data, stale } = doSubscribe();
1811
+ const { data, stale, unsubscribe } = cache.subscribeQuery(op.artifact, op.variables, listener);
1716
1812
  subscriptionHasData.set(op.key, data !== null);
1717
- if (data !== null) initialized = true;
1718
1813
  const teardown$ = require_make.pipe(ops$, require_make.filter((o) => o.variant === "teardown" && o.key === op.key), require_make.tap(() => {
1719
- if (currentUnsubscribe) currentUnsubscribe();
1814
+ unsubscribe();
1720
1815
  subscriptionHasData.delete(op.key);
1721
1816
  results.complete();
1722
1817
  }));
1723
- const resubStream$ = require_make.pipe(resubscribe$.source, require_make.filter((key) => key === op.key), require_make.mergeMap(() => {
1724
- doSubscribe();
1725
- initialized = true;
1726
- return empty();
1727
- }));
1728
1818
  const stream$ = require_make.pipe(require_make.merge(data === null ? fetchPolicy === "cache-only" ? require_make.fromValue({
1729
1819
  operation: op,
1730
1820
  data: null,
@@ -1734,7 +1824,7 @@ const cacheExchange = (options = {}) => {
1734
1824
  data,
1735
1825
  ...stale && { metadata: { cache: { stale: true } } },
1736
1826
  errors: []
1737
- }), results.source, resubStream$), require_make.takeUntil(teardown$));
1827
+ }), results.source), require_make.takeUntil(teardown$));
1738
1828
  if (stale) refetch$.next(op);
1739
1829
  return stream$;
1740
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) => {
@@ -1754,7 +1844,6 @@ const cacheExchange = (options = {}) => {
1754
1844
  });
1755
1845
  }
1756
1846
  subscriptionHasData.set(result.operation.key, true);
1757
- resubscribe$.next(result.operation.key);
1758
1847
  const { data } = cache.readQuery(result.operation.artifact, result.operation.variables);
1759
1848
  if (data !== null) return require_make.fromValue({
1760
1849
  ...result,
@@ -1771,99 +1860,6 @@ const cacheExchange = (options = {}) => {
1771
1860
  };
1772
1861
  };
1773
1862
 
1774
- //#endregion
1775
- //#region src/cache/patch.ts
1776
- const copyNode = (node) => Array.isArray(node) ? [...node] : { ...node };
1777
- const shallowCopyPath = (root, path) => {
1778
- if (path.length === 0) return root;
1779
- let result = copyNode(root);
1780
- const top = result;
1781
- for (let i = 0; i < path.length - 1; i++) {
1782
- const key = path[i];
1783
- result[key] = copyNode(result[key]);
1784
- result = result[key];
1785
- }
1786
- return top;
1787
- };
1788
- /**
1789
- * Sets a value at a nested path within an object.
1790
- * @param obj - The object to modify.
1791
- * @param path - The path to the target location.
1792
- * @param value - The value to set.
1793
- */
1794
- const setPath = (obj, path, value) => {
1795
- let current = obj;
1796
- for (let i = 0; i < path.length - 1; i++) current = current[path[i]];
1797
- current[path.at(-1)] = value;
1798
- };
1799
- /**
1800
- * Gets a value at a nested path within an object.
1801
- * @param obj - The object to read from.
1802
- * @param path - The path to the target location.
1803
- * @returns The value at the path, or the object itself if path is empty.
1804
- */
1805
- const getPath = (obj, path) => {
1806
- let current = obj;
1807
- for (const segment of path) {
1808
- if (current === void 0 || current === null) return void 0;
1809
- current = current[segment];
1810
- }
1811
- return current;
1812
- };
1813
- /**
1814
- * Applies cache patches to data immutably, shallow-copying only along changed paths.
1815
- */
1816
- const applyPatchesImmutable = (data, patches) => {
1817
- if (patches.length === 0) return data;
1818
- let result = data;
1819
- for (const patch of patches) if (patch.type === "set") {
1820
- if (patch.path.length === 0) {
1821
- result = patch.value;
1822
- continue;
1823
- }
1824
- result = shallowCopyPath(result, patch.path);
1825
- let target = result;
1826
- for (let i = 0; i < patch.path.length - 1; i++) target = target[patch.path[i]];
1827
- target[patch.path.at(-1)] = patch.value;
1828
- } else if (patch.type === "splice") {
1829
- result = shallowCopyPath(result, patch.path);
1830
- let target = result;
1831
- for (const segment of patch.path) target = target[segment];
1832
- const arr = [...target];
1833
- arr.splice(patch.index, patch.deleteCount, ...patch.items);
1834
- let parent = result;
1835
- for (let i = 0; i < patch.path.length - 1; i++) parent = parent[patch.path[i]];
1836
- parent[patch.path.at(-1)] = arr;
1837
- } else if (patch.type === "swap") {
1838
- result = shallowCopyPath(result, patch.path);
1839
- let target = result;
1840
- for (const segment of patch.path) target = target[segment];
1841
- const arr = [...target];
1842
- [arr[patch.i], arr[patch.j]] = [arr[patch.j], arr[patch.i]];
1843
- let parent = result;
1844
- for (let i = 0; i < patch.path.length - 1; i++) parent = parent[patch.path[i]];
1845
- parent[patch.path.at(-1)] = arr;
1846
- }
1847
- return result;
1848
- };
1849
- /**
1850
- * Applies cache patches to a mutable target object in place.
1851
- * @param target - The mutable object to apply patches to.
1852
- * @param patches - The patches to apply.
1853
- * @returns The new root value if a root-level set patch was applied, otherwise undefined.
1854
- */
1855
- const applyPatchesMutable = (target, patches) => {
1856
- let root;
1857
- for (const patch of patches) if (patch.type === "set") if (patch.path.length === 0) root = patch.value;
1858
- else setPath(target, patch.path, patch.value);
1859
- else if (patch.type === "splice") getPath(target, patch.path).splice(patch.index, patch.deleteCount, ...patch.items);
1860
- else if (patch.type === "swap") {
1861
- const arr = getPath(target, patch.path);
1862
- [arr[patch.i], arr[patch.j]] = [arr[patch.j], arr[patch.i]];
1863
- }
1864
- return root;
1865
- };
1866
-
1867
1863
  //#endregion
1868
1864
  //#region src/exchanges/retry.ts
1869
1865
  const defaultShouldRetry = (error) => isExchangeError(error, "http") && error.extensions?.statusCode !== void 0 && error.extensions.statusCode >= 500;
@@ -2054,7 +2050,7 @@ const subscriptionExchange = (options) => {
2054
2050
  return require_make.pipe(require_make.make((observer) => {
2055
2051
  let unsubscribe;
2056
2052
  let completed = false;
2057
- Promise.resolve().then(() => {
2053
+ const doSubscribe = () => {
2058
2054
  if (completed) return;
2059
2055
  unsubscribe = client.subscribe({
2060
2056
  operationName: op.artifact.name,
@@ -2063,16 +2059,28 @@ const subscriptionExchange = (options) => {
2063
2059
  }, {
2064
2060
  next: (result) => {
2065
2061
  const response = result;
2066
- observer.next({
2067
- operation: op,
2068
- data: response.data,
2069
- errors: response.errors?.map((err) => new GraphQLError(err.message, {
2070
- path: err.path,
2071
- locations: err.locations,
2072
- extensions: err.extensions
2073
- })),
2074
- extensions: response.extensions
2075
- });
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
+ }
2076
2084
  },
2077
2085
  error: (error) => {
2078
2086
  observer.next({
@@ -2082,11 +2090,13 @@ const subscriptionExchange = (options) => {
2082
2090
  cause: error
2083
2091
  })]
2084
2092
  });
2085
- observer.complete();
2093
+ unsubscribe = void 0;
2094
+ Promise.resolve().then(doSubscribe);
2086
2095
  },
2087
2096
  complete: observer.complete
2088
2097
  });
2089
- });
2098
+ };
2099
+ Promise.resolve().then(doSubscribe);
2090
2100
  return () => {
2091
2101
  completed = true;
2092
2102
  unsubscribe?.();