@mearie/core 0.5.2 → 0.6.0
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 +982 -223
- package/dist/index.d.cts +53 -2
- package/dist/index.d.mts +53 -2
- package/dist/index.mjs +980 -225
- package/package.json +1 -1
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,
|
|
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,494 @@ 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
|
-
|
|
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
|
|
627
|
-
if (name in fields) mergeFields(fields, { [name]:
|
|
628
|
-
else fields[name] =
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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
|
-
|
|
643
|
-
|
|
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
|
+
const existing = patchesBySubscription.get(entry.subscription) ?? [];
|
|
1073
|
+
existing.push({
|
|
1074
|
+
type: "set",
|
|
1075
|
+
path: entry.path,
|
|
1076
|
+
value: newValue
|
|
1077
|
+
});
|
|
1078
|
+
patchesBySubscription.set(entry.subscription, existing);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return patchesBySubscription;
|
|
1082
|
+
};
|
|
1083
|
+
|
|
653
1084
|
//#endregion
|
|
654
1085
|
//#region src/cache/cache.ts
|
|
655
1086
|
/**
|
|
@@ -660,7 +1091,6 @@ var Cache = class {
|
|
|
660
1091
|
#schemaMeta;
|
|
661
1092
|
#storage = { [RootFieldKey]: {} };
|
|
662
1093
|
#subscriptions = /* @__PURE__ */ new Map();
|
|
663
|
-
#memo = /* @__PURE__ */ new Map();
|
|
664
1094
|
#stale = /* @__PURE__ */ new Set();
|
|
665
1095
|
#optimisticKeys = [];
|
|
666
1096
|
#optimisticLayers = /* @__PURE__ */ new Map();
|
|
@@ -695,22 +1125,35 @@ var Cache = class {
|
|
|
695
1125
|
*/
|
|
696
1126
|
writeOptimistic(key, artifact, variables, data) {
|
|
697
1127
|
const layerStorage = { [RootFieldKey]: {} };
|
|
698
|
-
const
|
|
1128
|
+
const layerDependencies = /* @__PURE__ */ new Set();
|
|
699
1129
|
normalize(this.#schemaMeta, artifact.selections, layerStorage, data, variables, (storageKey, fieldKey) => {
|
|
700
|
-
|
|
1130
|
+
layerDependencies.add(makeDependencyKey(storageKey, fieldKey));
|
|
701
1131
|
});
|
|
1132
|
+
const oldValues = /* @__PURE__ */ new Map();
|
|
1133
|
+
const currentView = this.#getStorageView();
|
|
1134
|
+
for (const depKey of layerDependencies) {
|
|
1135
|
+
const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
|
|
1136
|
+
oldValues.set(depKey, currentView[sk]?.[fk]);
|
|
1137
|
+
}
|
|
702
1138
|
this.#optimisticKeys.push(key);
|
|
703
1139
|
this.#optimisticLayers.set(key, {
|
|
704
1140
|
storage: layerStorage,
|
|
705
|
-
dependencies
|
|
1141
|
+
dependencies: layerDependencies
|
|
706
1142
|
});
|
|
707
1143
|
this.#storageView = null;
|
|
708
|
-
const
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1144
|
+
const newView = this.#getStorageView();
|
|
1145
|
+
const changedKeys = /* @__PURE__ */ new Map();
|
|
1146
|
+
for (const depKey of layerDependencies) {
|
|
1147
|
+
const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
|
|
1148
|
+
const newVal = newView[sk]?.[fk];
|
|
1149
|
+
const oldVal = oldValues.get(depKey);
|
|
1150
|
+
if (oldVal !== newVal) changedKeys.set(depKey, {
|
|
1151
|
+
oldValue: oldVal,
|
|
1152
|
+
newValue: newVal
|
|
1153
|
+
});
|
|
712
1154
|
}
|
|
713
|
-
|
|
1155
|
+
const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, newView);
|
|
1156
|
+
for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
|
|
714
1157
|
}
|
|
715
1158
|
/**
|
|
716
1159
|
* Removes an optimistic layer and notifies affected subscribers.
|
|
@@ -720,42 +1163,71 @@ var Cache = class {
|
|
|
720
1163
|
removeOptimistic(key) {
|
|
721
1164
|
const layer = this.#optimisticLayers.get(key);
|
|
722
1165
|
if (!layer) return;
|
|
1166
|
+
const currentView = this.#getStorageView();
|
|
1167
|
+
const oldValues = /* @__PURE__ */ new Map();
|
|
1168
|
+
for (const depKey of layer.dependencies) {
|
|
1169
|
+
const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
|
|
1170
|
+
oldValues.set(depKey, currentView[sk]?.[fk]);
|
|
1171
|
+
}
|
|
723
1172
|
this.#optimisticLayers.delete(key);
|
|
724
1173
|
this.#optimisticKeys = this.#optimisticKeys.filter((k) => k !== key);
|
|
725
1174
|
this.#storageView = null;
|
|
726
|
-
const
|
|
1175
|
+
const newView = this.#getStorageView();
|
|
1176
|
+
const changedKeys = /* @__PURE__ */ new Map();
|
|
727
1177
|
for (const depKey of layer.dependencies) {
|
|
728
|
-
const
|
|
729
|
-
|
|
1178
|
+
const { storageKey: sk, fieldKey: fk } = this.#parseDepKey(depKey);
|
|
1179
|
+
const newVal = newView[sk]?.[fk];
|
|
1180
|
+
const oldVal = oldValues.get(depKey);
|
|
1181
|
+
if (oldVal !== newVal) changedKeys.set(depKey, {
|
|
1182
|
+
oldValue: oldVal,
|
|
1183
|
+
newValue: newVal
|
|
1184
|
+
});
|
|
730
1185
|
}
|
|
731
|
-
|
|
1186
|
+
const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, newView);
|
|
1187
|
+
for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
|
|
732
1188
|
}
|
|
733
1189
|
/**
|
|
734
1190
|
* Writes a query result to the cache, normalizing entities.
|
|
1191
|
+
* In addition to field-level stale clearing, this also clears entity-level stale entries
|
|
1192
|
+
* (e.g., `"User:1"`) when any field of that entity is written, because {@link invalidate}
|
|
1193
|
+
* supports entity-level invalidation without specifying a field.
|
|
735
1194
|
* @param artifact - GraphQL document artifact.
|
|
736
1195
|
* @param variables - Query variables.
|
|
737
1196
|
* @param data - Query result data.
|
|
738
1197
|
*/
|
|
739
1198
|
writeQuery(artifact, variables, data) {
|
|
740
|
-
const
|
|
741
|
-
const
|
|
1199
|
+
const changedKeys = /* @__PURE__ */ new Map();
|
|
1200
|
+
const staleClearedKeys = /* @__PURE__ */ new Set();
|
|
742
1201
|
const entityStaleCleared = /* @__PURE__ */ new Set();
|
|
743
1202
|
normalize(this.#schemaMeta, artifact.selections, this.#storage, data, variables, (storageKey, fieldKey, oldValue, newValue) => {
|
|
744
1203
|
const depKey = makeDependencyKey(storageKey, fieldKey);
|
|
745
|
-
if (this.#stale.delete(depKey))
|
|
1204
|
+
if (this.#stale.delete(depKey)) staleClearedKeys.add(depKey);
|
|
746
1205
|
if (!entityStaleCleared.has(storageKey) && this.#stale.delete(storageKey)) entityStaleCleared.add(storageKey);
|
|
747
|
-
if (oldValue !== newValue)
|
|
1206
|
+
if (oldValue !== newValue) changedKeys.set(depKey, {
|
|
1207
|
+
oldValue,
|
|
1208
|
+
newValue
|
|
1209
|
+
});
|
|
748
1210
|
});
|
|
749
|
-
|
|
750
|
-
for (const
|
|
751
|
-
|
|
752
|
-
|
|
1211
|
+
const patchesBySubscription = generatePatches(changedKeys, this.#subscriptions, this.#storage);
|
|
1212
|
+
for (const [subscription, patches] of patchesBySubscription) subscription.listener(patches);
|
|
1213
|
+
const staleOnlySubscriptions = /* @__PURE__ */ new Set();
|
|
1214
|
+
for (const depKey of staleClearedKeys) {
|
|
1215
|
+
if (changedKeys.has(depKey)) continue;
|
|
1216
|
+
const entries = this.#subscriptions.get(depKey);
|
|
1217
|
+
if (entries) {
|
|
1218
|
+
for (const entry of entries) if (!patchesBySubscription.has(entry.subscription)) staleOnlySubscriptions.add(entry.subscription);
|
|
1219
|
+
}
|
|
753
1220
|
}
|
|
754
|
-
for (const
|
|
1221
|
+
for (const entityKey of entityStaleCleared) {
|
|
1222
|
+
const prefix = `${entityKey}.`;
|
|
1223
|
+
for (const [depKey, entries] of this.#subscriptions) if (depKey.startsWith(prefix)) {
|
|
1224
|
+
for (const entry of entries) if (!patchesBySubscription.has(entry.subscription)) staleOnlySubscriptions.add(entry.subscription);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
for (const subscription of staleOnlySubscriptions) subscription.listener(null);
|
|
755
1228
|
}
|
|
756
1229
|
/**
|
|
757
1230
|
* Reads a query result from the cache, denormalizing entities if available.
|
|
758
|
-
* Uses structural sharing to preserve referential identity for unchanged subtrees.
|
|
759
1231
|
* @param artifact - GraphQL document artifact.
|
|
760
1232
|
* @param variables - Query variables.
|
|
761
1233
|
* @returns Denormalized query result or null if not found.
|
|
@@ -770,74 +1242,170 @@ var Cache = class {
|
|
|
770
1242
|
data: null,
|
|
771
1243
|
stale: false
|
|
772
1244
|
};
|
|
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
1245
|
return {
|
|
778
|
-
data
|
|
1246
|
+
data,
|
|
779
1247
|
stale
|
|
780
1248
|
};
|
|
781
1249
|
}
|
|
782
1250
|
/**
|
|
783
|
-
* Subscribes to cache
|
|
1251
|
+
* Subscribes to cache changes for a specific query.
|
|
784
1252
|
* @param artifact - GraphQL document artifact.
|
|
785
1253
|
* @param variables - Query variables.
|
|
786
|
-
* @param listener - Callback function to invoke on cache
|
|
787
|
-
* @returns
|
|
1254
|
+
* @param listener - Callback function to invoke on cache changes.
|
|
1255
|
+
* @returns Object containing initial data, stale status, unsubscribe function, and subscription.
|
|
788
1256
|
*/
|
|
789
1257
|
subscribeQuery(artifact, variables, listener) {
|
|
790
|
-
|
|
1258
|
+
let stale = false;
|
|
1259
|
+
const tuples = [];
|
|
791
1260
|
const storageView = this.#getStorageView();
|
|
792
|
-
denormalize(artifact.selections, storageView, storageView[RootFieldKey], variables, (storageKey, fieldKey) => {
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1261
|
+
const { data, partial } = denormalize(artifact.selections, storageView, storageView[RootFieldKey], variables, (storageKey, fieldKey, path, selections) => {
|
|
1262
|
+
tuples.push({
|
|
1263
|
+
storageKey,
|
|
1264
|
+
fieldKey,
|
|
1265
|
+
path,
|
|
1266
|
+
selections
|
|
1267
|
+
});
|
|
1268
|
+
if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
|
|
1269
|
+
}, { trackFragmentDeps: false });
|
|
1270
|
+
const entryTree = buildEntryTree(tuples);
|
|
1271
|
+
const subscription = {
|
|
1272
|
+
listener,
|
|
1273
|
+
selections: artifact.selections,
|
|
1274
|
+
variables,
|
|
1275
|
+
entryTree
|
|
1276
|
+
};
|
|
1277
|
+
for (const tuple of tuples) {
|
|
1278
|
+
const depKey = makeDependencyKey(tuple.storageKey, tuple.fieldKey);
|
|
1279
|
+
const entry = {
|
|
1280
|
+
path: tuple.path,
|
|
1281
|
+
subscription
|
|
1282
|
+
};
|
|
1283
|
+
let entrySet = this.#subscriptions.get(depKey);
|
|
1284
|
+
if (!entrySet) {
|
|
1285
|
+
entrySet = /* @__PURE__ */ new Set();
|
|
1286
|
+
this.#subscriptions.set(depKey, entrySet);
|
|
1287
|
+
}
|
|
1288
|
+
entrySet.add(entry);
|
|
1289
|
+
}
|
|
1290
|
+
const unsubscribe = () => {
|
|
1291
|
+
this.#removeSubscriptionFromTree(entryTree, subscription);
|
|
1292
|
+
};
|
|
1293
|
+
return {
|
|
1294
|
+
data: partial ? null : data,
|
|
1295
|
+
stale,
|
|
1296
|
+
unsubscribe,
|
|
1297
|
+
subscription
|
|
1298
|
+
};
|
|
797
1299
|
}
|
|
798
1300
|
/**
|
|
799
1301
|
* Reads a fragment from the cache for a specific entity.
|
|
800
|
-
* Uses structural sharing to preserve referential identity for unchanged subtrees.
|
|
801
1302
|
* @param artifact - GraphQL fragment artifact.
|
|
802
1303
|
* @param fragmentRef - Fragment reference containing entity key.
|
|
803
1304
|
* @returns Denormalized fragment data or null if not found or invalid.
|
|
804
1305
|
*/
|
|
805
1306
|
readFragment(artifact, fragmentRef) {
|
|
806
|
-
const
|
|
1307
|
+
const storageKey = fragmentRef[FragmentRefKey];
|
|
807
1308
|
const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
|
|
808
1309
|
const storageView = this.#getStorageView();
|
|
809
|
-
|
|
1310
|
+
let stale = false;
|
|
1311
|
+
const value = storageView[storageKey];
|
|
1312
|
+
if (!value) return {
|
|
810
1313
|
data: null,
|
|
811
1314
|
stale: false
|
|
812
1315
|
};
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
if (this.#stale.has(storageKey) || this.#stale.has(makeDependencyKey(storageKey, fieldKey))) stale = true;
|
|
1316
|
+
const { data, partial } = denormalize(artifact.selections, storageView, storageKey === RootFieldKey ? value : { [EntityLinkKey]: storageKey }, fragmentVars, (sk, fieldKey) => {
|
|
1317
|
+
if (this.#stale.has(sk) || this.#stale.has(makeDependencyKey(sk, fieldKey))) stale = true;
|
|
816
1318
|
});
|
|
817
1319
|
if (partial) return {
|
|
818
1320
|
data: null,
|
|
819
1321
|
stale: false
|
|
820
1322
|
};
|
|
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
1323
|
return {
|
|
827
|
-
data
|
|
1324
|
+
data,
|
|
828
1325
|
stale
|
|
829
1326
|
};
|
|
830
1327
|
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Subscribes to cache changes for a specific fragment.
|
|
1330
|
+
* @param artifact - GraphQL fragment artifact.
|
|
1331
|
+
* @param fragmentRef - Fragment reference containing entity key.
|
|
1332
|
+
* @param listener - Callback function to invoke on cache changes.
|
|
1333
|
+
* @returns Object containing initial data, stale status, unsubscribe function, and subscription.
|
|
1334
|
+
*/
|
|
831
1335
|
subscribeFragment(artifact, fragmentRef, listener) {
|
|
832
|
-
const
|
|
1336
|
+
const storageKey = fragmentRef[FragmentRefKey];
|
|
833
1337
|
const fragmentVars = getFragmentVars(fragmentRef, artifact.name);
|
|
834
|
-
const dependencies = /* @__PURE__ */ new Set();
|
|
835
1338
|
const storageView = this.#getStorageView();
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1339
|
+
const value = storageKey === RootFieldKey ? storageView[RootFieldKey] : storageView[storageKey];
|
|
1340
|
+
if (!value) {
|
|
1341
|
+
const entryTree = buildEntryTree([]);
|
|
1342
|
+
return {
|
|
1343
|
+
data: null,
|
|
1344
|
+
stale: false,
|
|
1345
|
+
unsubscribe: () => {},
|
|
1346
|
+
subscription: {
|
|
1347
|
+
listener,
|
|
1348
|
+
selections: artifact.selections,
|
|
1349
|
+
variables: fragmentVars,
|
|
1350
|
+
entryTree
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
let stale = false;
|
|
1355
|
+
const tuples = [];
|
|
1356
|
+
const denormalizeValue = storageKey === RootFieldKey ? value : { [EntityLinkKey]: storageKey };
|
|
1357
|
+
const { data, partial } = denormalize(artifact.selections, storageView, denormalizeValue, fragmentVars, (sk, fieldKey, path, selections) => {
|
|
1358
|
+
tuples.push({
|
|
1359
|
+
storageKey: sk,
|
|
1360
|
+
fieldKey,
|
|
1361
|
+
path,
|
|
1362
|
+
selections
|
|
1363
|
+
});
|
|
1364
|
+
if (this.#stale.has(sk) || this.#stale.has(makeDependencyKey(sk, fieldKey))) stale = true;
|
|
1365
|
+
}, { trackFragmentDeps: false });
|
|
1366
|
+
if (partial) {
|
|
1367
|
+
const entryTree = buildEntryTree([]);
|
|
1368
|
+
return {
|
|
1369
|
+
data: null,
|
|
1370
|
+
stale: false,
|
|
1371
|
+
unsubscribe: () => {},
|
|
1372
|
+
subscription: {
|
|
1373
|
+
listener,
|
|
1374
|
+
selections: artifact.selections,
|
|
1375
|
+
variables: fragmentVars,
|
|
1376
|
+
entryTree
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
const entryTree = buildEntryTree(tuples, storageKey === RootFieldKey ? void 0 : storageKey);
|
|
1381
|
+
const subscription = {
|
|
1382
|
+
listener,
|
|
1383
|
+
selections: artifact.selections,
|
|
1384
|
+
variables: fragmentVars,
|
|
1385
|
+
entryTree
|
|
1386
|
+
};
|
|
1387
|
+
for (const tuple of tuples) {
|
|
1388
|
+
const depKey = makeDependencyKey(tuple.storageKey, tuple.fieldKey);
|
|
1389
|
+
const entry = {
|
|
1390
|
+
path: tuple.path,
|
|
1391
|
+
subscription
|
|
1392
|
+
};
|
|
1393
|
+
let entrySet = this.#subscriptions.get(depKey);
|
|
1394
|
+
if (!entrySet) {
|
|
1395
|
+
entrySet = /* @__PURE__ */ new Set();
|
|
1396
|
+
this.#subscriptions.set(depKey, entrySet);
|
|
1397
|
+
}
|
|
1398
|
+
entrySet.add(entry);
|
|
1399
|
+
}
|
|
1400
|
+
const unsubscribe = () => {
|
|
1401
|
+
this.#removeSubscriptionFromTree(entryTree, subscription);
|
|
1402
|
+
};
|
|
1403
|
+
return {
|
|
1404
|
+
data: partial ? null : data,
|
|
1405
|
+
stale,
|
|
1406
|
+
unsubscribe,
|
|
1407
|
+
subscription
|
|
1408
|
+
};
|
|
841
1409
|
}
|
|
842
1410
|
readFragments(artifact, fragmentRefs) {
|
|
843
1411
|
const results = [];
|
|
@@ -851,42 +1419,35 @@ var Cache = class {
|
|
|
851
1419
|
if (result.stale) stale = true;
|
|
852
1420
|
results.push(result.data);
|
|
853
1421
|
}
|
|
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
1422
|
return {
|
|
860
|
-
data:
|
|
1423
|
+
data: results,
|
|
861
1424
|
stale
|
|
862
1425
|
};
|
|
863
1426
|
}
|
|
864
1427
|
subscribeFragments(artifact, fragmentRefs, listener) {
|
|
865
|
-
const
|
|
866
|
-
const storageView = this.#getStorageView();
|
|
1428
|
+
const unsubscribes = [];
|
|
867
1429
|
for (const ref of fragmentRefs) {
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
denormalize(artifact.selections, storageView, { [EntityLinkKey]: entityKey }, fragmentVars, (storageKey, fieldKey) => {
|
|
871
|
-
dependencies.add(makeDependencyKey(storageKey, fieldKey));
|
|
872
|
-
});
|
|
1430
|
+
const { unsubscribe } = this.subscribeFragment(artifact, ref, listener);
|
|
1431
|
+
unsubscribes.push(unsubscribe);
|
|
873
1432
|
}
|
|
874
|
-
return
|
|
1433
|
+
return () => {
|
|
1434
|
+
for (const unsub of unsubscribes) unsub();
|
|
1435
|
+
};
|
|
875
1436
|
}
|
|
876
1437
|
/**
|
|
877
1438
|
* Invalidates one or more cache entries and notifies affected subscribers.
|
|
878
1439
|
* @param targets - Cache entries to invalidate.
|
|
879
1440
|
*/
|
|
880
1441
|
invalidate(...targets) {
|
|
881
|
-
const
|
|
1442
|
+
const affectedSubscriptions = /* @__PURE__ */ new Set();
|
|
882
1443
|
for (const target of targets) if (target.__typename === "Query") if ("$field" in target) {
|
|
883
1444
|
const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
|
|
884
1445
|
const depKey = makeDependencyKey(RootFieldKey, fieldKey);
|
|
885
1446
|
this.#stale.add(depKey);
|
|
886
|
-
this.#collectSubscriptions(RootFieldKey, fieldKey,
|
|
1447
|
+
this.#collectSubscriptions(RootFieldKey, fieldKey, affectedSubscriptions);
|
|
887
1448
|
} else {
|
|
888
1449
|
this.#stale.add(RootFieldKey);
|
|
889
|
-
this.#collectSubscriptions(RootFieldKey, void 0,
|
|
1450
|
+
this.#collectSubscriptions(RootFieldKey, void 0, affectedSubscriptions);
|
|
890
1451
|
}
|
|
891
1452
|
else {
|
|
892
1453
|
const keyFields = this.#schemaMeta.entities[target.__typename]?.keyFields;
|
|
@@ -896,10 +1457,10 @@ var Cache = class {
|
|
|
896
1457
|
if ("$field" in target) {
|
|
897
1458
|
const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
|
|
898
1459
|
this.#stale.add(makeDependencyKey(entityKey, fieldKey));
|
|
899
|
-
this.#collectSubscriptions(entityKey, fieldKey,
|
|
1460
|
+
this.#collectSubscriptions(entityKey, fieldKey, affectedSubscriptions);
|
|
900
1461
|
} else {
|
|
901
1462
|
this.#stale.add(entityKey);
|
|
902
|
-
this.#collectSubscriptions(entityKey, void 0,
|
|
1463
|
+
this.#collectSubscriptions(entityKey, void 0, affectedSubscriptions);
|
|
903
1464
|
}
|
|
904
1465
|
} else {
|
|
905
1466
|
const prefix = `${target.__typename}:`;
|
|
@@ -908,15 +1469,30 @@ var Cache = class {
|
|
|
908
1469
|
if ("$field" in target) {
|
|
909
1470
|
const fieldKey = makeFieldKeyFromArgs(target.$field, target.$args);
|
|
910
1471
|
this.#stale.add(makeDependencyKey(entityKey, fieldKey));
|
|
911
|
-
this.#collectSubscriptions(entityKey, fieldKey,
|
|
1472
|
+
this.#collectSubscriptions(entityKey, fieldKey, affectedSubscriptions);
|
|
912
1473
|
} else {
|
|
913
1474
|
this.#stale.add(entityKey);
|
|
914
|
-
this.#collectSubscriptions(entityKey, void 0,
|
|
1475
|
+
this.#collectSubscriptions(entityKey, void 0, affectedSubscriptions);
|
|
915
1476
|
}
|
|
916
1477
|
}
|
|
917
1478
|
}
|
|
918
1479
|
}
|
|
919
|
-
for (const subscription of
|
|
1480
|
+
for (const subscription of affectedSubscriptions) subscription.listener(null);
|
|
1481
|
+
}
|
|
1482
|
+
/**
|
|
1483
|
+
* Checks if a subscription has stale data.
|
|
1484
|
+
* @internal
|
|
1485
|
+
*/
|
|
1486
|
+
isStale(subscription) {
|
|
1487
|
+
const check = (node) => {
|
|
1488
|
+
if (node.depKey.includes("@")) {
|
|
1489
|
+
const { storageKey } = parseDependencyKey(node.depKey);
|
|
1490
|
+
if (this.#stale.has(storageKey) || this.#stale.has(node.depKey)) return true;
|
|
1491
|
+
}
|
|
1492
|
+
for (const child of node.children.values()) if (check(child)) return true;
|
|
1493
|
+
return false;
|
|
1494
|
+
};
|
|
1495
|
+
return check(subscription.entryTree);
|
|
920
1496
|
}
|
|
921
1497
|
#hasKeyFields(target, keyFields) {
|
|
922
1498
|
return keyFields.every((f) => f in target);
|
|
@@ -924,48 +1500,43 @@ var Cache = class {
|
|
|
924
1500
|
#collectSubscriptions(storageKey, fieldKey, out) {
|
|
925
1501
|
if (fieldKey === void 0) {
|
|
926
1502
|
const prefix = `${storageKey}.`;
|
|
927
|
-
for (const [depKey,
|
|
1503
|
+
for (const [depKey, entries] of this.#subscriptions) if (depKey.startsWith(prefix)) for (const entry of entries) out.add(entry.subscription);
|
|
928
1504
|
} else {
|
|
929
1505
|
const depKey = makeDependencyKey(storageKey, fieldKey);
|
|
930
|
-
const
|
|
931
|
-
if (
|
|
1506
|
+
const entries = this.#subscriptions.get(depKey);
|
|
1507
|
+
if (entries) for (const entry of entries) out.add(entry.subscription);
|
|
932
1508
|
}
|
|
933
1509
|
}
|
|
934
|
-
#
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
|
|
939
|
-
|
|
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);
|
|
1510
|
+
#removeSubscriptionFromTree(node, subscription) {
|
|
1511
|
+
const entries = this.#subscriptions.get(node.depKey);
|
|
1512
|
+
if (entries) {
|
|
1513
|
+
for (const entry of entries) if (entry.subscription === subscription) {
|
|
1514
|
+
entries.delete(entry);
|
|
1515
|
+
break;
|
|
946
1516
|
}
|
|
947
|
-
|
|
1517
|
+
if (entries.size === 0) this.#subscriptions.delete(node.depKey);
|
|
1518
|
+
}
|
|
1519
|
+
for (const child of node.children.values()) this.#removeSubscriptionFromTree(child, subscription);
|
|
1520
|
+
}
|
|
1521
|
+
#parseDepKey(depKey) {
|
|
1522
|
+
return parseDependencyKey(depKey);
|
|
948
1523
|
}
|
|
949
1524
|
/**
|
|
950
|
-
* Extracts a serializable snapshot of the cache storage
|
|
1525
|
+
* Extracts a serializable snapshot of the cache storage.
|
|
951
1526
|
* Optimistic layers are excluded because they represent transient in-flight state.
|
|
952
1527
|
*/
|
|
953
1528
|
extract() {
|
|
954
|
-
return {
|
|
955
|
-
storage: structuredClone(this.#storage),
|
|
956
|
-
memo: Object.fromEntries(this.#memo)
|
|
957
|
-
};
|
|
1529
|
+
return { storage: structuredClone(this.#storage) };
|
|
958
1530
|
}
|
|
959
1531
|
/**
|
|
960
1532
|
* Hydrates the cache with a previously extracted snapshot.
|
|
961
1533
|
*/
|
|
962
1534
|
hydrate(snapshot) {
|
|
963
|
-
const { storage
|
|
1535
|
+
const { storage } = snapshot;
|
|
964
1536
|
for (const [key, fields] of Object.entries(storage)) this.#storage[key] = {
|
|
965
1537
|
...this.#storage[key],
|
|
966
1538
|
...fields
|
|
967
1539
|
};
|
|
968
|
-
for (const [key, value] of Object.entries(memo)) this.#memo.set(key, value);
|
|
969
1540
|
this.#storageView = null;
|
|
970
1541
|
}
|
|
971
1542
|
/**
|
|
@@ -974,7 +1545,6 @@ var Cache = class {
|
|
|
974
1545
|
clear() {
|
|
975
1546
|
this.#storage = { [RootFieldKey]: {} };
|
|
976
1547
|
this.#subscriptions.clear();
|
|
977
|
-
this.#memo.clear();
|
|
978
1548
|
this.#stale.clear();
|
|
979
1549
|
this.#optimisticKeys = [];
|
|
980
1550
|
this.#optimisticLayers.clear();
|
|
@@ -1010,6 +1580,9 @@ const cacheExchange = (options = {}) => {
|
|
|
1010
1580
|
clear: () => cache.clear()
|
|
1011
1581
|
},
|
|
1012
1582
|
io: (ops$) => {
|
|
1583
|
+
const subscriptionHasData = /* @__PURE__ */ new Map();
|
|
1584
|
+
const resubscribe$ = makeSubject();
|
|
1585
|
+
const refetch$ = makeSubject();
|
|
1013
1586
|
const fragment$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "fragment"), mergeMap((op) => {
|
|
1014
1587
|
const fragmentRef = op.metadata?.fragment?.ref;
|
|
1015
1588
|
if (!fragmentRef) return fromValue({
|
|
@@ -1017,77 +1590,152 @@ const cacheExchange = (options = {}) => {
|
|
|
1017
1590
|
errors: [new ExchangeError("Fragment operation missing fragment.ref in metadata. This usually happens when the wrong fragment reference was passed.", { exchangeName: "cache" })]
|
|
1018
1591
|
});
|
|
1019
1592
|
if (isFragmentRefArray(fragmentRef)) {
|
|
1020
|
-
const
|
|
1021
|
-
const
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1593
|
+
const results = makeSubject();
|
|
1594
|
+
const unsubscribes = [];
|
|
1595
|
+
const fragmentSubscriptions = [];
|
|
1596
|
+
for (const [index, ref] of fragmentRef.entries()) {
|
|
1597
|
+
const patchListener = (patches) => {
|
|
1598
|
+
if (patches) {
|
|
1599
|
+
const indexedPatches = patches.map((patch) => ({
|
|
1600
|
+
...patch,
|
|
1601
|
+
path: [index, ...patch.path]
|
|
1602
|
+
}));
|
|
1603
|
+
results.next({
|
|
1604
|
+
operation: op,
|
|
1605
|
+
metadata: { cache: { patches: indexedPatches } },
|
|
1606
|
+
errors: []
|
|
1607
|
+
});
|
|
1608
|
+
} else {
|
|
1609
|
+
const sub = fragmentSubscriptions[index];
|
|
1610
|
+
if (sub && cache.isStale(sub)) {
|
|
1611
|
+
const { data, stale } = cache.readFragments(op.artifact, fragmentRef);
|
|
1612
|
+
if (data !== null) results.next({
|
|
1613
|
+
operation: op,
|
|
1614
|
+
data,
|
|
1615
|
+
...stale && { metadata: { cache: { stale: true } } },
|
|
1616
|
+
errors: []
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
const { unsubscribe, subscription } = cache.subscribeFragment(op.artifact, ref, patchListener);
|
|
1622
|
+
unsubscribes.push(unsubscribe);
|
|
1623
|
+
fragmentSubscriptions.push(subscription);
|
|
1624
|
+
}
|
|
1625
|
+
const { data: initialData, stale: initialStale } = cache.readFragments(op.artifact, fragmentRef);
|
|
1626
|
+
const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => {
|
|
1627
|
+
for (const unsub of unsubscribes) unsub();
|
|
1628
|
+
results.complete();
|
|
1629
|
+
}));
|
|
1630
|
+
return pipe(merge(fromValue({
|
|
1026
1631
|
operation: op,
|
|
1027
|
-
data,
|
|
1028
|
-
...
|
|
1632
|
+
data: initialData,
|
|
1633
|
+
...initialStale && { metadata: { cache: { stale: true } } },
|
|
1029
1634
|
errors: []
|
|
1030
|
-
})));
|
|
1635
|
+
}), results.source), takeUntil(teardown$));
|
|
1031
1636
|
}
|
|
1032
1637
|
if (!isFragmentRef(fragmentRef)) return fromValue({
|
|
1033
1638
|
operation: op,
|
|
1034
1639
|
data: fragmentRef,
|
|
1035
1640
|
errors: []
|
|
1036
1641
|
});
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1642
|
+
const results = makeSubject();
|
|
1643
|
+
let currentUnsubscribe = null;
|
|
1644
|
+
let currentSubscription = null;
|
|
1645
|
+
const patchListener = (patches) => {
|
|
1646
|
+
if (patches) results.next({
|
|
1647
|
+
operation: op,
|
|
1648
|
+
metadata: { cache: { patches } },
|
|
1649
|
+
errors: []
|
|
1650
|
+
});
|
|
1651
|
+
else if (currentSubscription) {
|
|
1652
|
+
if (cache.isStale(currentSubscription)) {
|
|
1653
|
+
const { data: staleData } = cache.readFragment(op.artifact, fragmentRef);
|
|
1654
|
+
if (staleData !== null) results.next({
|
|
1655
|
+
operation: op,
|
|
1656
|
+
data: staleData,
|
|
1657
|
+
metadata: { cache: { stale: true } },
|
|
1658
|
+
errors: []
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
const { data, stale, unsubscribe, subscription } = cache.subscribeFragment(op.artifact, fragmentRef, patchListener);
|
|
1664
|
+
currentUnsubscribe = unsubscribe;
|
|
1665
|
+
currentSubscription = subscription;
|
|
1666
|
+
const teardown$ = pipe(ops$, filter((operation) => operation.variant === "teardown" && operation.key === op.key), tap(() => {
|
|
1667
|
+
if (currentUnsubscribe) currentUnsubscribe();
|
|
1668
|
+
results.complete();
|
|
1669
|
+
}));
|
|
1670
|
+
return pipe(merge(data === null ? empty() : fromValue({
|
|
1043
1671
|
operation: op,
|
|
1044
1672
|
data,
|
|
1045
1673
|
...stale && { metadata: { cache: { stale: true } } },
|
|
1046
1674
|
errors: []
|
|
1047
|
-
})));
|
|
1675
|
+
}), results.source), takeUntil(teardown$));
|
|
1048
1676
|
}));
|
|
1049
1677
|
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
1678
|
if (op.artifact.kind === "mutation" && op.metadata?.cache?.optimisticResponse) cache.writeOptimistic(op.key, op.artifact, op.variables, op.metadata.cache.optimisticResponse);
|
|
1051
1679
|
}));
|
|
1052
1680
|
const query$ = pipe(ops$, filter((op) => op.variant === "request" && op.artifact.kind === "query" && fetchPolicy !== "network-only"), share());
|
|
1053
|
-
const refetch$ = makeSubject();
|
|
1054
1681
|
return merge(fragment$, pipe(query$, mergeMap((op) => {
|
|
1055
|
-
const
|
|
1056
|
-
let
|
|
1057
|
-
|
|
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
|
-
|
|
1682
|
+
const results = makeSubject();
|
|
1683
|
+
let currentUnsubscribe = null;
|
|
1684
|
+
let currentSubscription = null;
|
|
1685
|
+
let initialized = false;
|
|
1686
|
+
const doSubscribe = () => {
|
|
1687
|
+
if (currentUnsubscribe) currentUnsubscribe();
|
|
1688
|
+
const patchListener = (patches) => {
|
|
1689
|
+
if (patches) {
|
|
1690
|
+
if (!initialized) return;
|
|
1691
|
+
results.next({
|
|
1692
|
+
operation: op,
|
|
1693
|
+
metadata: { cache: { patches } },
|
|
1694
|
+
errors: []
|
|
1695
|
+
});
|
|
1696
|
+
} else if (currentSubscription) {
|
|
1697
|
+
if (cache.isStale(currentSubscription)) {
|
|
1698
|
+
const { data: staleData } = cache.readQuery(op.artifact, op.variables);
|
|
1699
|
+
if (staleData !== null) results.next({
|
|
1700
|
+
operation: op,
|
|
1701
|
+
data: staleData,
|
|
1702
|
+
metadata: { cache: { stale: true } },
|
|
1703
|
+
errors: []
|
|
1704
|
+
});
|
|
1705
|
+
refetch$.next(op);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
const result = cache.subscribeQuery(op.artifact, op.variables, patchListener);
|
|
1710
|
+
currentUnsubscribe = result.unsubscribe;
|
|
1711
|
+
currentSubscription = result.subscription;
|
|
1712
|
+
return result;
|
|
1713
|
+
};
|
|
1714
|
+
const { data, stale } = doSubscribe();
|
|
1715
|
+
subscriptionHasData.set(op.key, data !== null);
|
|
1716
|
+
if (data !== null) initialized = true;
|
|
1717
|
+
const teardown$ = pipe(ops$, filter((o) => o.variant === "teardown" && o.key === op.key), tap(() => {
|
|
1718
|
+
if (currentUnsubscribe) currentUnsubscribe();
|
|
1719
|
+
subscriptionHasData.delete(op.key);
|
|
1720
|
+
results.complete();
|
|
1721
|
+
}));
|
|
1722
|
+
const resubStream$ = pipe(resubscribe$.source, filter((key) => key === op.key), mergeMap(() => {
|
|
1723
|
+
doSubscribe();
|
|
1724
|
+
initialized = true;
|
|
1089
1725
|
return empty();
|
|
1090
1726
|
}));
|
|
1727
|
+
const stream$ = pipe(merge(data === null ? fetchPolicy === "cache-only" ? fromValue({
|
|
1728
|
+
operation: op,
|
|
1729
|
+
data: null,
|
|
1730
|
+
errors: []
|
|
1731
|
+
}) : empty() : fromValue({
|
|
1732
|
+
operation: op,
|
|
1733
|
+
data,
|
|
1734
|
+
...stale && { metadata: { cache: { stale: true } } },
|
|
1735
|
+
errors: []
|
|
1736
|
+
}), results.source, resubStream$), takeUntil(teardown$));
|
|
1737
|
+
if (stale) refetch$.next(op);
|
|
1738
|
+
return stream$;
|
|
1091
1739
|
}), filter(() => fetchPolicy === "cache-only" || fetchPolicy === "cache-and-network" || fetchPolicy === "cache-first")), pipe(merge(nonCache$, pipe(query$, filter((op) => {
|
|
1092
1740
|
const { data } = cache.readQuery(op.artifact, op.variables);
|
|
1093
1741
|
return fetchPolicy === "cache-and-network" || data === null;
|
|
@@ -1095,8 +1743,22 @@ const cacheExchange = (options = {}) => {
|
|
|
1095
1743
|
if (result.operation.variant === "request" && result.operation.artifact.kind === "mutation" && result.operation.metadata?.cache?.optimisticResponse) cache.removeOptimistic(result.operation.key);
|
|
1096
1744
|
if (result.operation.variant === "request" && result.data) cache.writeQuery(result.operation.artifact, result.operation.variables, result.data);
|
|
1097
1745
|
if (result.operation.variant !== "request" || result.operation.artifact.kind !== "query" || fetchPolicy === "network-only" || !!(result.errors && result.errors.length > 0)) return fromValue(result);
|
|
1746
|
+
if (subscriptionHasData.get(result.operation.key)) {
|
|
1747
|
+
const { data } = cache.readQuery(result.operation.artifact, result.operation.variables);
|
|
1748
|
+
if (data !== null) return empty();
|
|
1749
|
+
return fromValue({
|
|
1750
|
+
operation: result.operation,
|
|
1751
|
+
data: void 0,
|
|
1752
|
+
errors: [new ExchangeError("Cache failed to denormalize the network response. This is likely a bug in the cache normalizer.", { exchangeName: "cache" })]
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
subscriptionHasData.set(result.operation.key, true);
|
|
1756
|
+
resubscribe$.next(result.operation.key);
|
|
1098
1757
|
const { data } = cache.readQuery(result.operation.artifact, result.operation.variables);
|
|
1099
|
-
if (data !== null) return
|
|
1758
|
+
if (data !== null) return fromValue({
|
|
1759
|
+
...result,
|
|
1760
|
+
data
|
|
1761
|
+
});
|
|
1100
1762
|
return fromValue({
|
|
1101
1763
|
operation: result.operation,
|
|
1102
1764
|
data: void 0,
|
|
@@ -1108,6 +1770,99 @@ const cacheExchange = (options = {}) => {
|
|
|
1108
1770
|
};
|
|
1109
1771
|
};
|
|
1110
1772
|
|
|
1773
|
+
//#endregion
|
|
1774
|
+
//#region src/cache/patch.ts
|
|
1775
|
+
const copyNode = (node) => Array.isArray(node) ? [...node] : { ...node };
|
|
1776
|
+
const shallowCopyPath = (root, path) => {
|
|
1777
|
+
if (path.length === 0) return root;
|
|
1778
|
+
let result = copyNode(root);
|
|
1779
|
+
const top = result;
|
|
1780
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
1781
|
+
const key = path[i];
|
|
1782
|
+
result[key] = copyNode(result[key]);
|
|
1783
|
+
result = result[key];
|
|
1784
|
+
}
|
|
1785
|
+
return top;
|
|
1786
|
+
};
|
|
1787
|
+
/**
|
|
1788
|
+
* Sets a value at a nested path within an object.
|
|
1789
|
+
* @param obj - The object to modify.
|
|
1790
|
+
* @param path - The path to the target location.
|
|
1791
|
+
* @param value - The value to set.
|
|
1792
|
+
*/
|
|
1793
|
+
const setPath = (obj, path, value) => {
|
|
1794
|
+
let current = obj;
|
|
1795
|
+
for (let i = 0; i < path.length - 1; i++) current = current[path[i]];
|
|
1796
|
+
current[path.at(-1)] = value;
|
|
1797
|
+
};
|
|
1798
|
+
/**
|
|
1799
|
+
* Gets a value at a nested path within an object.
|
|
1800
|
+
* @param obj - The object to read from.
|
|
1801
|
+
* @param path - The path to the target location.
|
|
1802
|
+
* @returns The value at the path, or the object itself if path is empty.
|
|
1803
|
+
*/
|
|
1804
|
+
const getPath = (obj, path) => {
|
|
1805
|
+
let current = obj;
|
|
1806
|
+
for (const segment of path) {
|
|
1807
|
+
if (current === void 0 || current === null) return void 0;
|
|
1808
|
+
current = current[segment];
|
|
1809
|
+
}
|
|
1810
|
+
return current;
|
|
1811
|
+
};
|
|
1812
|
+
/**
|
|
1813
|
+
* Applies cache patches to data immutably, shallow-copying only along changed paths.
|
|
1814
|
+
*/
|
|
1815
|
+
const applyPatchesImmutable = (data, patches) => {
|
|
1816
|
+
if (patches.length === 0) return data;
|
|
1817
|
+
let result = data;
|
|
1818
|
+
for (const patch of patches) if (patch.type === "set") {
|
|
1819
|
+
if (patch.path.length === 0) {
|
|
1820
|
+
result = patch.value;
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
result = shallowCopyPath(result, patch.path);
|
|
1824
|
+
let target = result;
|
|
1825
|
+
for (let i = 0; i < patch.path.length - 1; i++) target = target[patch.path[i]];
|
|
1826
|
+
target[patch.path.at(-1)] = patch.value;
|
|
1827
|
+
} else if (patch.type === "splice") {
|
|
1828
|
+
result = shallowCopyPath(result, patch.path);
|
|
1829
|
+
let target = result;
|
|
1830
|
+
for (const segment of patch.path) target = target[segment];
|
|
1831
|
+
const arr = [...target];
|
|
1832
|
+
arr.splice(patch.index, patch.deleteCount, ...patch.items);
|
|
1833
|
+
let parent = result;
|
|
1834
|
+
for (let i = 0; i < patch.path.length - 1; i++) parent = parent[patch.path[i]];
|
|
1835
|
+
parent[patch.path.at(-1)] = arr;
|
|
1836
|
+
} else if (patch.type === "swap") {
|
|
1837
|
+
result = shallowCopyPath(result, patch.path);
|
|
1838
|
+
let target = result;
|
|
1839
|
+
for (const segment of patch.path) target = target[segment];
|
|
1840
|
+
const arr = [...target];
|
|
1841
|
+
[arr[patch.i], arr[patch.j]] = [arr[patch.j], arr[patch.i]];
|
|
1842
|
+
let parent = result;
|
|
1843
|
+
for (let i = 0; i < patch.path.length - 1; i++) parent = parent[patch.path[i]];
|
|
1844
|
+
parent[patch.path.at(-1)] = arr;
|
|
1845
|
+
}
|
|
1846
|
+
return result;
|
|
1847
|
+
};
|
|
1848
|
+
/**
|
|
1849
|
+
* Applies cache patches to a mutable target object in place.
|
|
1850
|
+
* @param target - The mutable object to apply patches to.
|
|
1851
|
+
* @param patches - The patches to apply.
|
|
1852
|
+
* @returns The new root value if a root-level set patch was applied, otherwise undefined.
|
|
1853
|
+
*/
|
|
1854
|
+
const applyPatchesMutable = (target, patches) => {
|
|
1855
|
+
let root;
|
|
1856
|
+
for (const patch of patches) if (patch.type === "set") if (patch.path.length === 0) root = patch.value;
|
|
1857
|
+
else setPath(target, patch.path, patch.value);
|
|
1858
|
+
else if (patch.type === "splice") getPath(target, patch.path).splice(patch.index, patch.deleteCount, ...patch.items);
|
|
1859
|
+
else if (patch.type === "swap") {
|
|
1860
|
+
const arr = getPath(target, patch.path);
|
|
1861
|
+
[arr[patch.i], arr[patch.j]] = [arr[patch.j], arr[patch.i]];
|
|
1862
|
+
}
|
|
1863
|
+
return root;
|
|
1864
|
+
};
|
|
1865
|
+
|
|
1111
1866
|
//#endregion
|
|
1112
1867
|
//#region src/exchanges/retry.ts
|
|
1113
1868
|
const defaultShouldRetry = (error) => isExchangeError(error, "http") && error.extensions?.statusCode !== void 0 && error.extensions.statusCode >= 500;
|
|
@@ -1579,4 +2334,4 @@ const createClient = (config) => {
|
|
|
1579
2334
|
};
|
|
1580
2335
|
|
|
1581
2336
|
//#endregion
|
|
1582
|
-
export { AggregatedError, Client, ExchangeError, GraphQLError, RequiredFieldError, cacheExchange, createClient, dedupExchange, fragmentExchange, httpExchange, isAggregatedError, isExchangeError, isGraphQLError, requiredExchange, retryExchange, stringify, subscriptionExchange };
|
|
2337
|
+
export { AggregatedError, Client, ExchangeError, GraphQLError, RequiredFieldError, applyPatchesImmutable, applyPatchesMutable, cacheExchange, createClient, dedupExchange, fragmentExchange, getPath, httpExchange, isAggregatedError, isExchangeError, isGraphQLError, requiredExchange, retryExchange, setPath, stringify, subscriptionExchange };
|