@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.cjs +988 -223
- package/dist/index.d.cts +53 -2
- package/dist/index.d.mts +53 -2
- package/dist/index.mjs +986 -225
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -360,15 +360,6 @@ const makeFieldKey = (selection, variables) => {
|
|
|
360
360
|
return `${selection.name}@${args}`;
|
|
361
361
|
};
|
|
362
362
|
/**
|
|
363
|
-
* Generates a unique key for tracking memoized denormalized results for structural sharing.
|
|
364
|
-
* @internal
|
|
365
|
-
* @param kind - The operation kind ('query', 'fragment', 'fragments').
|
|
366
|
-
* @param name - The artifact name.
|
|
367
|
-
* @param id - Serialized identifier (variables, entity key, etc.).
|
|
368
|
-
* @returns A unique memo key.
|
|
369
|
-
*/
|
|
370
|
-
const makeMemoKey = (kind, name, id) => `${kind}:${name}:${id}`;
|
|
371
|
-
/**
|
|
372
363
|
* Gets a unique key for tracking a field dependency.
|
|
373
364
|
* @internal
|
|
374
365
|
* @param storageKey Storage key (entity or root query key).
|
|
@@ -445,43 +436,6 @@ const isEqual = (a, b) => {
|
|
|
445
436
|
}
|
|
446
437
|
return false;
|
|
447
438
|
};
|
|
448
|
-
/**
|
|
449
|
-
* Recursively replaces a new value tree with the previous one wherever structurally equal,
|
|
450
|
-
* preserving referential identity for unchanged subtrees.
|
|
451
|
-
*
|
|
452
|
-
* Returns `prev` (same reference) when the entire subtree is structurally equal.
|
|
453
|
-
* @internal
|
|
454
|
-
*/
|
|
455
|
-
const replaceEqualDeep = (prev, next) => {
|
|
456
|
-
if (prev === next) return prev;
|
|
457
|
-
if (typeof prev !== typeof next || prev === null || next === null || typeof prev !== "object") return next;
|
|
458
|
-
if (Array.isArray(prev)) {
|
|
459
|
-
if (!Array.isArray(next)) return next;
|
|
460
|
-
let allSame = prev.length === next.length;
|
|
461
|
-
const result = [];
|
|
462
|
-
for (const [i, item] of next.entries()) {
|
|
463
|
-
const shared = i < prev.length ? replaceEqualDeep(prev[i], item) : item;
|
|
464
|
-
result.push(shared);
|
|
465
|
-
if (shared !== prev[i]) allSame = false;
|
|
466
|
-
}
|
|
467
|
-
return allSame ? prev : result;
|
|
468
|
-
}
|
|
469
|
-
if (Array.isArray(next)) return next;
|
|
470
|
-
const prevObj = prev;
|
|
471
|
-
const nextObj = next;
|
|
472
|
-
const nextKeys = Object.keys(nextObj);
|
|
473
|
-
const prevKeys = Object.keys(prevObj);
|
|
474
|
-
let allSame = nextKeys.length === prevKeys.length;
|
|
475
|
-
const result = {};
|
|
476
|
-
for (const key of nextKeys) if (key in prevObj) {
|
|
477
|
-
result[key] = replaceEqualDeep(prevObj[key], nextObj[key]);
|
|
478
|
-
if (result[key] !== prevObj[key]) allSame = false;
|
|
479
|
-
} else {
|
|
480
|
-
result[key] = nextObj[key];
|
|
481
|
-
allSame = false;
|
|
482
|
-
}
|
|
483
|
-
return allSame ? prev : result;
|
|
484
|
-
};
|
|
485
439
|
const NormalizedKey = Symbol("mearie.normalized");
|
|
486
440
|
/**
|
|
487
441
|
* Marks a record as a normalized cache object so that {@link mergeFields}
|
|
@@ -535,6 +489,48 @@ const mergeFields = (target, source, deep) => {
|
|
|
535
489
|
const makeFieldKeyFromArgs = (field, args) => {
|
|
536
490
|
return `${field}@${args && Object.keys(args).length > 0 ? stringify(args) : "{}"}`;
|
|
537
491
|
};
|
|
492
|
+
/**
|
|
493
|
+
* Type guard to check if a value is an array containing entity links.
|
|
494
|
+
* @internal
|
|
495
|
+
* @param value - Value to check.
|
|
496
|
+
* @returns True if the value is an array containing at least one entity link.
|
|
497
|
+
*/
|
|
498
|
+
const isEntityLinkArray = (value) => {
|
|
499
|
+
if (!Array.isArray(value) || value.length === 0) return false;
|
|
500
|
+
for (const item of value) {
|
|
501
|
+
if (item === null || item === void 0) continue;
|
|
502
|
+
if (typeof item === "object" && !Array.isArray(item) && EntityLinkKey in item) return true;
|
|
503
|
+
if (Array.isArray(item) && isEntityLinkArray(item)) return true;
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
};
|
|
508
|
+
/**
|
|
509
|
+
* Compares two entity link arrays by their entity keys.
|
|
510
|
+
* @internal
|
|
511
|
+
* @param a - First entity link array.
|
|
512
|
+
* @param b - Second entity link array.
|
|
513
|
+
* @returns True if both arrays have the same entity keys at each position.
|
|
514
|
+
*/
|
|
515
|
+
const isEntityLinkArrayEqual = (a, b) => {
|
|
516
|
+
if (a.length !== b.length) return false;
|
|
517
|
+
for (const [i, element] of a.entries()) if ((element?.[EntityLinkKey] ?? null) !== (b[i]?.[EntityLinkKey] ?? null)) return false;
|
|
518
|
+
return true;
|
|
519
|
+
};
|
|
520
|
+
/**
|
|
521
|
+
* Parses a dependency key into its storage key and field key components.
|
|
522
|
+
* @internal
|
|
523
|
+
* @param depKey - The dependency key to parse.
|
|
524
|
+
* @returns The storage key and field key.
|
|
525
|
+
*/
|
|
526
|
+
const parseDependencyKey = (depKey) => {
|
|
527
|
+
const atIdx = depKey.indexOf("@");
|
|
528
|
+
const dotIdx = depKey.lastIndexOf(".", atIdx);
|
|
529
|
+
return {
|
|
530
|
+
storageKey: depKey.slice(0, dotIdx),
|
|
531
|
+
fieldKey: depKey.slice(dotIdx + 1)
|
|
532
|
+
};
|
|
533
|
+
};
|
|
538
534
|
|
|
539
535
|
//#endregion
|
|
540
536
|
//#region src/cache/normalize.ts
|
|
@@ -598,59 +594,500 @@ const typenameFieldKey = makeFieldKey({
|
|
|
598
594
|
name: "__typename",
|
|
599
595
|
type: "String"
|
|
600
596
|
}, {});
|
|
601
|
-
const denormalize = (selections, storage, value, variables, accessor) => {
|
|
597
|
+
const denormalize = (selections, storage, value, variables, accessor, options) => {
|
|
602
598
|
let partial = false;
|
|
603
|
-
const denormalizeField = (storageKey, selections, value) => {
|
|
599
|
+
const denormalizeField = (storageKey, selections, value, path) => {
|
|
604
600
|
if (isNullish(value)) return value;
|
|
605
|
-
if (Array.isArray(value)) return value.map((item) => denormalizeField(storageKey, selections, item));
|
|
601
|
+
if (Array.isArray(value)) return value.map((item, i) => denormalizeField(storageKey, selections, item, [...path, i]));
|
|
606
602
|
const data = value;
|
|
607
603
|
if (isEntityLink(data)) {
|
|
608
604
|
const entityKey = data[EntityLinkKey];
|
|
609
605
|
const entity = storage[entityKey];
|
|
610
606
|
if (!entity) {
|
|
611
|
-
accessor?.(entityKey, typenameFieldKey);
|
|
607
|
+
accessor?.(entityKey, typenameFieldKey, path);
|
|
612
608
|
partial = true;
|
|
613
609
|
return null;
|
|
614
610
|
}
|
|
615
|
-
return denormalizeField(entityKey, selections, entity);
|
|
611
|
+
return denormalizeField(entityKey, selections, entity, path);
|
|
616
612
|
}
|
|
617
613
|
const fields = {};
|
|
618
614
|
for (const selection of selections) if (selection.kind === "Field") {
|
|
619
615
|
const fieldKey = makeFieldKey(selection, variables);
|
|
620
616
|
const fieldValue = data[fieldKey];
|
|
621
|
-
|
|
617
|
+
const fieldPath = [...path, selection.alias ?? selection.name];
|
|
618
|
+
if (storageKey !== null) accessor?.(storageKey, fieldKey, fieldPath, selection.selections);
|
|
622
619
|
if (fieldValue === void 0) {
|
|
623
620
|
partial = true;
|
|
624
621
|
continue;
|
|
625
622
|
}
|
|
626
623
|
const name = selection.alias ?? selection.name;
|
|
627
|
-
const
|
|
628
|
-
if (name in fields) mergeFields(fields, { [name]:
|
|
629
|
-
else fields[name] =
|
|
624
|
+
const resolvedValue = selection.selections ? denormalizeField(null, selection.selections, fieldValue, fieldPath) : fieldValue;
|
|
625
|
+
if (name in fields) mergeFields(fields, { [name]: resolvedValue }, true);
|
|
626
|
+
else fields[name] = resolvedValue;
|
|
630
627
|
} else if (selection.kind === "FragmentSpread") if (storageKey !== null && storageKey !== RootFieldKey) {
|
|
631
628
|
fields[FragmentRefKey] = storageKey;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
};
|
|
629
|
+
const merged = selection.args ? {
|
|
630
|
+
...variables,
|
|
631
|
+
...resolveArguments(selection.args, variables)
|
|
632
|
+
} : { ...variables };
|
|
633
|
+
fields[FragmentVarsKey] = {
|
|
634
|
+
...fields[FragmentVarsKey],
|
|
635
|
+
[selection.name]: merged
|
|
636
|
+
};
|
|
637
|
+
if (accessor) {
|
|
638
|
+
if (denormalize(selection.selections, storage, { [EntityLinkKey]: storageKey }, variables, options?.trackFragmentDeps === false ? void 0 : accessor, options).partial) partial = true;
|
|
639
|
+
}
|
|
640
|
+
} else if (storageKey === RootFieldKey) {
|
|
641
|
+
fields[FragmentRefKey] = RootFieldKey;
|
|
642
|
+
const merged = selection.args ? {
|
|
643
|
+
...variables,
|
|
644
|
+
...resolveArguments(selection.args, variables)
|
|
645
|
+
} : { ...variables };
|
|
646
|
+
fields[FragmentVarsKey] = {
|
|
647
|
+
...fields[FragmentVarsKey],
|
|
648
|
+
[selection.name]: merged
|
|
649
|
+
};
|
|
650
|
+
if (accessor) {
|
|
651
|
+
if (denormalize(selection.selections, storage, storage[RootFieldKey], variables, options?.trackFragmentDeps === false ? void 0 : accessor, options).partial) partial = true;
|
|
642
652
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value), true);
|
|
653
|
+
} else mergeFields(fields, denormalizeField(storageKey, selection.selections, value, path), true);
|
|
654
|
+
else if (selection.kind === "InlineFragment" && selection.on === data[typenameFieldKey]) mergeFields(fields, denormalizeField(storageKey, selection.selections, value, path), true);
|
|
646
655
|
return fields;
|
|
647
656
|
};
|
|
648
657
|
return {
|
|
649
|
-
data: denormalizeField(RootFieldKey, selections, value),
|
|
658
|
+
data: denormalizeField(RootFieldKey, selections, value, []),
|
|
650
659
|
partial
|
|
651
660
|
};
|
|
652
661
|
};
|
|
653
662
|
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/cache/tree.ts
|
|
665
|
+
/**
|
|
666
|
+
* @internal
|
|
667
|
+
*/
|
|
668
|
+
const buildEntryTree = (tuples, rootDepKey) => {
|
|
669
|
+
const root = {
|
|
670
|
+
depKey: rootDepKey ?? "__root",
|
|
671
|
+
children: /* @__PURE__ */ new Map()
|
|
672
|
+
};
|
|
673
|
+
for (const { storageKey, fieldKey, path, selections } of tuples) {
|
|
674
|
+
let current = root;
|
|
675
|
+
for (const element of path) {
|
|
676
|
+
const key = String(element);
|
|
677
|
+
let child = current.children.get(key);
|
|
678
|
+
if (!child) {
|
|
679
|
+
child = {
|
|
680
|
+
depKey: "",
|
|
681
|
+
children: /* @__PURE__ */ new Map()
|
|
682
|
+
};
|
|
683
|
+
current.children.set(key, child);
|
|
684
|
+
}
|
|
685
|
+
current = child;
|
|
686
|
+
}
|
|
687
|
+
current.depKey = makeDependencyKey(storageKey, fieldKey);
|
|
688
|
+
if (selections) current.selections = selections;
|
|
689
|
+
}
|
|
690
|
+
return root;
|
|
691
|
+
};
|
|
692
|
+
/**
|
|
693
|
+
* @internal
|
|
694
|
+
*/
|
|
695
|
+
const findEntryTreeNode = (root, path) => {
|
|
696
|
+
let current = root;
|
|
697
|
+
for (const segment of path) {
|
|
698
|
+
if (!current) return void 0;
|
|
699
|
+
current = current.children.get(String(segment));
|
|
700
|
+
}
|
|
701
|
+
return current;
|
|
702
|
+
};
|
|
703
|
+
/**
|
|
704
|
+
* Removes all subscription entries for a given subscription from the subtree rooted at {@link node},
|
|
705
|
+
* and clears the node's children map. Both the subscription entries and the tree structure
|
|
706
|
+
* are cleaned up atomically to avoid stale references.
|
|
707
|
+
* @internal
|
|
708
|
+
*/
|
|
709
|
+
const removeSubtreeEntries = (node, subscription, subscriptions) => {
|
|
710
|
+
const entries = subscriptions.get(node.depKey);
|
|
711
|
+
if (entries) {
|
|
712
|
+
for (const entry of entries) if (entry.subscription === subscription) {
|
|
713
|
+
entries.delete(entry);
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
if (entries.size === 0) subscriptions.delete(node.depKey);
|
|
717
|
+
}
|
|
718
|
+
for (const child of node.children.values()) removeSubtreeEntries(child, subscription, subscriptions);
|
|
719
|
+
node.children.clear();
|
|
720
|
+
};
|
|
721
|
+
/**
|
|
722
|
+
* @internal
|
|
723
|
+
*/
|
|
724
|
+
const snapshotFields = (node, storage) => {
|
|
725
|
+
const result = /* @__PURE__ */ new Map();
|
|
726
|
+
for (const [fieldName, child] of node.children) {
|
|
727
|
+
const { storageKey, fieldKey } = parseDependencyKey(child.depKey);
|
|
728
|
+
const fields = storage[storageKey];
|
|
729
|
+
if (fields) result.set(fieldName, fields[fieldKey]);
|
|
730
|
+
}
|
|
731
|
+
return result;
|
|
732
|
+
};
|
|
733
|
+
/**
|
|
734
|
+
* @internal
|
|
735
|
+
*/
|
|
736
|
+
const partialDenormalize = (node, entity, basePath, rebuiltDepKeys, storage, subscriptions, subscription) => {
|
|
737
|
+
if (!node.selections) return {
|
|
738
|
+
data: null,
|
|
739
|
+
fieldValues: /* @__PURE__ */ new Map()
|
|
740
|
+
};
|
|
741
|
+
const tuples = [];
|
|
742
|
+
const { data } = denormalize(node.selections, storage, entity, subscription.variables, (storageKey, fieldKey, path, sels) => {
|
|
743
|
+
tuples.push({
|
|
744
|
+
storageKey,
|
|
745
|
+
fieldKey,
|
|
746
|
+
path: [...basePath, ...path],
|
|
747
|
+
selections: sels
|
|
748
|
+
});
|
|
749
|
+
}, { trackFragmentDeps: false });
|
|
750
|
+
node.children.clear();
|
|
751
|
+
const fieldValues = /* @__PURE__ */ new Map();
|
|
752
|
+
for (const tuple of tuples) {
|
|
753
|
+
const depKey = makeDependencyKey(tuple.storageKey, tuple.fieldKey);
|
|
754
|
+
rebuiltDepKeys.add(depKey);
|
|
755
|
+
const relativePath = tuple.path.slice(basePath.length);
|
|
756
|
+
let current = node;
|
|
757
|
+
for (const element of relativePath) {
|
|
758
|
+
const key = String(element);
|
|
759
|
+
let child = current.children.get(key);
|
|
760
|
+
if (!child) {
|
|
761
|
+
child = {
|
|
762
|
+
depKey: "",
|
|
763
|
+
children: /* @__PURE__ */ new Map()
|
|
764
|
+
};
|
|
765
|
+
current.children.set(key, child);
|
|
766
|
+
}
|
|
767
|
+
current = child;
|
|
768
|
+
}
|
|
769
|
+
current.depKey = depKey;
|
|
770
|
+
if (tuple.selections) current.selections = tuple.selections;
|
|
771
|
+
const entry = {
|
|
772
|
+
path: tuple.path,
|
|
773
|
+
subscription
|
|
774
|
+
};
|
|
775
|
+
let entrySet = subscriptions.get(depKey);
|
|
776
|
+
if (!entrySet) {
|
|
777
|
+
entrySet = /* @__PURE__ */ new Set();
|
|
778
|
+
subscriptions.set(depKey, entrySet);
|
|
779
|
+
}
|
|
780
|
+
entrySet.add(entry);
|
|
781
|
+
if (relativePath.length === 1) {
|
|
782
|
+
const fieldName = String(relativePath[0]);
|
|
783
|
+
if (data && typeof data === "object") fieldValues.set(fieldName, data[fieldName]);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
data,
|
|
788
|
+
fieldValues
|
|
789
|
+
};
|
|
790
|
+
};
|
|
791
|
+
const updateSubtreePaths = (node, basePath, newIndex, baseLen, subscription, subscriptions) => {
|
|
792
|
+
const entries = subscriptions.get(node.depKey);
|
|
793
|
+
if (entries) {
|
|
794
|
+
for (const entry of entries) if (entry.subscription === subscription && entry.path.length > baseLen) entry.path = [
|
|
795
|
+
...basePath,
|
|
796
|
+
newIndex,
|
|
797
|
+
...entry.path.slice(baseLen + 1)
|
|
798
|
+
];
|
|
799
|
+
}
|
|
800
|
+
for (const child of node.children.values()) updateSubtreePaths(child, basePath, newIndex, baseLen, subscription, subscriptions);
|
|
801
|
+
};
|
|
802
|
+
/**
|
|
803
|
+
* @internal
|
|
804
|
+
*/
|
|
805
|
+
const rebuildArrayIndices = (node, entry, subscriptions) => {
|
|
806
|
+
const basePath = entry.path;
|
|
807
|
+
const baseLen = basePath.length;
|
|
808
|
+
const children = [...node.children.entries()].toSorted(([a], [b]) => Number(a) - Number(b));
|
|
809
|
+
node.children.clear();
|
|
810
|
+
for (const [newIdx, child_] of children.entries()) {
|
|
811
|
+
const [, child] = child_;
|
|
812
|
+
const newKey = String(newIdx);
|
|
813
|
+
node.children.set(newKey, child);
|
|
814
|
+
updateSubtreePaths(child, basePath, newIdx, baseLen, entry.subscription, subscriptions);
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
//#endregion
|
|
819
|
+
//#region src/cache/diff.ts
|
|
820
|
+
/**
|
|
821
|
+
* Finds the common prefix and suffix boundaries between two key arrays.
|
|
822
|
+
* @internal
|
|
823
|
+
*/
|
|
824
|
+
const findCommonBounds = (oldKeys, newKeys) => {
|
|
825
|
+
let start = 0;
|
|
826
|
+
while (start < oldKeys.length && start < newKeys.length && oldKeys[start] === newKeys[start]) start++;
|
|
827
|
+
let oldEnd = oldKeys.length;
|
|
828
|
+
let newEnd = newKeys.length;
|
|
829
|
+
while (oldEnd > start && newEnd > start && oldKeys[oldEnd - 1] === newKeys[newEnd - 1]) {
|
|
830
|
+
oldEnd--;
|
|
831
|
+
newEnd--;
|
|
832
|
+
}
|
|
833
|
+
return {
|
|
834
|
+
start,
|
|
835
|
+
oldEnd,
|
|
836
|
+
newEnd
|
|
837
|
+
};
|
|
838
|
+
};
|
|
839
|
+
/**
|
|
840
|
+
* Computes swap operations to reorder oldKeys into newKeys order using selection sort.
|
|
841
|
+
* @internal
|
|
842
|
+
*/
|
|
843
|
+
const computeSwaps = (oldKeys, newKeys) => {
|
|
844
|
+
const working = [...oldKeys];
|
|
845
|
+
const swaps = [];
|
|
846
|
+
for (const [i, newKey] of newKeys.entries()) {
|
|
847
|
+
if (working[i] === newKey) continue;
|
|
848
|
+
const j = working.indexOf(newKey, i + 1);
|
|
849
|
+
if (j === -1) continue;
|
|
850
|
+
[working[i], working[j]] = [working[j], working[i]];
|
|
851
|
+
swaps.push({
|
|
852
|
+
i,
|
|
853
|
+
j
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
return swaps;
|
|
857
|
+
};
|
|
858
|
+
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region src/cache/change.ts
|
|
861
|
+
/**
|
|
862
|
+
* @internal
|
|
863
|
+
*/
|
|
864
|
+
const classifyChanges = (changedKeys) => {
|
|
865
|
+
const structural = [];
|
|
866
|
+
const scalar = [];
|
|
867
|
+
for (const [depKey, { oldValue, newValue }] of changedKeys) {
|
|
868
|
+
if (isEntityLink(oldValue) && isEntityLink(newValue) && oldValue[EntityLinkKey] === newValue[EntityLinkKey]) continue;
|
|
869
|
+
if (isEntityLinkArray(oldValue) && isEntityLinkArray(newValue) && isEntityLinkArrayEqual(oldValue, newValue)) continue;
|
|
870
|
+
if (isEntityLink(oldValue) || isEntityLink(newValue) || isEntityLinkArray(oldValue) || isEntityLinkArray(newValue)) structural.push({
|
|
871
|
+
depKey,
|
|
872
|
+
oldValue,
|
|
873
|
+
newValue
|
|
874
|
+
});
|
|
875
|
+
else scalar.push({
|
|
876
|
+
depKey,
|
|
877
|
+
newValue
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
structural,
|
|
882
|
+
scalar
|
|
883
|
+
};
|
|
884
|
+
};
|
|
885
|
+
/**
|
|
886
|
+
* @internal
|
|
887
|
+
*/
|
|
888
|
+
const processStructuralChange = (entry, node, oldValue, newValue, rebuiltDepKeys, storage, subscriptions) => {
|
|
889
|
+
const patches = [];
|
|
890
|
+
if (isEntityLink(oldValue) || isEntityLink(newValue)) {
|
|
891
|
+
if (isNullish(newValue)) {
|
|
892
|
+
removeSubtreeEntries(node, entry.subscription, subscriptions);
|
|
893
|
+
patches.push({
|
|
894
|
+
type: "set",
|
|
895
|
+
path: entry.path,
|
|
896
|
+
value: null
|
|
897
|
+
});
|
|
898
|
+
return patches;
|
|
899
|
+
}
|
|
900
|
+
if (isNullish(oldValue)) {
|
|
901
|
+
const entity = storage[newValue[EntityLinkKey]];
|
|
902
|
+
if (entity) {
|
|
903
|
+
const { data } = partialDenormalize(node, entity, entry.path, rebuiltDepKeys, storage, subscriptions, entry.subscription);
|
|
904
|
+
patches.push({
|
|
905
|
+
type: "set",
|
|
906
|
+
path: entry.path,
|
|
907
|
+
value: data
|
|
908
|
+
});
|
|
909
|
+
} else patches.push({
|
|
910
|
+
type: "set",
|
|
911
|
+
path: entry.path,
|
|
912
|
+
value: null
|
|
913
|
+
});
|
|
914
|
+
return patches;
|
|
915
|
+
}
|
|
916
|
+
const oldFields = snapshotFields(node, storage);
|
|
917
|
+
removeSubtreeEntries(node, entry.subscription, subscriptions);
|
|
918
|
+
const newEntity = storage[newValue[EntityLinkKey]];
|
|
919
|
+
if (!newEntity) {
|
|
920
|
+
patches.push({
|
|
921
|
+
type: "set",
|
|
922
|
+
path: entry.path,
|
|
923
|
+
value: null
|
|
924
|
+
});
|
|
925
|
+
return patches;
|
|
926
|
+
}
|
|
927
|
+
const { fieldValues: newFields } = partialDenormalize(node, newEntity, entry.path, rebuiltDepKeys, storage, subscriptions, entry.subscription);
|
|
928
|
+
for (const [fieldName, newVal] of newFields) if (!isEqual(oldFields.get(fieldName), newVal)) patches.push({
|
|
929
|
+
type: "set",
|
|
930
|
+
path: [...entry.path, fieldName],
|
|
931
|
+
value: newVal
|
|
932
|
+
});
|
|
933
|
+
for (const [fieldName] of oldFields) if (!newFields.has(fieldName)) patches.push({
|
|
934
|
+
type: "set",
|
|
935
|
+
path: [...entry.path, fieldName],
|
|
936
|
+
value: null
|
|
937
|
+
});
|
|
938
|
+
return patches;
|
|
939
|
+
}
|
|
940
|
+
if (isEntityLinkArray(oldValue) || isEntityLinkArray(newValue)) {
|
|
941
|
+
const oldArr = Array.isArray(oldValue) ? oldValue : [];
|
|
942
|
+
const newArr = Array.isArray(newValue) ? newValue : [];
|
|
943
|
+
const oldKeys = oldArr.map((item) => item !== null && item !== void 0 && typeof item === "object" && EntityLinkKey in item ? item[EntityLinkKey] : null);
|
|
944
|
+
const newKeys = newArr.map((item) => item !== null && item !== void 0 && typeof item === "object" && EntityLinkKey in item ? item[EntityLinkKey] : null);
|
|
945
|
+
const { start, oldEnd, newEnd } = findCommonBounds(oldKeys, newKeys);
|
|
946
|
+
const oldMiddle = oldKeys.slice(start, oldEnd);
|
|
947
|
+
const newMiddle = newKeys.slice(start, newEnd);
|
|
948
|
+
const newMiddleSet = new Set(newMiddle.filter((k) => k !== null));
|
|
949
|
+
const oldMiddleSet = new Set(oldMiddle.filter((k) => k !== null));
|
|
950
|
+
const removedIndices = [];
|
|
951
|
+
for (let i = oldMiddle.length - 1; i >= 0; i--) {
|
|
952
|
+
const key = oldMiddle[i];
|
|
953
|
+
if (key !== null && !newMiddleSet.has(key)) removedIndices.push(start + i);
|
|
954
|
+
}
|
|
955
|
+
for (const idx of removedIndices) {
|
|
956
|
+
const childKey = String(idx);
|
|
957
|
+
const child = node.children.get(childKey);
|
|
958
|
+
if (child) {
|
|
959
|
+
removeSubtreeEntries(child, entry.subscription, subscriptions);
|
|
960
|
+
node.children.delete(childKey);
|
|
961
|
+
}
|
|
962
|
+
patches.push({
|
|
963
|
+
type: "splice",
|
|
964
|
+
path: entry.path,
|
|
965
|
+
index: idx,
|
|
966
|
+
deleteCount: 1,
|
|
967
|
+
items: []
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
compactChildren(node);
|
|
971
|
+
const retainedOld = oldMiddle.filter((k) => k !== null && newMiddleSet.has(k));
|
|
972
|
+
const retainedNew = newMiddle.filter((k) => k !== null && oldMiddleSet.has(k));
|
|
973
|
+
if (retainedOld.length > 0) {
|
|
974
|
+
const swaps = computeSwaps(retainedOld, retainedNew);
|
|
975
|
+
for (const { i, j } of swaps) {
|
|
976
|
+
const absI = start + i;
|
|
977
|
+
const absJ = start + j;
|
|
978
|
+
patches.push({
|
|
979
|
+
type: "swap",
|
|
980
|
+
path: entry.path,
|
|
981
|
+
i: absI,
|
|
982
|
+
j: absJ
|
|
983
|
+
});
|
|
984
|
+
const childI = node.children.get(String(absI));
|
|
985
|
+
const childJ = node.children.get(String(absJ));
|
|
986
|
+
if (childI && childJ) {
|
|
987
|
+
node.children.set(String(absI), childJ);
|
|
988
|
+
node.children.set(String(absJ), childI);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
const siblingSelections = findSiblingSelections(node);
|
|
993
|
+
const addedKeys = newMiddle.filter((k) => k !== null && !oldMiddleSet.has(k));
|
|
994
|
+
for (const key of addedKeys) {
|
|
995
|
+
const idx = start + newMiddle.indexOf(key);
|
|
996
|
+
shiftChildrenRight(node, idx);
|
|
997
|
+
const entity = storage[key];
|
|
998
|
+
const insertNode = {
|
|
999
|
+
depKey: "",
|
|
1000
|
+
children: /* @__PURE__ */ new Map(),
|
|
1001
|
+
...siblingSelections && { selections: siblingSelections }
|
|
1002
|
+
};
|
|
1003
|
+
if (entity) {
|
|
1004
|
+
const { data } = partialDenormalize(insertNode, entity, [...entry.path, idx], rebuiltDepKeys, storage, subscriptions, entry.subscription);
|
|
1005
|
+
node.children.set(String(idx), insertNode);
|
|
1006
|
+
patches.push({
|
|
1007
|
+
type: "splice",
|
|
1008
|
+
path: entry.path,
|
|
1009
|
+
index: idx,
|
|
1010
|
+
deleteCount: 0,
|
|
1011
|
+
items: [data]
|
|
1012
|
+
});
|
|
1013
|
+
} else {
|
|
1014
|
+
node.children.set(String(idx), insertNode);
|
|
1015
|
+
patches.push({
|
|
1016
|
+
type: "splice",
|
|
1017
|
+
path: entry.path,
|
|
1018
|
+
index: idx,
|
|
1019
|
+
deleteCount: 0,
|
|
1020
|
+
items: [null]
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
rebuildArrayIndices(node, entry, subscriptions);
|
|
1025
|
+
return patches;
|
|
1026
|
+
}
|
|
1027
|
+
return patches;
|
|
1028
|
+
};
|
|
1029
|
+
const compactChildren = (node) => {
|
|
1030
|
+
const sorted = [...node.children.entries()].toSorted(([a], [b]) => Number(a) - Number(b));
|
|
1031
|
+
node.children.clear();
|
|
1032
|
+
for (const [i, element] of sorted.entries()) node.children.set(String(i), element[1]);
|
|
1033
|
+
};
|
|
1034
|
+
const findSiblingSelections = (node) => {
|
|
1035
|
+
for (const child of node.children.values()) if (child.selections) return child.selections;
|
|
1036
|
+
return node.selections;
|
|
1037
|
+
};
|
|
1038
|
+
const shiftChildrenRight = (node, fromIndex) => {
|
|
1039
|
+
const entries = [...node.children.entries()].toSorted(([a], [b]) => Number(a) - Number(b));
|
|
1040
|
+
node.children.clear();
|
|
1041
|
+
for (const [key, child] of entries) {
|
|
1042
|
+
const idx = Number(key);
|
|
1043
|
+
if (idx >= fromIndex) node.children.set(String(idx + 1), child);
|
|
1044
|
+
else node.children.set(key, child);
|
|
1045
|
+
}
|
|
1046
|
+
};
|
|
1047
|
+
/**
|
|
1048
|
+
* @internal
|
|
1049
|
+
*/
|
|
1050
|
+
const generatePatches = (changedKeys, subscriptions, storage) => {
|
|
1051
|
+
const patchesBySubscription = /* @__PURE__ */ new Map();
|
|
1052
|
+
const rebuiltDepKeys = /* @__PURE__ */ new Set();
|
|
1053
|
+
const { structural, scalar } = classifyChanges(changedKeys);
|
|
1054
|
+
for (const { depKey, oldValue, newValue } of structural) {
|
|
1055
|
+
const entries = subscriptions.get(depKey);
|
|
1056
|
+
if (!entries) continue;
|
|
1057
|
+
for (const entry of entries) {
|
|
1058
|
+
const node = findEntryTreeNode(entry.subscription.entryTree, entry.path);
|
|
1059
|
+
if (!node) continue;
|
|
1060
|
+
const patches = processStructuralChange(entry, node, oldValue, newValue, rebuiltDepKeys, storage, subscriptions);
|
|
1061
|
+
if (patches.length > 0) {
|
|
1062
|
+
const existing = patchesBySubscription.get(entry.subscription) ?? [];
|
|
1063
|
+
existing.push(...patches);
|
|
1064
|
+
patchesBySubscription.set(entry.subscription, existing);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
for (const { depKey, newValue } of scalar) {
|
|
1069
|
+
if (rebuiltDepKeys.has(depKey)) continue;
|
|
1070
|
+
const entries = subscriptions.get(depKey);
|
|
1071
|
+
if (!entries) continue;
|
|
1072
|
+
for (const entry of entries) {
|
|
1073
|
+
let patchValue = newValue;
|
|
1074
|
+
const node = findEntryTreeNode(entry.subscription.entryTree, entry.path);
|
|
1075
|
+
if (node?.selections && isNormalizedRecord(newValue)) {
|
|
1076
|
+
const { data } = denormalize(node.selections, storage, newValue, entry.subscription.variables);
|
|
1077
|
+
patchValue = data;
|
|
1078
|
+
}
|
|
1079
|
+
const existing = patchesBySubscription.get(entry.subscription) ?? [];
|
|
1080
|
+
existing.push({
|
|
1081
|
+
type: "set",
|
|
1082
|
+
path: entry.path,
|
|
1083
|
+
value: patchValue
|
|
1084
|
+
});
|
|
1085
|
+
patchesBySubscription.set(entry.subscription, existing);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
return patchesBySubscription;
|
|
1089
|
+
};
|
|
1090
|
+
|
|
654
1091
|
//#endregion
|
|
655
1092
|
//#region src/cache/cache.ts
|
|
656
1093
|
/**
|
|
@@ -661,7 +1098,6 @@ var Cache = class {
|
|
|
661
1098
|
#schemaMeta;
|
|
662
1099
|
#storage = { [RootFieldKey]: {} };
|
|
663
1100
|
#subscriptions = /* @__PURE__ */ new Map();
|
|
664
|
-
#memo = /* @__PURE__ */ new Map();
|
|
665
1101
|
#stale = /* @__PURE__ */ new Set();
|
|
666
1102
|
#optimisticKeys = [];
|
|
667
1103
|
#optimisticLayers = /* @__PURE__ */ new Map();
|
|
@@ -696,22 +1132,35 @@ var Cache = class {
|
|
|
696
1132
|
*/
|
|
697
1133
|
writeOptimistic(key, artifact, variables, data) {
|
|
698
1134
|
const layerStorage = { [RootFieldKey]: {} };
|
|
699
|
-
const
|
|
1135
|
+
const layerDependencies = /* @__PURE__ */ new Set();
|
|
700
1136
|
normalize(this.#schemaMeta, artifact.selections, layerStorage, data, variables, (storageKey, fieldKey) => {
|
|
701
|
-
|
|
1137
|
+
layerDependencies.add(makeDependencyKey(storageKey, fieldKey));
|
|
702
1138
|
});
|
|
1139
|
+
const oldValues = /* @__PURE__ */ new Map();
|
|
1140
|
+
const currentView = this.#getStorageView();
|
|
1141
|
+
for (const depKey of layerDependencies) {
|
|
1142
|
+
const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
|
|
1143
|
+
oldValues.set(depKey, currentView[sk]?.[fk]);
|
|
1144
|
+
}
|
|
703
1145
|
this.#optimisticKeys.push(key);
|
|
704
1146
|
this.#optimisticLayers.set(key, {
|
|
705
1147
|
storage: layerStorage,
|
|
706
|
-
dependencies
|
|
1148
|
+
dependencies: layerDependencies
|
|
707
1149
|
});
|
|
708
1150
|
this.#storageView = null;
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1151
|
+
const newView = this.#getStorageView();
|
|
1152
|
+
const changedKeys = /* @__PURE__ */ new Map();
|
|
1153
|
+
for (const depKey of layerDependencies) {
|
|
1154
|
+
const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
|
|
1155
|
+
const newVal = newView[sk]?.[fk];
|
|
1156
|
+
const oldVal = oldValues.get(depKey);
|
|
1157
|
+
if (oldVal !== newVal) changedKeys.set(depKey, {
|
|
1158
|
+
oldValue: oldVal,
|
|
1159
|
+
newValue: newVal
|
|
1160
|
+
});
|
|
713
1161
|
}
|
|
714
|
-
|
|
1162
|
+
const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, newView);
|
|
1163
|
+
for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
|
|
715
1164
|
}
|
|
716
1165
|
/**
|
|
717
1166
|
* Removes an optimistic layer and notifies affected subscribers.
|
|
@@ -721,42 +1170,71 @@ var Cache = class {
|
|
|
721
1170
|
removeOptimistic(key) {
|
|
722
1171
|
const layer = this.#optimisticLayers.get(key);
|
|
723
1172
|
if (!layer) return;
|
|
1173
|
+
const currentView = this.#getStorageView();
|
|
1174
|
+
const oldValues = /* @__PURE__ */ new Map();
|
|
1175
|
+
for (const depKey of layer.dependencies) {
|
|
1176
|
+
const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
|
|
1177
|
+
oldValues.set(depKey, currentView[sk]?.[fk]);
|
|
1178
|
+
}
|
|
724
1179
|
this.#optimisticLayers.delete(key);
|
|
725
1180
|
this.#optimisticKeys = this.#optimisticKeys.filter((k) => k !== key);
|
|
726
1181
|
this.#storageView = null;
|
|
727
|
-
const
|
|
1182
|
+
const newView = this.#getStorageView();
|
|
1183
|
+
const changedKeys = /* @__PURE__ */ new Map();
|
|
728
1184
|
for (const depKey of layer.dependencies) {
|
|
729
|
-
const
|
|
730
|
-
|
|
1185
|
+
const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
|
|
1186
|
+
const newVal = newView[sk]?.[fk];
|
|
1187
|
+
const oldVal = oldValues.get(depKey);
|
|
1188
|
+
if (oldVal !== newVal) changedKeys.set(depKey, {
|
|
1189
|
+
oldValue: oldVal,
|
|
1190
|
+
newValue: newVal
|
|
1191
|
+
});
|
|
731
1192
|
}
|
|
732
|
-
|
|
1193
|
+
const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, newView);
|
|
1194
|
+
for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
|
|
733
1195
|
}
|
|
734
1196
|
/**
|
|
735
1197
|
* Writes a query result to the cache, normalizing entities.
|
|
1198
|
+
* In addition to field-level stale clearing, this also clears entity-level stale entries
|
|
1199
|
+
* (e.g., `"User:1"`) when any field of that entity is written, because {@link invalidate}
|
|
1200
|
+
* supports entity-level invalidation without specifying a field.
|
|
736
1201
|
* @param artifact - GraphQL document artifact.
|
|
737
1202
|
* @param variables - Query variables.
|
|
738
1203
|
* @param data - Query result data.
|
|
739
1204
|
*/
|
|
740
1205
|
writeQuery(artifact, variables, data) {
|
|
741
|
-
const
|
|
742
|
-
const
|
|
1206
|
+
const changedKeys = /* @__PURE__ */ new Map();
|
|
1207
|
+
const staleClearedKeys = /* @__PURE__ */ new Set();
|
|
743
1208
|
const entityStaleCleared = /* @__PURE__ */ new Set();
|
|
744
1209
|
normalize(this.#schemaMeta, artifact.selections, this.#storage, data, variables, (storageKey, fieldKey, oldValue, newValue) => {
|
|
745
1210
|
const depKey = makeDependencyKey(storageKey, fieldKey);
|
|
746
|
-
if (this.#stale.delete(depKey))
|
|
1211
|
+
if (this.#stale.delete(depKey)) staleClearedKeys.add(depKey);
|
|
747
1212
|
if (!entityStaleCleared.has(storageKey) && this.#stale.delete(storageKey)) entityStaleCleared.add(storageKey);
|
|
748
|
-
if (oldValue !== newValue)
|
|
1213
|
+
if (oldValue !== newValue) changedKeys.set(depKey, {
|
|
1214
|
+
oldValue,
|
|
1215
|
+
newValue
|
|
1216
|
+
});
|
|
749
1217
|
});
|
|
750
|
-
|
|
751
|
-
for (const
|
|
752
|
-
|
|
753
|
-
|
|
1218
|
+
const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, this.#storage);
|
|
1219
|
+
for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
|
|
1220
|
+
const staleOnlySubscriptions = /* @__PURE__ */ new Set();
|
|
1221
|
+
for (const depKey of staleClearedKeys) {
|
|
1222
|
+
if (changedKeys.has(depKey)) continue;
|
|
1223
|
+
const entries = this.#subscriptions.get(depKey);
|
|
1224
|
+
if (entries) {
|
|
1225
|
+
for (const entry of entries) if (!patchesBySubscription.has(entry.subscription)) staleOnlySubscriptions.add(entry.subscription);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
for (const entityKey of entityStaleCleared) {
|
|
1229
|
+
const prefix = `${entityKey}.`;
|
|
1230
|
+
for (const [depKey, entries] of this.#subscriptions) if (depKey.startsWith(prefix)) {
|
|
1231
|
+
for (const entry of entries) if (!patchesBySubscription.has(entry.subscription)) staleOnlySubscriptions.add(entry.subscription);
|
|
1232
|
+
}
|
|
754
1233
|
}
|
|
755
|
-
for (const subscription of
|
|
1234
|
+
for (const subscription of staleOnlySubscriptions) subscription.listener(null);
|
|
756
1235
|
}
|
|
757
1236
|
/**
|
|
758
1237
|
* Reads a query result from the cache, denormalizing entities if available.
|
|
759
|
-
* Uses structural sharing to preserve referential identity for unchanged subtrees.
|
|
760
1238
|
* @param artifact - GraphQL document artifact.
|
|
761
1239
|
* @param variables - Query variables.
|
|
762
1240
|
* @returns Denormalized query result or null if not found.
|
|
@@ -771,74 +1249,170 @@ var Cache = class {
|
|
|
771
1249
|
data: null,
|
|
772
1250
|
stale: false
|
|
773
1251
|
};
|
|
774
|
-
const key = makeMemoKey("query", artifact.name, stringify(variables));
|
|
775
|
-
const prev = this.#memo.get(key);
|
|
776
|
-
const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
|
|
777
|
-
this.#memo.set(key, result);
|
|
778
1252
|
return {
|
|
779
|
-
data
|
|
1253
|
+
data,
|
|
780
1254
|
stale
|
|
781
1255
|
};
|
|
782
1256
|
}
|
|
783
1257
|
/**
|
|
784
|
-
* Subscribes to cache
|
|
1258
|
+
* Subscribes to cache changes for a specific query.
|
|
785
1259
|
* @param artifact - GraphQL document artifact.
|
|
786
1260
|
* @param variables - Query variables.
|
|
787
|
-
* @param listener - Callback function to invoke on cache
|
|
788
|
-
* @returns
|
|
1261
|
+
* @param listener - Callback function to invoke on cache changes.
|
|
1262
|
+
* @returns Object containing initial data, stale status, unsubscribe function, and subscription.
|
|
789
1263
|
*/
|
|
790
1264
|
subscribeQuery(artifact, variables, listener) {
|
|
791
|
-
|
|
1265
|
+
let stale = false;
|
|
1266
|
+
const tuples = [];
|
|
792
1267
|
const storageView = this.#getStorageView();
|
|
793
|
-
denormalize(artifact.selections, storageView, storageView[RootFieldKey], variables, (storageKey, fieldKey) => {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1268
|
+
const { data, partial } = denormalize(artifact.selections, storageView, storageView[RootFieldKey], variables, (storageKey, fieldKey, path, selections) => {
|
|
1269
|
+
tuples.push({
|
|
1270
|
+
storageKey,
|
|
1271
|
+
fieldKey,
|
|
1272
|
+
path,
|
|
1273
|
+
selections
|
|
1274
|
+
});
|
|
1275
|
+
if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
|
|
1276
|
+
}, { trackFragmentDeps: false });
|
|
1277
|
+
const entryTree = buildEntryTree(tuples);
|
|
1278
|
+
const subscription = {
|
|
1279
|
+
listener,
|
|
1280
|
+
selections: artifact.selections,
|
|
1281
|
+
variables,
|
|
1282
|
+
entryTree
|
|
1283
|
+
};
|
|
1284
|
+
for (const tuple of tuples) {
|
|
1285
|
+
const depKey = makeDependencyKey(tuple.storageKey, tuple.fieldKey);
|
|
1286
|
+
const entry = {
|
|
1287
|
+
path: tuple.path,
|
|
1288
|
+
subscription
|
|
1289
|
+
};
|
|
1290
|
+
let entrySet = this.#subscriptions.get(depKey);
|
|
1291
|
+
if (!entrySet) {
|
|
1292
|
+
entrySet = /* @__PURE__ */ new Set();
|
|
1293
|
+
this.#subscriptions.set(depKey, entrySet);
|
|
1294
|
+
}
|
|
1295
|
+
entrySet.add(entry);
|
|
1296
|
+
}
|
|
1297
|
+
const unsubscribe = () => {
|
|
1298
|
+
this.#removeSubscriptionFromTree(entryTree, subscription);
|
|
1299
|
+
};
|
|
1300
|
+
return {
|
|
1301
|
+
data: partial ? null : data,
|
|
1302
|
+
stale,
|
|
1303
|
+
unsubscribe,
|
|
1304
|
+
subscription
|
|
1305
|
+
};
|
|
798
1306
|
}
|
|
799
1307
|
/**
|
|
800
1308
|
* Reads a fragment from the cache for a specific entity.
|
|
801
|
-
* Uses structural sharing to preserve referential identity for unchanged subtrees.
|
|
802
1309
|
* @param artifact - GraphQL fragment artifact.
|
|
803
1310
|
* @param fragmentRef - Fragment reference containing entity key.
|
|
804
1311
|
* @returns Denormalized fragment data or null if not found or invalid.
|
|
805
1312
|
*/
|
|
806
1313
|
readFragment(artifact, fragmentRef) {
|
|
807
|
-
const
|
|
1314
|
+
const storageKey = fragmentRef[FragmentRefKey];
|
|
808
1315
|
const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
|
|
809
1316
|
const storageView = this.#getStorageView();
|
|
810
|
-
|
|
1317
|
+
let stale = false;
|
|
1318
|
+
const value = storageView[storageKey];
|
|
1319
|
+
if (!value) return {
|
|
811
1320
|
data: null,
|
|
812
1321
|
stale: false
|
|
813
1322
|
};
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
|
|
1323
|
+
const { data, partial } = denormalize(artifact.selections, storageView, storageKey === RootFieldKey ? value : { [EntityLinkKey]: storageKey }, fragmentVars, (sk, fieldKey) => {
|
|
1324
|
+
if (this.#stale.has(sk) || this.#stale.has(makeDependencyKey(sk, fieldKey))) stale = true;
|
|
817
1325
|
});
|
|
818
1326
|
if (partial) return {
|
|
819
1327
|
data: null,
|
|
820
1328
|
stale: false
|
|
821
1329
|
};
|
|
822
|
-
const argsId = Object.keys(fragmentVars).length > 0 ? entityKey + stringify(fragmentVars) : entityKey;
|
|
823
|
-
const key = makeMemoKey("fragment", artifact.name, argsId);
|
|
824
|
-
const prev = this.#memo.get(key);
|
|
825
|
-
const result = prev === void 0 ? data : replaceEqualDeep(prev, data);
|
|
826
|
-
this.#memo.set(key, result);
|
|
827
1330
|
return {
|
|
828
|
-
data
|
|
1331
|
+
data,
|
|
829
1332
|
stale
|
|
830
1333
|
};
|
|
831
1334
|
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Subscribes to cache changes for a specific fragment.
|
|
1337
|
+
* @param artifact - GraphQL fragment artifact.
|
|
1338
|
+
* @param fragmentRef - Fragment reference containing entity key.
|
|
1339
|
+
* @param listener - Callback function to invoke on cache changes.
|
|
1340
|
+
* @returns Object containing initial data, stale status, unsubscribe function, and subscription.
|
|
1341
|
+
*/
|
|
832
1342
|
subscribeFragment(artifact, fragmentRef, listener) {
|
|
833
|
-
const
|
|
1343
|
+
const storageKey = fragmentRef[FragmentRefKey];
|
|
834
1344
|
const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
|
|
835
|
-
const dependencies = /* @__PURE__ */ new Set();
|
|
836
1345
|
const storageView = this.#getStorageView();
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1346
|
+
const value = storageKey === RootFieldKey ? storageView[RootFieldKey] : storageView[storageKey];
|
|
1347
|
+
if (!value) {
|
|
1348
|
+
const entryTree = buildEntryTree([]);
|
|
1349
|
+
return {
|
|
1350
|
+
data: null,
|
|
1351
|
+
stale: false,
|
|
1352
|
+
unsubscribe: () => {},
|
|
1353
|
+
subscription: {
|
|
1354
|
+
listener,
|
|
1355
|
+
selections: artifact.selections,
|
|
1356
|
+
variables: fragmentVars,
|
|
1357
|
+
entryTree
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
let stale = false;
|
|
1362
|
+
const tuples = [];
|
|
1363
|
+
const denormalizeValue = storageKey === RootFieldKey ? value : { [EntityLinkKey]: storageKey };
|
|
1364
|
+
const { data, partial } = denormalize(artifact.selections, storageView, denormalizeValue, fragmentVars, (sk, fieldKey, path, selections) => {
|
|
1365
|
+
tuples.push({
|
|
1366
|
+
storageKey: sk,
|
|
1367
|
+
fieldKey,
|
|
1368
|
+
path,
|
|
1369
|
+
selections
|
|
1370
|
+
});
|
|
1371
|
+
if (this.#stale.has(sk) || this.#stale.has(makeDependencyKey(sk, fieldKey))) stale = true;
|
|
1372
|
+
}, { trackFragmentDeps: false });
|
|
1373
|
+
if (partial) {
|
|
1374
|
+
const entryTree = buildEntryTree([]);
|
|
1375
|
+
return {
|
|
1376
|
+
data: null,
|
|
1377
|
+
stale: false,
|
|
1378
|
+
unsubscribe: () => {},
|
|
1379
|
+
subscription: {
|
|
1380
|
+
listener,
|
|
1381
|
+
selections: artifact.selections,
|
|
1382
|
+
variables: fragmentVars,
|
|
1383
|
+
entryTree
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
const entryTree = buildEntryTree(tuples, storageKey === RootFieldKey ? void 0 : storageKey);
|
|
1388
|
+
const subscription = {
|
|
1389
|
+
listener,
|
|
1390
|
+
selections: artifact.selections,
|
|
1391
|
+
variables: fragmentVars,
|
|
1392
|
+
entryTree
|
|
1393
|
+
};
|
|
1394
|
+
for (const tuple of tuples) {
|
|
1395
|
+
const depKey = makeDependencyKey(tuple.storageKey, tuple.fieldKey);
|
|
1396
|
+
const entry = {
|
|
1397
|
+
path: tuple.path,
|
|
1398
|
+
subscription
|
|
1399
|
+
};
|
|
1400
|
+
let entrySet = this.#subscriptions.get(depKey);
|
|
1401
|
+
if (!entrySet) {
|
|
1402
|
+
entrySet = /* @__PURE__ */ new Set();
|
|
1403
|
+
this.#subscriptions.set(depKey, entrySet);
|
|
1404
|
+
}
|
|
1405
|
+
entrySet.add(entry);
|
|
1406
|
+
}
|
|
1407
|
+
const unsubscribe = () => {
|
|
1408
|
+
this.#removeSubscriptionFromTree(entryTree, subscription);
|
|
1409
|
+
};
|
|
1410
|
+
return {
|
|
1411
|
+
data: partial ? null : data,
|
|
1412
|
+
stale,
|
|
1413
|
+
unsubscribe,
|
|
1414
|
+
subscription
|
|
1415
|
+
};
|
|
842
1416
|
}
|
|
843
1417
|
readFragments(artifact, fragmentRefs) {
|
|
844
1418
|
const results = [];
|
|
@@ -852,42 +1426,35 @@ var Cache = class {
|
|
|
852
1426
|
if (result.stale) stale = true;
|
|
853
1427
|
results.push(result.data);
|
|
854
1428
|
}
|
|
855
|
-
const entityKeys = fragmentRefs.map((ref) => ref[FragmentRefKey]);
|
|
856
|
-
const key = makeMemoKey("fragments", artifact.name, entityKeys.join(","));
|
|
857
|
-
const prev = this.#memo.get(key);
|
|
858
|
-
const result = prev === void 0 ? results : replaceEqualDeep(prev, results);
|
|
859
|
-
this.#memo.set(key, result);
|
|
860
1429
|
return {
|
|
861
|
-
data:
|
|
1430
|
+
data: results,
|
|
862
1431
|
stale
|
|
863
1432
|
};
|
|
864
1433
|
}
|
|
865
1434
|
subscribeFragments(artifact, fragmentRefs, listener) {
|
|
866
|
-
const
|
|
867
|
-
const storageView = this.#getStorageView();
|
|
1435
|
+
const unsubscribes = [];
|
|
868
1436
|
for (const ref of fragmentRefs) {
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
|
|
872
|
-
dependencies.add(makeDependencyKey(storageKey, fieldKey));
|
|
873
|
-
});
|
|
1437
|
+
const { unsubscribe } = this.subscribeFragment(artifact, ref, listener);
|
|
1438
|
+
unsubscribes.push(unsubscribe);
|
|
874
1439
|
}
|
|
875
|
-
return
|
|
1440
|
+
return () => {
|
|
1441
|
+
for (const unsub of unsubscribes) unsub();
|
|
1442
|
+
};
|
|
876
1443
|
}
|
|
877
1444
|
/**
|
|
878
1445
|
* Invalidates one or more cache entries and notifies affected subscribers.
|
|
879
1446
|
* @param targets - Cache entries to invalidate.
|
|
880
1447
|
*/
|
|
881
1448
|
invalidate(...targets) {
|
|
882
|
-
const
|
|
1449
|
+
const affectedSubscriptions = /* @__PURE__ */ new Set();
|
|
883
1450
|
for (const target of targets) if (target.__typename === "Query") if ("$field" in target) {
|
|
884
1451
|
const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
|
|
885
1452
|
const depKey = makeDependencyKey(RootFieldKey, fieldKey);
|
|
886
1453
|
this.#stale.add(depKey);
|
|
887
|
-
this.#collectSubscriptions(RootFieldKey, fieldKey,
|
|
1454
|
+
this.#collectSubscriptions(RootFieldKey, fieldKey, affectedSubscriptions);
|
|
888
1455
|
} else {
|
|
889
1456
|
this.#stale.add(RootFieldKey);
|
|
890
|
-
this.#collectSubscriptions(RootFieldKey, void 0,
|
|
1457
|
+
this.#collectSubscriptions(RootFieldKey, void 0, affectedSubscriptions);
|
|
891
1458
|
}
|
|
892
1459
|
else {
|
|
893
1460
|
const keyFields = this.#schemaMeta.entities[target.__typename]?.keyFields;
|
|
@@ -897,10 +1464,10 @@ var Cache = class {
|
|
|
897
1464
|
if ("$field" in target) {
|
|
898
1465
|
const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
|
|
899
1466
|
this.#stale.add(makeDependencyKey(entityKey, fieldKey));
|
|
900
|
-
this.#collectSubscriptions(entityKey, fieldKey,
|
|
1467
|
+
this.#collectSubscriptions(entityKey, fieldKey, affectedSubscriptions);
|
|
901
1468
|
} else {
|
|
902
1469
|
this.#stale.add(entityKey);
|
|
903
|
-
this.#collectSubscriptions(entityKey, void 0,
|
|
1470
|
+
this.#collectSubscriptions(entityKey, void 0, affectedSubscriptions);
|
|
904
1471
|
}
|
|
905
1472
|
} else {
|
|
906
1473
|
const prefix = `${target.__typename}:`;
|
|
@@ -909,15 +1476,30 @@ var Cache = class {
|
|
|
909
1476
|
if ("$field" in target) {
|
|
910
1477
|
const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
|
|
911
1478
|
this.#stale.add(makeDependencyKey(entityKey, fieldKey));
|
|
912
|
-
this.#collectSubscriptions(entityKey, fieldKey,
|
|
1479
|
+
this.#collectSubscriptions(entityKey, fieldKey, affectedSubscriptions);
|
|
913
1480
|
} else {
|
|
914
1481
|
this.#stale.add(entityKey);
|
|
915
|
-
this.#collectSubscriptions(entityKey, void 0,
|
|
1482
|
+
this.#collectSubscriptions(entityKey, void 0, affectedSubscriptions);
|
|
916
1483
|
}
|
|
917
1484
|
}
|
|
918
1485
|
}
|
|
919
1486
|
}
|
|
920
|
-
for (const subscription of
|
|
1487
|
+
for (const subscription of affectedSubscriptions) subscription.listener(null);
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Checks if a subscription has stale data.
|
|
1491
|
+
* @internal
|
|
1492
|
+
*/
|
|
1493
|
+
isStale(subscription) {
|
|
1494
|
+
const check = (node) => {
|
|
1495
|
+
if (node.depKey.includes("@")) {
|
|
1496
|
+
const { storageKey } = parseDependencyKey(node.depKey);
|
|
1497
|
+
if (this.#stale.has(storageKey) || this.#stale.has(node.depKey)) return true;
|
|
1498
|
+
}
|
|
1499
|
+
for (const child of node.children.values()) if (check(child)) return true;
|
|
1500
|
+
return false;
|
|
1501
|
+
};
|
|
1502
|
+
return check(subscription.entryTree);
|
|
921
1503
|
}
|
|
922
1504
|
#hasKeyFields(target, keyFields) {
|
|
923
1505
|
return keyFields.every((f) => f in target);
|
|
@@ -925,48 +1507,43 @@ var Cache = class {
|
|
|
925
1507
|
#collectSubscriptions(storageKey, fieldKey, out) {
|
|
926
1508
|
if (fieldKey === void 0) {
|
|
927
1509
|
const prefix = `${storageKey}.`;
|
|
928
|
-
for (const [depKey,
|
|
1510
|
+
for (const [depKey, entries] of this.#subscriptions) if (depKey.startsWith(prefix)) for (const entry of entries) out.add(entry.subscription);
|
|
929
1511
|
} else {
|
|
930
1512
|
const depKey = makeDependencyKey(storageKey, fieldKey);
|
|
931
|
-
const
|
|
932
|
-
if (
|
|
1513
|
+
const entries = this.#subscriptions.get(depKey);
|
|
1514
|
+
if (entries) for (const entry of entries) out.add(entry.subscription);
|
|
933
1515
|
}
|
|
934
1516
|
}
|
|
935
|
-
#
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
const
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
}
|
|
942
|
-
return () => {
|
|
943
|
-
for (const dependency of dependencies) {
|
|
944
|
-
const subscriptions = this.#subscriptions.get(dependency);
|
|
945
|
-
subscriptions?.delete(subscription);
|
|
946
|
-
if (subscriptions?.size === 0) this.#subscriptions.delete(dependency);
|
|
1517
|
+
#removeSubscriptionFromTree(node, subscription) {
|
|
1518
|
+
const entries = this.#subscriptions.get(node.depKey);
|
|
1519
|
+
if (entries) {
|
|
1520
|
+
for (const entry of entries) if (entry.subscription === subscription) {
|
|
1521
|
+
entries.delete(entry);
|
|
1522
|
+
break;
|
|
947
1523
|
}
|
|
948
|
-
|
|
1524
|
+
if (entries.size === 0) this.#subscriptions.delete(node.depKey);
|
|
1525
|
+
}
|
|
1526
|
+
for (const child of node.children.values()) this.#removeSubscriptionFromTree(child, subscription);
|
|
1527
|
+
}
|
|
1528
|
+
#parseDepKey(depKey) {
|
|
1529
|
+
return parseDependencyKey(depKey);
|
|
949
1530
|
}
|
|
950
1531
|
/**
|
|
951
|
-
* Extracts a serializable snapshot of the cache storage
|
|
1532
|
+
* Extracts a serializable snapshot of the cache storage.
|
|
952
1533
|
* Optimistic layers are excluded because they represent transient in-flight state.
|
|
953
1534
|
*/
|
|
954
1535
|
extract() {
|
|
955
|
-
return {
|
|
956
|
-
storage: structuredClone(this.#storage),
|
|
957
|
-
memo: Object.fromEntries(this.#memo)
|
|
958
|
-
};
|
|
1536
|
+
return { storage: structuredClone(this.#storage) };
|
|
959
1537
|
}
|
|
960
1538
|
/**
|
|
961
1539
|
* Hydrates the cache with a previously extracted snapshot.
|
|
962
1540
|
*/
|
|
963
1541
|
hydrate(snapshot) {
|
|
964
|
-
const { storage
|
|
1542
|
+
const { storage } = snapshot;
|
|
965
1543
|
for (const [key, fields] of Object.entries(storage)) this.#storage[key] = {
|
|
966
1544
|
...this.#storage[key],
|
|
967
1545
|
...fields
|
|
968
1546
|
};
|
|
969
|
-
for (const [key, value] of Object.entries(memo)) this.#memo.set(key, value);
|
|
970
1547
|
this.#storageView = null;
|
|
971
1548
|
}
|
|
972
1549
|
/**
|
|
@@ -975,7 +1552,6 @@ var Cache = class {
|
|
|
975
1552
|
clear() {
|
|
976
1553
|
this.#storage = { [RootFieldKey]: {} };
|
|
977
1554
|
this.#subscriptions.clear();
|
|
978
|
-
this.#memo.clear();
|
|
979
1555
|
this.#stale.clear();
|
|
980
1556
|
this.#optimisticKeys = [];
|
|
981
1557
|
this.#optimisticLayers.clear();
|
|
@@ -1011,6 +1587,9 @@ const cacheExchange = (options = {}) => {
|
|
|
1011
1587
|
clear: () => cache.clear()
|
|
1012
1588
|
},
|
|
1013
1589
|
io: (ops$) => {
|
|
1590
|
+
const subscriptionHasData = /* @__PURE__ */ new Map();
|
|
1591
|
+
const resubscribe$ = require_make.makeSubject();
|
|
1592
|
+
const refetch$ = require_make.makeSubject();
|
|
1014
1593
|
const fragment$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), require_make.mergeMap((op) => {
|
|
1015
1594
|
const fragmentRef = op.metadata?.fragment?.ref;
|
|
1016
1595
|
if (!fragmentRef) return require_make.fromValue({
|
|
@@ -1018,77 +1597,152 @@ const cacheExchange = (options = {}) => {
|
|
|
1018
1597
|
errors: [new ExchangeError("Fragment operation missing fragment.ref in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
|
|
1019
1598
|
});
|
|
1020
1599
|
if (isFragmentRefArray(fragmentRef)) {
|
|
1021
|
-
const
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1600
|
+
const results = require_make.makeSubject();
|
|
1601
|
+
const unsubscribes = [];
|
|
1602
|
+
const fragmentSubscriptions = [];
|
|
1603
|
+
for (const [index, ref] of fragmentRef.entries()) {
|
|
1604
|
+
const patchListener = (patches) => {
|
|
1605
|
+
if (patches) {
|
|
1606
|
+
const indexedPatches = patches.map((patch) => ({
|
|
1607
|
+
...patch,
|
|
1608
|
+
path: [index, ...patch.path]
|
|
1609
|
+
}));
|
|
1610
|
+
results.next({
|
|
1611
|
+
operation: op,
|
|
1612
|
+
metadata: { cache: { patches: indexedPatches } },
|
|
1613
|
+
errors: []
|
|
1614
|
+
});
|
|
1615
|
+
} else {
|
|
1616
|
+
const sub = fragmentSubscriptions[index];
|
|
1617
|
+
if (sub && cache.isStale(sub)) {
|
|
1618
|
+
const { data, stale } = cache.readFragments(op.artifact, fragmentRef);
|
|
1619
|
+
if (data !== null) results.next({
|
|
1620
|
+
operation: op,
|
|
1621
|
+
data,
|
|
1622
|
+
...stale && { metadata: { cache: { stale: true } } },
|
|
1623
|
+
errors: []
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
};
|
|
1628
|
+
const { unsubscribe, subscription } = cache.subscribeFragment(op.artifact, ref, patchListener);
|
|
1629
|
+
unsubscribes.push(unsubscribe);
|
|
1630
|
+
fragmentSubscriptions.push(subscription);
|
|
1631
|
+
}
|
|
1632
|
+
const { data: initialData, stale: initialStale } = cache.readFragments(op.artifact, fragmentRef);
|
|
1633
|
+
const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => {
|
|
1634
|
+
for (const unsub of unsubscribes) unsub();
|
|
1635
|
+
results.complete();
|
|
1636
|
+
}));
|
|
1637
|
+
return require_make.pipe(require_make.merge(require_make.fromValue({
|
|
1027
1638
|
operation: op,
|
|
1028
|
-
data,
|
|
1029
|
-
...
|
|
1639
|
+
data: initialData,
|
|
1640
|
+
...initialStale && { metadata: { cache: { stale: true } } },
|
|
1030
1641
|
errors: []
|
|
1031
|
-
})));
|
|
1642
|
+
}), results.source), require_make.takeUntil(teardown$));
|
|
1032
1643
|
}
|
|
1033
1644
|
if (!isFragmentRef(fragmentRef)) return require_make.fromValue({
|
|
1034
1645
|
operation: op,
|
|
1035
1646
|
data: fragmentRef,
|
|
1036
1647
|
errors: []
|
|
1037
1648
|
});
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1649
|
+
const results = require_make.makeSubject();
|
|
1650
|
+
let currentUnsubscribe = null;
|
|
1651
|
+
let currentSubscription = null;
|
|
1652
|
+
const patchListener = (patches) => {
|
|
1653
|
+
if (patches) results.next({
|
|
1654
|
+
operation: op,
|
|
1655
|
+
metadata: { cache: { patches } },
|
|
1656
|
+
errors: []
|
|
1657
|
+
});
|
|
1658
|
+
else if (currentSubscription) {
|
|
1659
|
+
if (cache.isStale(currentSubscription)) {
|
|
1660
|
+
const { data: staleData } = cache.readFragment(op.artifact, fragmentRef);
|
|
1661
|
+
if (staleData !== null) results.next({
|
|
1662
|
+
operation: op,
|
|
1663
|
+
data: staleData,
|
|
1664
|
+
metadata: { cache: { stale: true } },
|
|
1665
|
+
errors: []
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
const { data, stale, unsubscribe, subscription } = cache.subscribeFragment(op.artifact, fragmentRef, patchListener);
|
|
1671
|
+
currentUnsubscribe = unsubscribe;
|
|
1672
|
+
currentSubscription = subscription;
|
|
1673
|
+
const teardown$ = require_make.pipe(ops$, require_make.filter((operation) => operation.variant === "teardown" && operation.key === op.key), require_make.tap(() => {
|
|
1674
|
+
if (currentUnsubscribe) currentUnsubscribe();
|
|
1675
|
+
results.complete();
|
|
1676
|
+
}));
|
|
1677
|
+
return require_make.pipe(require_make.merge(data === null ? empty() : require_make.fromValue({
|
|
1044
1678
|
operation: op,
|
|
1045
1679
|
data,
|
|
1046
1680
|
...stale && { metadata: { cache: { stale: true } } },
|
|
1047
1681
|
errors: []
|
|
1048
|
-
})));
|
|
1682
|
+
}), results.source), require_make.takeUntil(teardown$));
|
|
1049
1683
|
}));
|
|
1050
1684
|
const nonCache$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && (op.artifact.kind === "mutation" || op.artifact.kind === "subscription" || op.artifact.kind === "query" && fetchPolicy === "network-only")), require_make.tap((op) => {
|
|
1051
1685
|
if (op.artifact.kind === "mutation" && op.metadata?.cache?.optimisticResponse) cache.writeOptimistic(op.key, op.artifact, op.variables, op.metadata.cache.optimisticResponse);
|
|
1052
1686
|
}));
|
|
1053
1687
|
const query$ = require_make.pipe(ops$, require_make.filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), require_make.share());
|
|
1054
|
-
const refetch$ = require_make.makeSubject();
|
|
1055
1688
|
return require_make.merge(fragment$, require_make.pipe(query$, require_make.mergeMap((op) => {
|
|
1056
|
-
const
|
|
1057
|
-
let
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1689
|
+
const results = require_make.makeSubject();
|
|
1690
|
+
let currentUnsubscribe = null;
|
|
1691
|
+
let currentSubscription = null;
|
|
1692
|
+
let initialized = false;
|
|
1693
|
+
const doSubscribe = () => {
|
|
1694
|
+
if (currentUnsubscribe) currentUnsubscribe();
|
|
1695
|
+
const patchListener = (patches) => {
|
|
1696
|
+
if (patches) {
|
|
1697
|
+
if (!initialized) return;
|
|
1698
|
+
results.next({
|
|
1699
|
+
operation: op,
|
|
1700
|
+
metadata: { cache: { patches } },
|
|
1701
|
+
errors: []
|
|
1702
|
+
});
|
|
1703
|
+
} else if (currentSubscription) {
|
|
1704
|
+
if (cache.isStale(currentSubscription)) {
|
|
1705
|
+
const { data: staleData } = cache.readQuery(op.artifact, op.variables);
|
|
1706
|
+
if (staleData !== null) results.next({
|
|
1707
|
+
operation: op,
|
|
1708
|
+
data: staleData,
|
|
1709
|
+
metadata: { cache: { stale: true } },
|
|
1710
|
+
errors: []
|
|
1711
|
+
});
|
|
1712
|
+
refetch$.next(op);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
const result = cache.subscribeQuery(op.artifact, op.variables, patchListener);
|
|
1717
|
+
currentUnsubscribe = result.unsubscribe;
|
|
1718
|
+
currentSubscription = result.subscription;
|
|
1719
|
+
return result;
|
|
1720
|
+
};
|
|
1721
|
+
const { data, stale } = doSubscribe();
|
|
1722
|
+
subscriptionHasData.set(op.key, data !== null);
|
|
1723
|
+
if (data !== null) initialized = true;
|
|
1724
|
+
const teardown$ = require_make.pipe(ops$, require_make.filter((o) => o.variant === "teardown" && o.key === op.key), require_make.tap(() => {
|
|
1725
|
+
if (currentUnsubscribe) currentUnsubscribe();
|
|
1726
|
+
subscriptionHasData.delete(op.key);
|
|
1727
|
+
results.complete();
|
|
1728
|
+
}));
|
|
1729
|
+
const resubStream$ = require_make.pipe(resubscribe$.source, require_make.filter((key) => key === op.key), require_make.mergeMap(() => {
|
|
1730
|
+
doSubscribe();
|
|
1731
|
+
initialized = true;
|
|
1090
1732
|
return empty();
|
|
1091
1733
|
}));
|
|
1734
|
+
const stream$ = require_make.pipe(require_make.merge(data === null ? fetchPolicy === "cache-only" ? require_make.fromValue({
|
|
1735
|
+
operation: op,
|
|
1736
|
+
data: null,
|
|
1737
|
+
errors: []
|
|
1738
|
+
}) : empty() : require_make.fromValue({
|
|
1739
|
+
operation: op,
|
|
1740
|
+
data,
|
|
1741
|
+
...stale && { metadata: { cache: { stale: true } } },
|
|
1742
|
+
errors: []
|
|
1743
|
+
}), results.source, resubStream$), require_make.takeUntil(teardown$));
|
|
1744
|
+
if (stale) refetch$.next(op);
|
|
1745
|
+
return stream$;
|
|
1092
1746
|
}), require_make.filter(() => fetchPolicy === "cache-only" || fetchPolicy === "cache-and-network" || fetchPolicy === "cache-first")), require_make.pipe(require_make.merge(nonCache$, require_make.pipe(query$, require_make.filter((op) => {
|
|
1093
1747
|
const { data } = cache.readQuery(op.artifact, op.variables);
|
|
1094
1748
|
return fetchPolicy === "cache-and-network" || data === null;
|
|
@@ -1096,8 +1750,22 @@ const cacheExchange = (options = {}) => {
|
|
|
1096
1750
|
if (result.operation.variant === "request" && result.operation.artifact.kind === "mutation" && result.operation.metadata?.cache?.optimisticResponse) cache.removeOptimistic(result.operation.key);
|
|
1097
1751
|
if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
|
|
1098
1752
|
if (result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0)) return require_make.fromValue(result);
|
|
1753
|
+
if (subscriptionHasData.get(result.operation.key)) {
|
|
1754
|
+
const { data } = cache.readQuery(result.operation.artifact, result.operation.variables);
|
|
1755
|
+
if (data !== null) return empty();
|
|
1756
|
+
return require_make.fromValue({
|
|
1757
|
+
operation: result.operation,
|
|
1758
|
+
data: void 0,
|
|
1759
|
+
errors: [new ExchangeError("Cache failed to denormalize the network response. This is likely a bug in the cache normalizer.", { exchangeName: "cache" })]
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
subscriptionHasData.set(result.operation.key, true);
|
|
1763
|
+
resubscribe$.next(result.operation.key);
|
|
1099
1764
|
const { data } = cache.readQuery(result.operation.artifact, result.operation.variables);
|
|
1100
|
-
if (data !== null) return
|
|
1765
|
+
if (data !== null) return require_make.fromValue({
|
|
1766
|
+
...result,
|
|
1767
|
+
data
|
|
1768
|
+
});
|
|
1101
1769
|
return require_make.fromValue({
|
|
1102
1770
|
operation: result.operation,
|
|
1103
1771
|
data: void 0,
|
|
@@ -1109,6 +1777,99 @@ const cacheExchange = (options = {}) => {
|
|
|
1109
1777
|
};
|
|
1110
1778
|
};
|
|
1111
1779
|
|
|
1780
|
+
//#endregion
|
|
1781
|
+
//#region src/cache/patch.ts
|
|
1782
|
+
const copyNode = (node) => Array.isArray(node) ? [...node] : { ...node };
|
|
1783
|
+
const shallowCopyPath = (root, path) => {
|
|
1784
|
+
if (path.length === 0) return root;
|
|
1785
|
+
let result = copyNode(root);
|
|
1786
|
+
const top = result;
|
|
1787
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1788
|
+
const key = path[i];
|
|
1789
|
+
result[key] = copyNode(result[key]);
|
|
1790
|
+
result = result[key];
|
|
1791
|
+
}
|
|
1792
|
+
return top;
|
|
1793
|
+
};
|
|
1794
|
+
/**
|
|
1795
|
+
* Sets a value at a nested path within an object.
|
|
1796
|
+
* @param obj - The object to modify.
|
|
1797
|
+
* @param path - The path to the target location.
|
|
1798
|
+
* @param value - The value to set.
|
|
1799
|
+
*/
|
|
1800
|
+
const setPath = (obj, path, value) => {
|
|
1801
|
+
let current = obj;
|
|
1802
|
+
for (let i = 0; i < path.length - 1; i++) current = current[path[i]];
|
|
1803
|
+
current[path.at(-1)] = value;
|
|
1804
|
+
};
|
|
1805
|
+
/**
|
|
1806
|
+
* Gets a value at a nested path within an object.
|
|
1807
|
+
* @param obj - The object to read from.
|
|
1808
|
+
* @param path - The path to the target location.
|
|
1809
|
+
* @returns The value at the path, or the object itself if path is empty.
|
|
1810
|
+
*/
|
|
1811
|
+
const getPath = (obj, path) => {
|
|
1812
|
+
let current = obj;
|
|
1813
|
+
for (const segment of path) {
|
|
1814
|
+
if (current === void 0 || current === null) return void 0;
|
|
1815
|
+
current = current[segment];
|
|
1816
|
+
}
|
|
1817
|
+
return current;
|
|
1818
|
+
};
|
|
1819
|
+
/**
|
|
1820
|
+
* Applies cache patches to data immutably, shallow-copying only along changed paths.
|
|
1821
|
+
*/
|
|
1822
|
+
const applyPatchesImmutable = (data, patches) => {
|
|
1823
|
+
if (patches.length === 0) return data;
|
|
1824
|
+
let result = data;
|
|
1825
|
+
for (const patch of patches) if (patch.type === "set") {
|
|
1826
|
+
if (patch.path.length === 0) {
|
|
1827
|
+
result = patch.value;
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
result = shallowCopyPath(result, patch.path);
|
|
1831
|
+
let target = result;
|
|
1832
|
+
for (let i = 0; i < patch.path.length - 1; i++) target = target[patch.path[i]];
|
|
1833
|
+
target[patch.path.at(-1)] = patch.value;
|
|
1834
|
+
} else if (patch.type === "splice") {
|
|
1835
|
+
result = shallowCopyPath(result, patch.path);
|
|
1836
|
+
let target = result;
|
|
1837
|
+
for (const segment of patch.path) target = target[segment];
|
|
1838
|
+
const arr = [...target];
|
|
1839
|
+
arr.splice(patch.index, patch.deleteCount, ...patch.items);
|
|
1840
|
+
let parent = result;
|
|
1841
|
+
for (let i = 0; i < patch.path.length - 1; i++) parent = parent[patch.path[i]];
|
|
1842
|
+
parent[patch.path.at(-1)] = arr;
|
|
1843
|
+
} else if (patch.type === "swap") {
|
|
1844
|
+
result = shallowCopyPath(result, patch.path);
|
|
1845
|
+
let target = result;
|
|
1846
|
+
for (const segment of patch.path) target = target[segment];
|
|
1847
|
+
const arr = [...target];
|
|
1848
|
+
[arr[patch.i], arr[patch.j]] = [arr[patch.j], arr[patch.i]];
|
|
1849
|
+
let parent = result;
|
|
1850
|
+
for (let i = 0; i < patch.path.length - 1; i++) parent = parent[patch.path[i]];
|
|
1851
|
+
parent[patch.path.at(-1)] = arr;
|
|
1852
|
+
}
|
|
1853
|
+
return result;
|
|
1854
|
+
};
|
|
1855
|
+
/**
|
|
1856
|
+
* Applies cache patches to a mutable target object in place.
|
|
1857
|
+
* @param target - The mutable object to apply patches to.
|
|
1858
|
+
* @param patches - The patches to apply.
|
|
1859
|
+
* @returns The new root value if a root-level set patch was applied, otherwise undefined.
|
|
1860
|
+
*/
|
|
1861
|
+
const applyPatchesMutable = (target, patches) => {
|
|
1862
|
+
let root;
|
|
1863
|
+
for (const patch of patches) if (patch.type === "set") if (patch.path.length === 0) root = patch.value;
|
|
1864
|
+
else setPath(target, patch.path, patch.value);
|
|
1865
|
+
else if (patch.type === "splice") getPath(target, patch.path).splice(patch.index, patch.deleteCount, ...patch.items);
|
|
1866
|
+
else if (patch.type === "swap") {
|
|
1867
|
+
const arr = getPath(target, patch.path);
|
|
1868
|
+
[arr[patch.i], arr[patch.j]] = [arr[patch.j], arr[patch.i]];
|
|
1869
|
+
}
|
|
1870
|
+
return root;
|
|
1871
|
+
};
|
|
1872
|
+
|
|
1112
1873
|
//#endregion
|
|
1113
1874
|
//#region src/exchanges/retry.ts
|
|
1114
1875
|
const defaultShouldRetry = (error) => isExchangeError(error, "http") && error.extensions?.statusCode !== void 0 && error.extensions.statusCode >= 500;
|
|
@@ -1585,15 +2346,19 @@ exports.Client = Client;
|
|
|
1585
2346
|
exports.ExchangeError = ExchangeError;
|
|
1586
2347
|
exports.GraphQLError = GraphQLError;
|
|
1587
2348
|
exports.RequiredFieldError = RequiredFieldError;
|
|
2349
|
+
exports.applyPatchesImmutable = applyPatchesImmutable;
|
|
2350
|
+
exports.applyPatchesMutable = applyPatchesMutable;
|
|
1588
2351
|
exports.cacheExchange = cacheExchange;
|
|
1589
2352
|
exports.createClient = createClient;
|
|
1590
2353
|
exports.dedupExchange = dedupExchange;
|
|
1591
2354
|
exports.fragmentExchange = fragmentExchange;
|
|
2355
|
+
exports.getPath = getPath;
|
|
1592
2356
|
exports.httpExchange = httpExchange;
|
|
1593
2357
|
exports.isAggregatedError = isAggregatedError;
|
|
1594
2358
|
exports.isExchangeError = isExchangeError;
|
|
1595
2359
|
exports.isGraphQLError = isGraphQLError;
|
|
1596
2360
|
exports.requiredExchange = requiredExchange;
|
|
1597
2361
|
exports.retryExchange = retryExchange;
|
|
2362
|
+
exports.setPath = setPath;
|
|
1598
2363
|
exports.stringify = stringify;
|
|
1599
2364
|
exports.subscriptionExchange = subscriptionExchange;
|