Description
When restoring a cache snapshot, normalized objects can unexpectedly get garbage collected. This is especially noticeable when components suspend (via suspense: true), as the React tree temporarily drops references to the cache data while waiting for the network, allowing the GC to wipe the objects.
Root Cause
By design, normalized objects without immediate active query references need to be strongly referenced inside FrailMap to survive.
In packages/gqty/src/Cache/normalization.ts, this is correctly enforced:
store.set(id, result, {
strong: true, // Intended strong reference
});
However, in packages/gqty/src/Cache/persistence.ts (inside importCacheSnapshot), a redundant .set() call accidentally overwrites this strong reference with a weak one:
data.normalizedObjects = Object.entries(normalized).reduce(
(store, [key, value]) => {
const norbject = normalizeObject(value as CacheObject, {
...options,
store,
});
// ❌ BUG: Since key === id, this overwrites the entry
// with FrailMap's default weak reference (strong: false)
if (norbject !== undefined) {
store.set(key, norbject);
}
return store;
},
new FrailMap<string, NormalizedObjectShell<CacheObject>>()
);
Proposed Solution
Remove the redundant store.set(key, norbject) call entirely in packages/gqty/src/Cache/persistence.ts. normalizeObject already guarantees that the object is placed in the store with the correct id and a strong: true reference.
data.normalizedObjects = Object.entries(normalized).reduce(
(store, [key, value]) => {
- const norbject = normalizeObject(value as CacheObject, {
+ normalizeObject(value as CacheObject, {
...options,
store,
});
- if (norbject !== undefined) {
- store.set(key, norbject);
- }
-
return store;
},
new FrailMap<string, NormalizedObjectShell<CacheObject>>()
);
Description
When restoring a cache snapshot, normalized objects can unexpectedly get garbage collected. This is especially noticeable when components suspend (via
suspense: true), as the React tree temporarily drops references to the cache data while waiting for the network, allowing the GC to wipe the objects.Root Cause
By design, normalized objects without immediate active query references need to be strongly referenced inside
FrailMapto survive.In
packages/gqty/src/Cache/normalization.ts, this is correctly enforced:However, in
packages/gqty/src/Cache/persistence.ts(insideimportCacheSnapshot), a redundant.set()call accidentally overwrites this strong reference with a weak one:Proposed Solution
Remove the redundant
store.set(key, norbject)call entirely inpackages/gqty/src/Cache/persistence.ts.normalizeObjectalready guarantees that the object is placed in the store with the correctidand astrong: truereference.data.normalizedObjects = Object.entries(normalized).reduce( (store, [key, value]) => { - const norbject = normalizeObject(value as CacheObject, { + normalizeObject(value as CacheObject, { ...options, store, }); - if (norbject !== undefined) { - store.set(key, norbject); - } - return store; }, new FrailMap<string, NormalizedObjectShell<CacheObject>>() );