diff --git a/src/cache/inmemory/__tests__/policies.ts b/src/cache/inmemory/__tests__/policies.ts index 69916c4447c..cae421d2801 100644 --- a/src/cache/inmemory/__tests__/policies.ts +++ b/src/cache/inmemory/__tests__/policies.ts @@ -1722,7 +1722,7 @@ describe("type policies", function () { expect(incoming).toBe("initial value"); return storage.cached = "merged value"; }, - finalize(existing, { storage }) { + drop(existing, { storage }) { expect(existing).toBe("merged value"); expect(storage.cached).toBe(existing); delete storage.cached; @@ -1842,7 +1842,7 @@ describe("type policies", function () { ] : incoming; }, - finalize(existing) { + drop(existing) { expect(existing).toEqual([ { __ref: 'Task:{"taskID":1}' }, { __ref: 'Task:{"taskID":2}' }, diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts index cb4fd90bf0d..ace6dfd498e 100644 --- a/src/cache/inmemory/entityStore.ts +++ b/src/cache/inmemory/entityStore.ts @@ -188,48 +188,39 @@ export abstract class EntityStore implements NormalizedCache { // that lead from the entity to the nested object within it. this.group.assignPaths(dataId, merged); - // Run finalize functions for fields that are being removed. We consider - // only the fields of existing that are shared by incoming, since those - // are the only fields that could be changing. if (existing) { - const context: ReadMergeModifyContext = { store: this }; - - const walk = (existing: StoreValue, incoming: StoreValue | undefined) => { - if (existing === incoming) return; - - if (Array.isArray(existing)) { - (existing as StoreValue[]).forEach((child, i) => { - walk( - child, - incoming && Array.isArray(incoming) - ? incoming[i] - : void 0, - ); + // Collect objects and field names removed by this merge, so we can run + // drop functions configured for the fields that are about to removed + // (before we finally set this.data[dataId] = merged, below). + const drops: [StoreObject, string][] = []; + + const walk = (exVal: StoreValue, inVal: StoreValue | undefined) => { + if (exVal === inVal) return; + + if (Array.isArray(exVal)) { + (exVal as StoreValue[]).forEach((exChild, i) => { + const inChild = inVal && Array.isArray(inVal) ? inVal[i] : void 0; + walk(exChild, inChild); }); - } else if (storeValueIsStoreObject(existing)) { - Object.keys(existing).forEach(storeFieldName => { - const eChild = existing[storeFieldName]; - const iChild = incoming && storeValueIsStoreObject(incoming) - ? incoming[storeFieldName] + } else if (storeValueIsStoreObject(exVal)) { + Object.keys(exVal).forEach(storeFieldName => { + const exChild = exVal[storeFieldName]; + const inChild = inVal && storeValueIsStoreObject(inVal) + ? inVal[storeFieldName] : void 0; - // Visit children before running finalizeField for eChild. - walk(eChild, iChild); + // Visit children before running dropField for eChild. + walk(exChild, inChild); - if (iChild === void 0) { - this.policies.finalizeField( - existing.__typename, - existing, - storeFieldName, - context, - ); + if (inChild === void 0) { + drops.push([exVal, storeFieldName]); } }); } }; - // To detect field removals (in order to run finalize functions), we can + // To detect field removals (in order to run drop functions), we can // restrict our attention to the incoming fields, since those are the // top-level fields that might have changed. Object.keys(incoming).forEach(storeFieldName => { @@ -239,14 +230,22 @@ export abstract class EntityStore implements NormalizedCache { walk(eChild, iChild); if (iChild === void 0) { - this.policies.finalizeField( - existing.__typename, - existing, + drops.push([existing, storeFieldName]); + } + }); + + if (drops.length) { + const context: ReadMergeModifyContext = { store: this }; + + drops.forEach(([storeObject, storeFieldName]) => { + this.policies.dropField( + storeObject.__typename, + storeObject, storeFieldName, context, ); - } - }); + }); + } } } @@ -670,11 +669,13 @@ class CacheGroup { if (isReference(parentObjOrRef)) { path.push(parentObjOrRef.__ref); } else { - // See assignPathsAndFinalize to understand how this map is populated. + // See assignPaths to understand how this map is populated. const assignedPath = this.paths.get(parentObjOrRef); if (assignedPath) { assignedPath.forEach(push); } else { + // If we can't find a path for this object, use the object reference + // itself as a key. path.push(parentObjOrRef); } } diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index 6cc9df73089..4084765bcbf 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -132,7 +132,7 @@ export type FieldPolicy< keyArgs?: KeySpecifier | KeyArgsFunction | false; read?: FieldReadFunction; merge?: FieldMergeFunction | boolean; - finalize?: FieldFinalizeFunction; + drop?: FieldDropFunction; }; export type StorageType = Record; @@ -223,7 +223,7 @@ export type FieldMergeFunction = ( options: FieldFunctionOptions, ) => SafeReadonly; -export type FieldFinalizeFunction = ( +export type FieldDropFunction = ( existing: SafeReadonly | undefined, options: FieldFunctionOptions, ) => void; @@ -276,7 +276,7 @@ export class Policies { keyFn?: KeyArgsFunction; read?: FieldReadFunction; merge?: FieldMergeFunction; - finalize?: FieldFinalizeFunction; + drop?: FieldDropFunction; }; }; }; @@ -452,7 +452,7 @@ export class Policies { if (typeof incoming === "function") { existing.read = incoming; } else { - const { keyArgs, read, merge, finalize } = incoming; + const { keyArgs, read, merge, drop } = incoming; existing.keyFn = // Pass false to disable argument-based differentiation of @@ -472,8 +472,8 @@ export class Policies { setMerge(existing, merge); - if (typeof finalize === "function") { - existing.finalize = finalize; + if (typeof drop === "function") { + existing.drop = drop; } } @@ -779,7 +779,7 @@ export class Policies { return existing; } - public finalizeField( + public dropField( typename: string | undefined, objectOrReference: StoreObject | Reference, storeFieldName: string, @@ -787,9 +787,9 @@ export class Policies { ) { const fieldName = fieldNameFromStoreName(storeFieldName); const policy = this.getFieldPolicy(typename, fieldName, false); - const finalize = policy && policy.finalize; - if (finalize) { - finalize( + const drop = policy && policy.drop; + if (drop) { + drop( context.store.getFieldValue(objectOrReference, storeFieldName), // TODO Consolidate this code with similiar code in readField? makeFieldFunctionOptions(