diff --git a/docs/source/caching/cache-field-behavior.md b/docs/source/caching/cache-field-behavior.md index 7d8672dd78b..654a3b50545 100644 --- a/docs/source/caching/cache-field-behavior.md +++ b/docs/source/caching/cache-field-behavior.md @@ -336,7 +336,8 @@ const cache = new InMemoryCache({ merge(existing: any[], incoming: any[], { args }) { const merged = existing ? existing.slice(0) : []; // Insert the incoming elements in the right places, according to args. - for (let i = args.offset; i < args.offset + args.limit; ++i) { + const end = args.offset + Math.min(args.limit, incoming.length); + for (let i = args.offset; i < end; ++i) { merged[i] = incoming[i - args.offset]; } return merged; @@ -346,10 +347,16 @@ const cache = new InMemoryCache({ // If we read the field before any data has been written to the // cache, this function will return undefined, which correctly // indicates that the field is missing. - return existing && existing.slice( + const page = existing && existing.slice( args.offset, args.offset + args.limit, ); + // If we ask for a page outside the bounds of the existing array, + // page.length will be 0, and we should return undefined instead of + // the empty array. + if (page && page.length > 0) { + return page; + } }, }, }, @@ -394,10 +401,13 @@ const cache = new InMemoryCache({ const afterIndex = existing.findIndex( task => args.afterId === readField("id", task)); if (afterIndex >= 0) { - return existing.slice( + const page = existing.slice( afterIndex + 1, afterIndex + 1 + args.limit, ); + if (page && page.length > 0) { + return page; + } } } }, diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index a3ea1c0606a..bbc39896481 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -74,8 +74,8 @@ export type TypePolicy = { fields?: { [fieldName: string]: - | FieldPolicy - | FieldReadFunction; + | FieldPolicy + | FieldReadFunction; } }; @@ -88,8 +88,17 @@ type KeyArgsFunction = ( }, ) => ReturnType; +// The Readonly type only really works for object types, since it marks +// all of the object's properties as readonly, but there are many cases when +// a generic type parameter like TExisting might be a string or some other +// primitive type, in which case we need to avoid wrapping it with Readonly. +// SafeReadonly collapses to just string, which makes string +// assignable to SafeReadonly, whereas string is not assignable to +// Readonly, somewhat surprisingly. +type SafeReadonly = T extends object ? Readonly : T; + export type FieldPolicy< - TExisting, + TExisting = any, TIncoming = TExisting, TReadResult = TExisting, > = { @@ -140,7 +149,7 @@ interface FieldFunctionOptions { readField( nameOrField: string | FieldNode, foreignObjOrRef?: StoreObject | Reference, - ): Readonly; + ): SafeReadonly; // A handy place to put field-specific data that you want to survive // across multiple read function calls. Useful for field-level caching, @@ -148,7 +157,7 @@ interface FieldFunctionOptions { storage: StorageType; } -export type FieldReadFunction = ( +export type FieldReadFunction = ( // When reading a field, one often needs to know about any existing // value stored for that field. If the field is read before any value // has been written to the cache, this existing parameter will be @@ -157,15 +166,15 @@ export type FieldReadFunction = ( // than one of the named options) because that makes it possible for the // developer to annotate it with a type, without also having to provide // a whole new type for the options object. - existing: Readonly | undefined, + existing: SafeReadonly | undefined, options: FieldFunctionOptions, ) => TReadResult; -export type FieldMergeFunction = ( - existing: Readonly | undefined, +export type FieldMergeFunction = ( + existing: SafeReadonly | undefined, // The incoming parameter needs to be positional as well, for the same // reasons discussed in FieldReadFunction above. - incoming: Readonly, + incoming: SafeReadonly, options: FieldFunctionOptions, ) => TExisting; @@ -193,8 +202,8 @@ export class Policies { fields?: { [fieldName: string]: { keyFn?: KeyArgsFunction; - read?: FieldReadFunction; - merge?: FieldMergeFunction; + read?: FieldReadFunction; + merge?: FieldMergeFunction; }; }; }; @@ -429,7 +438,7 @@ export class Policies { return function getFieldValue( objectOrReference: StoreObject | Reference, storeFieldName: string, - ): Readonly { + ): SafeReadonly { let fieldValue: StoreValue; if (isReference(objectOrReference)) { const dataId = objectOrReference.__ref; @@ -444,7 +453,7 @@ export class Policies { fieldValue = objectOrReference && objectOrReference[storeFieldName]; } // Enforce Readonly at runtime, in development. - return maybeDeepFreeze(fieldValue) as T; + return maybeDeepFreeze(fieldValue) as SafeReadonly; }; } @@ -490,7 +499,7 @@ export class Policies { getFieldValue: FieldValueGetter, variables?: Record, typename = getFieldValue(objectOrReference, "__typename"), - ): Readonly { + ): SafeReadonly { invariant( objectOrReference, "Must provide an object or Reference when calling Policies#readField", @@ -520,7 +529,7 @@ export class Policies { storage, getFieldValue, variables, - )) as Readonly; + )) as SafeReadonly; } return existing;