@mearie/core 0.5.2 → 0.6.1

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.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { C as mergeMap, S as filter, a as finalize, b as merge, c as share, d as map, i as fromValue, l as takeUntil, m as collect, n as fromSubscription, o as initialize, r as makeSubject, s as switchMap, t as make, u as take, v as fromArray, w as pipe, x as fromPromise, y as tap } from "./make-C7I1YIXm.mjs";
1
+ import { C as mergeMap, S as filter, a as finalize, b as merge, c as share, d as map, i as fromValue, l as takeUntil, m as collect, o as initialize, r as makeSubject, t as make, u as take, v as fromArray, w as pipe, x as fromPromise, y as tap } from "./make-C7I1YIXm.mjs";
2
2
 
3
3
  //#region src/errors.ts
4
4
  /**
@@ -359,15 +359,6 @@ const makeFieldKey = (selection, variables) => {
359
359
  return `${selection.name}@${args}`;
360
360
  };
361
361
  /**
362
- * Generates a unique key for tracking memoized denormalized results for structural sharing.
363
- * @internal
364
- * @param kind - The operation kind ('query', 'fragment', 'fragments').
365
- * @param name - The artifact name.
366
- * @param id - Serialized identifier (variables, entity key, etc.).
367
- * @returns A unique memo key.
368
- */
369
- const makeMemoKey = (kind, name, id) => `${kind}:${name}:${id}`;
370
- /**
371
362
  * Gets a unique key for tracking a field dependency.
372
363
  * @internal
373
364
  * @param storageKey Storage key (entity or root query key).
@@ -444,43 +435,6 @@ const isEqual = (a, b) => {
444
435
  }
445
436
  return false;
446
437
  };
447
- /**
448
- * Recursively replaces a new value tree with the previous one wherever structurally equal,
449
- * preserving referential identity for unchanged subtrees.
450
- *
451
- * Returns `prev` (same reference) when the entire subtree is structurally equal.
452
- * @internal
453
- */
454
- const replaceEqualDeep = (prev, next) => {
455
- if (prev === next) return prev;
456
- if (typeof prev !== typeof next || prev === null || next === null || typeof prev !== "object") return next;
457
- if (Array.isArray(prev)) {
458
- if (!Array.isArray(next)) return next;
459
- let allSame = prev.length === next.length;
460
- const result = [];
461
- for (const [i, item] of next.entries()) {
462
- const shared = i < prev.length ? replaceEqualDeep(prev[i], item) : item;
463
- result.push(shared);
464
- if (shared !== prev[i]) allSame = false;
465
- }
466
- return allSame ? prev : result;
467
- }
468
- if (Array.isArray(next)) return next;
469
- const prevObj = prev;
470
- const nextObj = next;
471
- const nextKeys = Object.keys(nextObj);
472
- const prevKeys = Object.keys(prevObj);
473
- let allSame = nextKeys.length === prevKeys.length;
474
- const result = {};
475
- for (const key of nextKeys) if (key in prevObj) {
476
- result[key] = replaceEqualDeep(prevObj[key], nextObj[key]);
477
- if (result[key] !== prevObj[key]) allSame = false;
478
- } else {
479
- result[key] = nextObj[key];
480
- allSame = false;
481
- }
482
- return allSame ? prev : result;
483
- };
484
438
  const NormalizedKey = Symbol("mearie.normalized");
485
439
  /**
486
440
  * Marks a record as a normalized cache object so that {@link mergeFields}
@@ -534,6 +488,48 @@ const mergeFields = (target, source, deep) => {
534
488
  const makeFieldKeyFromArgs = (field, args) => {
535
489
  return `${field}@${args && Object.keys(args).length > 0 ? stringify(args) : "{}"}`;
536
490
  };
491
+ /**
492
+ * Type guard to check if a value is an array containing entity links.
493
+ * @internal
494
+ * @param value - Value to check.
495
+ * @returns True if the value is an array containing at least one entity link.
496
+ */
497
+ const isEntityLinkArray = (value) => {
498
+ if (!Array.isArray(value) || value.length === 0) return false;
499
+ for (const item of value) {
500
+ if (item === null || item === void 0) continue;
501
+ if (typeof item === "object" && !Array.isArray(item) && EntityLinkKey in item) return true;
502
+ if (Array.isArray(item) && isEntityLinkArray(item)) return true;
503
+ return false;
504
+ }
505
+ return false;
506
+ };
507
+ /**
508
+ * Compares two entity link arrays by their entity keys.
509
+ * @internal
510
+ * @param a - First entity link array.
511
+ * @param b - Second entity link array.
512
+ * @returns True if both arrays have the same entity keys at each position.
513
+ */
514
+ const isEntityLinkArrayEqual = (a, b) => {
515
+ if (a.length !== b.length) return false;
516
+ for (const [i, element] of a.entries()) if ((element?.[EntityLinkKey] ?? null) !== (b[i]?.[EntityLinkKey] ?? null)) return false;
517
+ return true;
518
+ };
519
+ /**
520
+ * Parses a dependency key into its storage key and field key components.
521
+ * @internal
522
+ * @param depKey - The dependency key to parse.
523
+ * @returns The storage key and field key.
524
+ */
525
+ const parseDependencyKey = (depKey) => {
526
+ const atIdx = depKey.indexOf("@");
527
+ const dotIdx = depKey.lastIndexOf(".", atIdx);
528
+ return {
529
+ storageKey: depKey.slice(0, dotIdx),
530
+ fieldKey: depKey.slice(dotIdx + 1)
531
+ };
532
+ };
537
533
 
538
534
  //#endregion
539
535
  //#region src/cache/normalize.ts
@@ -597,59 +593,500 @@ const typenameFieldKey = makeFieldKey({
597
593
  name: "__typename",
598
594
  type: "String"
599
595
  }, {});
600
- const denormalize = (selections, storage, value, variables, accessor) => {
596
+ const denormalize = (selections, storage, value, variables, accessor, options) => {
601
597
  let partial = false;
602
- const denormalizeField = (storageKey, selections, value) => {
598
+ const denormalizeField = (storageKey, selections, value, path) => {
603
599
  if (isNullish(value)) return value;
604
- if (Array.isArray(value)) return value.map((item) => denormalizeField(storageKey, selections, item));
600
+ if (Array.isArray(value)) return value.map((item, i) => denormalizeField(storageKey, selections, item, [...path, i]));
605
601
  const data = value;
606
602
  if (isEntityLink(data)) {
607
603
  const entityKey = data[EntityLinkKey];
608
604
  const entity = storage[entityKey];
609
605
  if (!entity) {
610
- accessor?.(entityKey, typenameFieldKey);
606
+ accessor?.(entityKey, typenameFieldKey, path);
611
607
  partial = true;
612
608
  return null;
613
609
  }
614
- return denormalizeField(entityKey, selections, entity);
610
+ return denormalizeField(entityKey, selections, entity, path);
615
611
  }
616
612
  const fields = {};
617
613
  for (const selection of selections) if (selection.kind === "Field") {
618
614
  const fieldKey = makeFieldKey(selection, variables);
619
615
  const fieldValue = data[fieldKey];
620
- if (storageKey !== null) accessor?.(storageKey, fieldKey);
616
+ const fieldPath = [...path, selection.alias ?? selection.name];
617
+ if (storageKey !== null) accessor?.(storageKey, fieldKey, fieldPath, selection.selections);
621
618
  if (fieldValue === void 0) {
622
619
  partial = true;
623
620
  continue;
624
621
  }
625
622
  const name = selection.alias ?? selection.name;
626
- const value = selection.selections ? denormalizeField(null, selection.selections, fieldValue) : fieldValue;
627
- if (name in fields) mergeFields(fields, { [name]: value }, true);
628
- else fields[name] = value;
623
+ const resolvedValue = selection.selections ? denormalizeField(null, selection.selections, fieldValue, fieldPath) : fieldValue;
624
+ if (name in fields) mergeFields(fields, { [name]: resolvedValue }, true);
625
+ else fields[name] = resolvedValue;
629
626
  } else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
630
627
  fields[FragmentRefKey] = storageKey;
631
- if (selection.args) {
632
- const resolvedArgs = resolveArguments(selection.args, variables);
633
- const mergedVars = {
634
- ...variables,
635
- ...resolvedArgs
636
- };
637
- fields[FragmentVarsKey] = {
638
- ...fields[FragmentVarsKey],
639
- [selection.name]: mergedVars
640
- };
628
+ const merged = selection.args ? {
629
+ ...variables,
630
+ ...resolveArguments(selection.args, variables)
631
+ } : { ...variables };
632
+ fields[FragmentVarsKey] = {
633
+ ...fields[FragmentVarsKey],
634
+ [selection.name]: merged
635
+ };
636
+ if (accessor) {
637
+ if (denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, options?.trackFragmentDeps === false ? void 0 : accessor, options).partial) partial = true;
638
+ }
639
+ } else if (storageKey === RootFieldKey) {
640
+ fields[FragmentRefKey] = RootFieldKey;
641
+ const merged = selection.args ? {
642
+ ...variables,
643
+ ...resolveArguments(selection.args, variables)
644
+ } : { ...variables };
645
+ fields[FragmentVarsKey] = {
646
+ ...fields[FragmentVarsKey],
647
+ [selection.name]: merged
648
+ };
649
+ if (accessor) {
650
+ if (denormalize(selection.selections, storage, storage[RootFieldKey], variables, options?.trackFragmentDeps === false ? void 0 : accessor, options).partial) partial = true;
641
651
  }
642
- if (accessor) denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, accessor);
643
- } else mergeFields(fields, denormalizeField(storageKey, selection.selections, value), true);
644
- else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value), true);
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);
645
654
  return fields;
646
655
  };
647
656
  return {
648
- data: denormalizeField(RootFieldKey, selections, value),
657
+ data: denormalizeField(RootFieldKey, selections, value, []),
649
658
  partial
650
659
  };
651
660
  };
652
661
 
662
+ //#endregion
663
+ //#region src/cache/tree.ts
664
+ /**
665
+ * @internal
666
+ */
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;
685
+ }
686
+ current.depKey = makeDependencyKey(storageKey, fieldKey);
687
+ if (selections) current.selections = selections;
688
+ }
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));
699
+ }
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;
714
+ }
715
+ if (entries.size === 0) subscriptions.delete(node.depKey);
716
+ }
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]);
729
+ }
730
+ return result;
731
+ };
732
+ /**
733
+ * @internal
734
+ */
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
773
+ };
774
+ let entrySet = subscriptions.get(depKey);
775
+ if (!entrySet) {
776
+ entrySet = /* @__PURE__ */ new Set();
777
+ subscriptions.set(depKey, entrySet);
778
+ }
779
+ entrySet.add(entry);
780
+ if (relativePath.length === 1) {
781
+ const fieldName = String(relativePath[0]);
782
+ if (data && typeof data === "object") fieldValues.set(fieldName, data[fieldName]);
783
+ }
784
+ }
785
+ return {
786
+ data,
787
+ fieldValues
788
+ };
789
+ };
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
+
817
+ //#endregion
818
+ //#region src/cache/diff.ts
819
+ /**
820
+ * Finds the common prefix and suffix boundaries between two key arrays.
821
+ * @internal
822
+ */
823
+ const findCommonBounds = (oldKeys, newKeys) => {
824
+ let start = 0;
825
+ while (start < oldKeys.length && start < newKeys.length && oldKeys[start] === newKeys[start]) start++;
826
+ let oldEnd = oldKeys.length;
827
+ let newEnd = newKeys.length;
828
+ while (oldEnd > start && newEnd > start && oldKeys[oldEnd - 1] === newKeys[newEnd - 1]) {
829
+ oldEnd--;
830
+ newEnd--;
831
+ }
832
+ return {
833
+ start,
834
+ oldEnd,
835
+ newEnd
836
+ };
837
+ };
838
+ /**
839
+ * Computes swap operations to reorder oldKeys into newKeys order using selection sort.
840
+ * @internal
841
+ */
842
+ const computeSwaps = (oldKeys, newKeys) => {
843
+ const working = [...oldKeys];
844
+ const swaps = [];
845
+ for (const [i, newKey] of newKeys.entries()) {
846
+ if (working[i] === newKey) continue;
847
+ const j = working.indexOf(newKey, i + 1);
848
+ if (j === -1) continue;
849
+ [working[i], working[j]] = [working[j], working[i]];
850
+ swaps.push({
851
+ i,
852
+ j
853
+ });
854
+ }
855
+ return swaps;
856
+ };
857
+
858
+ //#endregion
859
+ //#region src/cache/change.ts
860
+ /**
861
+ * @internal
862
+ */
863
+ const classifyChanges = (changedKeys) => {
864
+ const structural = [];
865
+ const scalar = [];
866
+ for (const [depKey, { oldValue, newValue }] of changedKeys) {
867
+ if (isEntityLink(oldValue) && isEntityLink(newValue) && oldValue[EntityLinkKey] === newValue[EntityLinkKey]) continue;
868
+ 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
+ });
878
+ }
879
+ return {
880
+ structural,
881
+ scalar
882
+ };
883
+ };
884
+ /**
885
+ * @internal
886
+ */
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);
892
+ patches.push({
893
+ type: "set",
894
+ path: entry.path,
895
+ value: null
896
+ });
897
+ return patches;
898
+ }
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
912
+ });
913
+ return patches;
914
+ }
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
923
+ });
924
+ return patches;
925
+ }
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
936
+ });
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);
960
+ }
961
+ patches.push({
962
+ type: "splice",
963
+ path: entry.path,
964
+ index: idx,
965
+ deleteCount: 1,
966
+ items: []
967
+ });
968
+ }
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
+ }
1022
+ }
1023
+ rebuildArrayIndices(node, entry, subscriptions);
1024
+ return patches;
1025
+ }
1026
+ return patches;
1027
+ };
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]);
1032
+ };
1033
+ const findSiblingSelections = (node) => {
1034
+ for (const child of node.children.values()) if (child.selections) return child.selections;
1035
+ return node.selections;
1036
+ };
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);
1044
+ }
1045
+ };
1046
+ /**
1047
+ * @internal
1048
+ */
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
+ }
1065
+ }
1066
+ }
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
+ }
1086
+ }
1087
+ return patchesBySubscription;
1088
+ };
1089
+
653
1090
  //#endregion
654
1091
  //#region src/cache/cache.ts
655
1092
  /**
@@ -660,7 +1097,6 @@ var Cache = class {
660
1097
  #schemaMeta;
661
1098
  #storage = { [RootFieldKey]: {} };
662
1099
  #subscriptions = /* @__PURE__ */ new Map();
663
- #memo = /* @__PURE__ */ new Map();
664
1100
  #stale = /* @__PURE__ */ new Set();
665
1101
  #optimisticKeys = [];
666
1102
  #optimisticLayers = /* @__PURE__ */ new Map();
@@ -695,22 +1131,35 @@ var Cache = class {
695
1131
  */
696
1132
  writeOptimistic(key, artifact, variables, data) {
697
1133
  const layerStorage = { [RootFieldKey]: {} };
698
- const dependencies = /* @__PURE__ */ new Set();
1134
+ const layerDependencies = /* @__PURE__ */ new Set();
699
1135
  normalize(this.#schemaMeta, artifact.selections, layerStorage, data, variables, (storageKey, fieldKey) => {
700
- dependencies.add(makeDependencyKey(storageKey, fieldKey));
1136
+ layerDependencies.add(makeDependencyKey(storageKey, fieldKey));
701
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
+ }
702
1144
  this.#optimisticKeys.push(key);
703
1145
  this.#optimisticLayers.set(key, {
704
1146
  storage: layerStorage,
705
- dependencies
1147
+ dependencies: layerDependencies
706
1148
  });
707
1149
  this.#storageView = null;
708
- const subscriptions = /* @__PURE__ */ new Set();
709
- for (const depKey of dependencies) {
710
- const ss = this.#subscriptions.get(depKey);
711
- if (ss) for (const s of ss) subscriptions.add(s);
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
+ });
712
1160
  }
713
- for (const subscription of subscriptions) subscription.listener();
1161
+ const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, newView);
1162
+ for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
714
1163
  }
715
1164
  /**
716
1165
  * Removes an optimistic layer and notifies affected subscribers.
@@ -720,42 +1169,71 @@ var Cache = class {
720
1169
  removeOptimistic(key) {
721
1170
  const layer = this.#optimisticLayers.get(key);
722
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
+ }
723
1178
  this.#optimisticLayers.delete(key);
724
1179
  this.#optimisticKeys = this.#optimisticKeys.filter((k) => k !== key);
725
1180
  this.#storageView = null;
726
- const subscriptions = /* @__PURE__ */ new Set();
1181
+ const newView = this.#getStorageView();
1182
+ const changedKeys = /* @__PURE__ */ new Map();
727
1183
  for (const depKey of layer.dependencies) {
728
- const ss = this.#subscriptions.get(depKey);
729
- if (ss) for (const s of ss) subscriptions.add(s);
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
+ });
730
1191
  }
731
- for (const subscription of subscriptions) subscription.listener();
1192
+ const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, newView);
1193
+ for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
732
1194
  }
733
1195
  /**
734
1196
  * 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.
735
1200
  * @param artifact - GraphQL document artifact.
736
1201
  * @param variables - Query variables.
737
1202
  * @param data - Query result data.
738
1203
  */
739
1204
  writeQuery(artifact, variables, data) {
740
- const dependencies = /* @__PURE__ */ new Set();
741
- const subscriptions = /* @__PURE__ */ new Set();
1205
+ const changedKeys = /* @__PURE__ */ new Map();
1206
+ const staleClearedKeys = /* @__PURE__ */ new Set();
742
1207
  const entityStaleCleared = /* @__PURE__ */ new Set();
743
1208
  normalize(this.#schemaMeta, artifact.selections, this.#storage, data, variables, (storageKey, fieldKey, oldValue, newValue) => {
744
1209
  const depKey = makeDependencyKey(storageKey, fieldKey);
745
- if (this.#stale.delete(depKey)) dependencies.add(depKey);
1210
+ if (this.#stale.delete(depKey)) staleClearedKeys.add(depKey);
746
1211
  if (!entityStaleCleared.has(storageKey) && this.#stale.delete(storageKey)) entityStaleCleared.add(storageKey);
747
- if (oldValue !== newValue) dependencies.add(depKey);
1212
+ if (oldValue !== newValue) changedKeys.set(depKey, {
1213
+ oldValue,
1214
+ newValue
1215
+ });
748
1216
  });
749
- for (const entityKey of entityStaleCleared) this.#collectSubscriptions(entityKey, void 0, subscriptions);
750
- for (const dependency of dependencies) {
751
- const ss = this.#subscriptions.get(dependency);
752
- if (ss) for (const s of ss) subscriptions.add(s);
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
+ }
753
1232
  }
754
- for (const subscription of subscriptions) subscription.listener();
1233
+ for (const subscription of staleOnlySubscriptions) subscription.listener(null);
755
1234
  }
756
1235
  /**
757
1236
  * Reads a query result from the cache, denormalizing entities if available.
758
- * Uses structural sharing to preserve referential identity for unchanged subtrees.
759
1237
  * @param artifact - GraphQL document artifact.
760
1238
  * @param variables - Query variables.
761
1239
  * @returns Denormalized query result or null if not found.
@@ -770,74 +1248,170 @@ var Cache = class {
770
1248
  data: null,
771
1249
  stale: false
772
1250
  };
773
- const key = makeMemoKey("query", artifact.name, stringify(variables));
774
- const prev = this.#memo.get(key);
775
- const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
776
- this.#memo.set(key, result);
777
1251
  return {
778
- data: result,
1252
+ data,
779
1253
  stale
780
1254
  };
781
1255
  }
782
1256
  /**
783
- * Subscribes to cache invalidations for a specific query.
1257
+ * Subscribes to cache changes for a specific query.
784
1258
  * @param artifact - GraphQL document artifact.
785
1259
  * @param variables - Query variables.
786
- * @param listener - Callback function to invoke on cache invalidation.
787
- * @returns Unsubscribe function.
1260
+ * @param listener - Callback function to invoke on cache changes.
1261
+ * @returns Object containing initial data, stale status, unsubscribe function, and subscription.
788
1262
  */
789
1263
  subscribeQuery(artifact, variables, listener) {
790
- const dependencies = /* @__PURE__ */ new Set();
1264
+ let stale = false;
1265
+ const tuples = [];
791
1266
  const storageView = this.#getStorageView();
792
- denormalize(artifact.selections, storageView, storageView[RootFieldKey], variables, (storageKey, fieldKey) => {
793
- const dependencyKey = makeDependencyKey(storageKey, fieldKey);
794
- dependencies.add(dependencyKey);
795
- });
796
- return this.#subscribe(dependencies, listener);
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);
1277
+ const subscription = {
1278
+ listener,
1279
+ selections: artifact.selections,
1280
+ variables,
1281
+ entryTree
1282
+ };
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
+ }
1296
+ const unsubscribe = () => {
1297
+ this.#removeSubscriptionFromTree(entryTree, subscription);
1298
+ };
1299
+ return {
1300
+ data: partial ? null : data,
1301
+ stale,
1302
+ unsubscribe,
1303
+ subscription
1304
+ };
797
1305
  }
798
1306
  /**
799
1307
  * Reads a fragment from the cache for a specific entity.
800
- * Uses structural sharing to preserve referential identity for unchanged subtrees.
801
1308
  * @param artifact - GraphQL fragment artifact.
802
1309
  * @param fragmentRef - Fragment reference containing entity key.
803
1310
  * @returns Denormalized fragment data or null if not found or invalid.
804
1311
  */
805
1312
  readFragment(artifact, fragmentRef) {
806
- const entityKey = fragmentRef[FragmentRefKey];
1313
+ const storageKey = fragmentRef[FragmentRefKey];
807
1314
  const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
808
1315
  const storageView = this.#getStorageView();
809
- if (!storageView[entityKey]) return {
1316
+ let stale = false;
1317
+ const value = storageView[storageKey];
1318
+ if (!value) return {
810
1319
  data: null,
811
1320
  stale: false
812
1321
  };
813
- let stale = false;
814
- const { data, partial } = denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
815
- if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
1322
+ const { data, partial } = denormalize(artifact.selections, storageView, storageKey === RootFieldKey ? value : { [EntityLinkKey]: storageKey }, fragmentVars, (sk, fieldKey) => {
1323
+ if (this.#stale.has(sk) || this.#stale.has(makeDependencyKey(sk, fieldKey))) stale = true;
816
1324
  });
817
1325
  if (partial) return {
818
1326
  data: null,
819
1327
  stale: false
820
1328
  };
821
- const argsId = Object.keys(fragmentVars).length > 0 ? entityKey + stringify(fragmentVars) : entityKey;
822
- const key = makeMemoKey("fragment", artifact.name, argsId);
823
- const prev = this.#memo.get(key);
824
- const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
825
- this.#memo.set(key, result);
826
1329
  return {
827
- data: result,
1330
+ data,
828
1331
  stale
829
1332
  };
830
1333
  }
1334
+ /**
1335
+ * Subscribes to cache changes for a specific fragment.
1336
+ * @param artifact - GraphQL fragment artifact.
1337
+ * @param fragmentRef - Fragment reference containing entity key.
1338
+ * @param listener - Callback function to invoke on cache changes.
1339
+ * @returns Object containing initial data, stale status, unsubscribe function, and subscription.
1340
+ */
831
1341
  subscribeFragment(artifact, fragmentRef, listener) {
832
- const entityKey = fragmentRef[FragmentRefKey];
1342
+ const storageKey = fragmentRef[FragmentRefKey];
833
1343
  const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
834
- const dependencies = /* @__PURE__ */ new Set();
835
1344
  const storageView = this.#getStorageView();
836
- denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
837
- const dependencyKey = makeDependencyKey(storageKey, fieldKey);
838
- dependencies.add(dependencyKey);
839
- });
840
- return this.#subscribe(dependencies, listener);
1345
+ const value = storageKey === RootFieldKey ? storageView[RootFieldKey] : storageView[storageKey];
1346
+ if (!value) {
1347
+ const entryTree = buildEntryTree([]);
1348
+ return {
1349
+ data: null,
1350
+ stale: false,
1351
+ unsubscribe: () => {},
1352
+ subscription: {
1353
+ listener,
1354
+ selections: artifact.selections,
1355
+ variables: fragmentVars,
1356
+ entryTree
1357
+ }
1358
+ };
1359
+ }
1360
+ 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([]);
1374
+ return {
1375
+ data: null,
1376
+ stale: false,
1377
+ unsubscribe: () => {},
1378
+ subscription: {
1379
+ listener,
1380
+ selections: artifact.selections,
1381
+ variables: fragmentVars,
1382
+ entryTree
1383
+ }
1384
+ };
1385
+ }
1386
+ const entryTree = buildEntryTree(tuples, storageKey === RootFieldKey ? void 0 : storageKey);
1387
+ const subscription = {
1388
+ listener,
1389
+ selections: artifact.selections,
1390
+ variables: fragmentVars,
1391
+ entryTree
1392
+ };
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
+ }
1406
+ const unsubscribe = () => {
1407
+ this.#removeSubscriptionFromTree(entryTree, subscription);
1408
+ };
1409
+ return {
1410
+ data: partial ? null : data,
1411
+ stale,
1412
+ unsubscribe,
1413
+ subscription
1414
+ };
841
1415
  }
842
1416
  readFragments(artifact, fragmentRefs) {
843
1417
  const results = [];
@@ -851,42 +1425,35 @@ var Cache = class {
851
1425
  if (result.stale) stale = true;
852
1426
  results.push(result.data);
853
1427
  }
854
- const entityKeys = fragmentRefs.map((ref) => ref[FragmentRefKey]);
855
- const key = makeMemoKey("fragments", artifact.name, entityKeys.join(","));
856
- const prev = this.#memo.get(key);
857
- const result = prev === void 0 ? results : replaceEqualDeep(prev, results);
858
- this.#memo.set(key, result);
859
1428
  return {
860
- data: result,
1429
+ data: results,
861
1430
  stale
862
1431
  };
863
1432
  }
864
1433
  subscribeFragments(artifact, fragmentRefs, listener) {
865
- const dependencies = /* @__PURE__ */ new Set();
866
- const storageView = this.#getStorageView();
1434
+ const unsubscribes = [];
867
1435
  for (const ref of fragmentRefs) {
868
- const entityKey = ref[FragmentRefKey];
869
- const fragmentVars = getFragmentVars(ref, artifact.name);
870
- denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
871
- dependencies.add(makeDependencyKey(storageKey, fieldKey));
872
- });
1436
+ const { unsubscribe } = this.subscribeFragment(artifact, ref, listener);
1437
+ unsubscribes.push(unsubscribe);
873
1438
  }
874
- return this.#subscribe(dependencies, listener);
1439
+ return () => {
1440
+ for (const unsub of unsubscribes) unsub();
1441
+ };
875
1442
  }
876
1443
  /**
877
1444
  * Invalidates one or more cache entries and notifies affected subscribers.
878
1445
  * @param targets - Cache entries to invalidate.
879
1446
  */
880
1447
  invalidate(...targets) {
881
- const subscriptions = /* @__PURE__ */ new Set();
1448
+ const affectedSubscriptions = /* @__PURE__ */ new Set();
882
1449
  for (const target of targets) if (target.__typename === "Query") if ("$field" in target) {
883
1450
  const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
884
1451
  const depKey = makeDependencyKey(RootFieldKey, fieldKey);
885
1452
  this.#stale.add(depKey);
886
- this.#collectSubscriptions(RootFieldKey, fieldKey, subscriptions);
1453
+ this.#collectSubscriptions(RootFieldKey, fieldKey, affectedSubscriptions);
887
1454
  } else {
888
1455
  this.#stale.add(RootFieldKey);
889
- this.#collectSubscriptions(RootFieldKey, void 0, subscriptions);
1456
+ this.#collectSubscriptions(RootFieldKey, void 0, affectedSubscriptions);
890
1457
  }
891
1458
  else {
892
1459
  const keyFields = this.#schemaMeta.entities[target.__typename]?.keyFields;
@@ -896,10 +1463,10 @@ var Cache = class {
896
1463
  if ("$field" in target) {
897
1464
  const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
898
1465
  this.#stale.add(makeDependencyKey(entityKey, fieldKey));
899
- this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
1466
+ this.#collectSubscriptions(entityKey, fieldKey, affectedSubscriptions);
900
1467
  } else {
901
1468
  this.#stale.add(entityKey);
902
- this.#collectSubscriptions(entityKey, void 0, subscriptions);
1469
+ this.#collectSubscriptions(entityKey, void 0, affectedSubscriptions);
903
1470
  }
904
1471
  } else {
905
1472
  const prefix = `${target.__typename}:`;
@@ -908,15 +1475,30 @@ var Cache = class {
908
1475
  if ("$field" in target) {
909
1476
  const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
910
1477
  this.#stale.add(makeDependencyKey(entityKey, fieldKey));
911
- this.#collectSubscriptions(entityKey, fieldKey, subscriptions);
1478
+ this.#collectSubscriptions(entityKey, fieldKey, affectedSubscriptions);
912
1479
  } else {
913
1480
  this.#stale.add(entityKey);
914
- this.#collectSubscriptions(entityKey, void 0, subscriptions);
1481
+ this.#collectSubscriptions(entityKey, void 0, affectedSubscriptions);
915
1482
  }
916
1483
  }
917
1484
  }
918
1485
  }
919
- for (const subscription of subscriptions) subscription.listener();
1486
+ for (const subscription of affectedSubscriptions) subscription.listener(null);
1487
+ }
1488
+ /**
1489
+ * Checks if a subscription has stale data.
1490
+ * @internal
1491
+ */
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);
920
1502
  }
921
1503
  #hasKeyFields(target, keyFields) {
922
1504
  return keyFields.every((f) => f in target);
@@ -924,48 +1506,43 @@ var Cache = class {
924
1506
  #collectSubscriptions(storageKey, fieldKey, out) {
925
1507
  if (fieldKey === void 0) {
926
1508
  const prefix = `${storageKey}.`;
927
- for (const [depKey, ss] of this.#subscriptions) if (depKey.startsWith(prefix)) for (const s of ss) out.add(s);
1509
+ for (const [depKey, entries] of this.#subscriptions) if (depKey.startsWith(prefix)) for (const entry of entries) out.add(entry.subscription);
928
1510
  } else {
929
1511
  const depKey = makeDependencyKey(storageKey, fieldKey);
930
- const ss = this.#subscriptions.get(depKey);
931
- if (ss) for (const s of ss) out.add(s);
1512
+ const entries = this.#subscriptions.get(depKey);
1513
+ if (entries) for (const entry of entries) out.add(entry.subscription);
932
1514
  }
933
1515
  }
934
- #subscribe(dependencies, listener) {
935
- const subscription = { listener };
936
- for (const dependency of dependencies) {
937
- const subscriptions = this.#subscriptions.get(dependency) ?? /* @__PURE__ */ new Set();
938
- subscriptions.add(subscription);
939
- this.#subscriptions.set(dependency, subscriptions);
940
- }
941
- return () => {
942
- for (const dependency of dependencies) {
943
- const subscriptions = this.#subscriptions.get(dependency);
944
- subscriptions?.delete(subscription);
945
- if (subscriptions?.size === 0) this.#subscriptions.delete(dependency);
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;
946
1522
  }
947
- };
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);
948
1529
  }
949
1530
  /**
950
- * Extracts a serializable snapshot of the cache storage and structural sharing state.
1531
+ * Extracts a serializable snapshot of the cache storage.
951
1532
  * Optimistic layers are excluded because they represent transient in-flight state.
952
1533
  */
953
1534
  extract() {
954
- return {
955
- storage: structuredClone(this.#storage),
956
- memo: Object.fromEntries(this.#memo)
957
- };
1535
+ return { storage: structuredClone(this.#storage) };
958
1536
  }
959
1537
  /**
960
1538
  * Hydrates the cache with a previously extracted snapshot.
961
1539
  */
962
1540
  hydrate(snapshot) {
963
- const { storage, memo } = snapshot;
1541
+ const { storage } = snapshot;
964
1542
  for (const [key, fields] of Object.entries(storage)) this.#storage[key] = {
965
1543
  ...this.#storage[key],
966
1544
  ...fields
967
1545
  };
968
- for (const [key, value] of Object.entries(memo)) this.#memo.set(key, value);
969
1546
  this.#storageView = null;
970
1547
  }
971
1548
  /**
@@ -974,7 +1551,6 @@ var Cache = class {
974
1551
  clear() {
975
1552
  this.#storage = { [RootFieldKey]: {} };
976
1553
  this.#subscriptions.clear();
977
- this.#memo.clear();
978
1554
  this.#stale.clear();
979
1555
  this.#optimisticKeys = [];
980
1556
  this.#optimisticLayers.clear();
@@ -1010,6 +1586,9 @@ const cacheExchange = (options = {}) => {
1010
1586
  clear: () => cache.clear()
1011
1587
  },
1012
1588
  io: (ops$) => {
1589
+ const subscriptionHasData = /* @__PURE__ */ new Map();
1590
+ const resubscribe$ = makeSubject();
1591
+ const refetch$ = makeSubject();
1013
1592
  const fragment$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), mergeMap((op) => {
1014
1593
  const fragmentRef = op.metadata?.fragment?.ref;
1015
1594
  if (!fragmentRef) return fromValue({
@@ -1017,77 +1596,152 @@ const cacheExchange = (options = {}) => {
1017
1596
  errors: [new ExchangeError("Fragment operation missing fragment.ref in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
1018
1597
  });
1019
1598
  if (isFragmentRefArray(fragmentRef)) {
1020
- const trigger = makeSubject();
1021
- const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
1022
- return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readFragments(op.artifact, fragmentRef), () => cache.subscribeFragments(op.artifact, fragmentRef, async () => {
1023
- await Promise.resolve();
1024
- trigger.next();
1025
- }))), takeUntil(teardown$), map(({ data, stale }) => ({
1599
+ const results = makeSubject();
1600
+ const unsubscribes = [];
1601
+ const fragmentSubscriptions = [];
1602
+ for (const [index, ref] of fragmentRef.entries()) {
1603
+ const patchListener = (patches) => {
1604
+ if (patches) {
1605
+ const indexedPatches = patches.map((patch) => ({
1606
+ ...patch,
1607
+ path: [index, ...patch.path]
1608
+ }));
1609
+ results.next({
1610
+ operation: op,
1611
+ metadata: { cache: { patches: indexedPatches } },
1612
+ errors: []
1613
+ });
1614
+ } 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
+ }
1625
+ }
1626
+ };
1627
+ const { unsubscribe, subscription } = cache.subscribeFragment(op.artifact, ref, patchListener);
1628
+ unsubscribes.push(unsubscribe);
1629
+ fragmentSubscriptions.push(subscription);
1630
+ }
1631
+ const { data: initialData, stale: initialStale } = cache.readFragments(op.artifact, fragmentRef);
1632
+ const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => {
1633
+ for (const unsub of unsubscribes) unsub();
1634
+ results.complete();
1635
+ }));
1636
+ return pipe(merge(fromValue({
1026
1637
  operation: op,
1027
- data,
1028
- ...stale && { metadata: { cache: { stale: true } } },
1638
+ data: initialData,
1639
+ ...initialStale && { metadata: { cache: { stale: true } } },
1029
1640
  errors: []
1030
- })));
1641
+ }), results.source), takeUntil(teardown$));
1031
1642
  }
1032
1643
  if (!isFragmentRef(fragmentRef)) return fromValue({
1033
1644
  operation: op,
1034
1645
  data: fragmentRef,
1035
1646
  errors: []
1036
1647
  });
1037
- const trigger = makeSubject();
1038
- const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
1039
- return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readFragment(op.artifact, fragmentRef), () => cache.subscribeFragment(op.artifact, fragmentRef, async () => {
1040
- await Promise.resolve();
1041
- trigger.next();
1042
- }))), takeUntil(teardown$), map(({ data, stale }) => ({
1648
+ const results = makeSubject();
1649
+ let currentUnsubscribe = null;
1650
+ let currentSubscription = null;
1651
+ const patchListener = (patches) => {
1652
+ if (patches) results.next({
1653
+ operation: op,
1654
+ metadata: { cache: { patches } },
1655
+ errors: []
1656
+ });
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
+ }
1667
+ }
1668
+ };
1669
+ const { data, stale, unsubscribe, subscription } = cache.subscribeFragment(op.artifact, fragmentRef, patchListener);
1670
+ currentUnsubscribe = unsubscribe;
1671
+ currentSubscription = subscription;
1672
+ const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => {
1673
+ if (currentUnsubscribe) currentUnsubscribe();
1674
+ results.complete();
1675
+ }));
1676
+ return pipe(merge(data === null ? empty() : fromValue({
1043
1677
  operation: op,
1044
1678
  data,
1045
1679
  ...stale && { metadata: { cache: { stale: true } } },
1046
1680
  errors: []
1047
- })));
1681
+ }), results.source), takeUntil(teardown$));
1048
1682
  }));
1049
1683
  const nonCache$ = pipe(ops$, filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "subscription" || op.artifact.kind === "query" && fetchPolicy === "network-only")), tap((op) => {
1050
1684
  if (op.artifact.kind === "mutation" && op.metadata?.cache?.optimisticResponse) cache.writeOptimistic(op.key, op.artifact, op.variables, op.metadata.cache.optimisticResponse);
1051
1685
  }));
1052
1686
  const query$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), share());
1053
- const refetch$ = makeSubject();
1054
1687
  return merge(fragment$, pipe(query$, mergeMap((op) => {
1055
- const trigger = makeSubject();
1056
- let hasData = false;
1057
- const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => trigger.complete()));
1058
- return pipe(merge(fromValue(void 0), trigger.source), switchMap(() => fromSubscription(() => cache.readQuery(op.artifact, op.variables), () => cache.subscribeQuery(op.artifact, op.variables, async () => {
1059
- await Promise.resolve();
1060
- trigger.next();
1061
- }))), takeUntil(teardown$), mergeMap(({ data, stale }) => {
1062
- if (data !== null && !stale) {
1063
- hasData = true;
1064
- return fromValue({
1065
- operation: op,
1066
- data,
1067
- errors: []
1068
- });
1069
- }
1070
- if (data !== null && stale) {
1071
- hasData = true;
1072
- refetch$.next(op);
1073
- return fromValue({
1074
- operation: op,
1075
- data,
1076
- metadata: { cache: { stale: true } },
1077
- errors: []
1078
- });
1079
- }
1080
- if (hasData) {
1081
- refetch$.next(op);
1082
- return empty();
1083
- }
1084
- if (fetchPolicy === "cache-only") return fromValue({
1085
- operation: op,
1086
- data: null,
1087
- errors: []
1088
- });
1688
+ 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({
1698
+ operation: op,
1699
+ metadata: { cache: { patches } },
1700
+ errors: []
1701
+ });
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
+ }
1713
+ }
1714
+ };
1715
+ const result = cache.subscribeQuery(op.artifact, op.variables, patchListener);
1716
+ currentUnsubscribe = result.unsubscribe;
1717
+ currentSubscription = result.subscription;
1718
+ return result;
1719
+ };
1720
+ const { data, stale } = doSubscribe();
1721
+ subscriptionHasData.set(op.key, data !== null);
1722
+ if (data !== null) initialized = true;
1723
+ const teardown$ = pipe(ops$, filter((o) => o.variant === "teardown" && o.key === op.key), tap(() => {
1724
+ if (currentUnsubscribe) currentUnsubscribe();
1725
+ subscriptionHasData.delete(op.key);
1726
+ results.complete();
1727
+ }));
1728
+ const resubStream$ = pipe(resubscribe$.source, filter((key) => key === op.key), mergeMap(() => {
1729
+ doSubscribe();
1730
+ initialized = true;
1089
1731
  return empty();
1090
1732
  }));
1733
+ const stream$ = pipe(merge(data === null ? fetchPolicy === "cache-only" ? fromValue({
1734
+ operation: op,
1735
+ data: null,
1736
+ errors: []
1737
+ }) : empty() : fromValue({
1738
+ operation: op,
1739
+ data,
1740
+ ...stale && { metadata: { cache: { stale: true } } },
1741
+ errors: []
1742
+ }), results.source, resubStream$), takeUntil(teardown$));
1743
+ if (stale) refetch$.next(op);
1744
+ return stream$;
1091
1745
  }), filter(() => fetchPolicy === "cache-only" || fetchPolicy === "cache-and-network" || fetchPolicy === "cache-first")), pipe(merge(nonCache$, pipe(query$, filter((op) => {
1092
1746
  const { data } = cache.readQuery(op.artifact, op.variables);
1093
1747
  return fetchPolicy === "cache-and-network" || data === null;
@@ -1095,8 +1749,22 @@ const cacheExchange = (options = {}) => {
1095
1749
  if (result.operation.variant === "request" && result.operation.artifact.kind === "mutation" && result.operation.metadata?.cache?.optimisticResponse) cache.removeOptimistic(result.operation.key);
1096
1750
  if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
1097
1751
  if (result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0)) return fromValue(result);
1752
+ if (subscriptionHasData.get(result.operation.key)) {
1753
+ const { data } = cache.readQuery(result.operation.artifact, result.operation.variables);
1754
+ if (data !== null) return empty();
1755
+ return fromValue({
1756
+ operation: result.operation,
1757
+ data: void 0,
1758
+ errors: [new ExchangeError("Cache failed to denormalize the network response. This is likely a bug in the cache normalizer.", { exchangeName: "cache" })]
1759
+ });
1760
+ }
1761
+ subscriptionHasData.set(result.operation.key, true);
1762
+ resubscribe$.next(result.operation.key);
1098
1763
  const { data } = cache.readQuery(result.operation.artifact, result.operation.variables);
1099
- if (data !== null) return empty();
1764
+ if (data !== null) return fromValue({
1765
+ ...result,
1766
+ data
1767
+ });
1100
1768
  return fromValue({
1101
1769
  operation: result.operation,
1102
1770
  data: void 0,
@@ -1108,6 +1776,99 @@ const cacheExchange = (options = {}) => {
1108
1776
  };
1109
1777
  };
1110
1778
 
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
+
1111
1872
  //#endregion
1112
1873
  //#region src/exchanges/retry.ts
1113
1874
  const defaultShouldRetry = (error) => isExchangeError(error, "http") && error.extensions?.statusCode !== void 0 && error.extensions.statusCode >= 500;
@@ -1579,4 +2340,4 @@ const createClient = (config) => {
1579
2340
  };
1580
2341
 
1581
2342
  //#endregion
1582
- export { AggregatedError, Client, ExchangeError, GraphQLError, RequiredFieldError, cacheExchange, createClient, dedupExchange, fragmentExchange, httpExchange, isAggregatedError, isExchangeError, isGraphQLError, requiredExchange, retryExchange, stringify, subscriptionExchange };
2343
+ export { AggregatedError, Client, ExchangeError, GraphQLError, RequiredFieldError, applyPatchesImmutable, applyPatchesMutable, cacheExchange, createClient, dedupExchange, fragmentExchange, getPath, httpExchange, isAggregatedError, isExchangeError, isGraphQLError, requiredExchange, retryExchange, setPath, stringify, subscriptionExchange };