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