Skip to content

Commit

Permalink
Process typePolicies lazily, upon first access by getTypePolicy.
Browse files Browse the repository at this point in the history
Passing typePolicies to the InMemoryCache constructor is something that
typically happens during application startup, so it's important that the
processing of typePolicies does not become a performance problem.

Fortunately, we don't have to do all the processing of typePolicies until
we actually look up a given type policy for the first time, which might
occur much later in the lifetime of the application for some types.

Once we implement inheritance of typePolicies, there will often be
supertype-subtype relationships among the types listed in typePolicies.
Without the laziness introduced by this commit, it would be necessary
either to keep typePolicies in supertypes-before-subtypes (topological)
order, or for addTypePolicies somehow to sort the types topologically.

Fortunately, that careful ordering/sorting becomes completely unnecessary
thanks to the laziness, because none of the types can be used until all of
them have been registered.
  • Loading branch information
benjamn committed Sep 23, 2020
1 parent 14f4da6 commit c696b9a
Showing 1 changed file with 81 additions and 62 deletions.
143 changes: 81 additions & 62 deletions src/cache/inmemory/policies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
isReference,
getStoreKeyName,
canUseWeakMap,
compact,
} from '../../utilities';
import { IdGetter, ReadMergeModifyContext } from "./types";
import {
Expand Down Expand Up @@ -248,6 +249,10 @@ export class Policies {
};
} = Object.create(null);

private toBeAdded: {
[__typename: string]: TypePolicy[];
} = Object.create(null);

// Map from subtype names to sets of supertype names. Note that this
// representation inverts the structure of possibleTypes (whose keys are
// supertypes and whose values are arrays of subtypes) because it tends
Expand Down Expand Up @@ -341,73 +346,81 @@ export class Policies {

public addTypePolicies(typePolicies: TypePolicies) {
Object.keys(typePolicies).forEach(typename => {
const existing = this.getTypePolicy(typename);
const incoming = typePolicies[typename];
const { keyFields, fields } = incoming;

if (incoming.queryType) this.setRootTypename("Query", typename);
if (incoming.mutationType) this.setRootTypename("Mutation", typename);
if (incoming.subscriptionType) this.setRootTypename("Subscription", typename);

existing.keyFn =
// Pass false to disable normalization for this typename.
keyFields === false ? nullKeyFieldsFn :
// Pass an array of strings to use those fields to compute a
// composite ID for objects of this typename.
Array.isArray(keyFields) ? keyFieldsFnFromSpecifier(keyFields) :
// Pass a function to take full control over identification.
typeof keyFields === "function" ? keyFields :
// Leave existing.keyFn unchanged if above cases fail.
existing.keyFn;

if (fields) {
Object.keys(fields).forEach(fieldName => {
const existing = this.getFieldPolicy(typename, fieldName, true)!;
const incoming = fields[fieldName];

if (typeof incoming === "function") {
existing.read = incoming;
} else {
const { keyArgs, read, merge } = incoming;

existing.keyFn =
// Pass false to disable argument-based differentiation of
// field identities.
keyArgs === false ? simpleKeyArgsFn :
// Pass an array of strings to use named arguments to
// compute a composite identity for the field.
Array.isArray(keyArgs) ? keyArgsFnFromSpecifier(keyArgs) :
// Pass a function to take full control over field identity.
typeof keyArgs === "function" ? keyArgs :
// Leave existing.keyFn unchanged if above cases fail.
existing.keyFn;

if (typeof read === "function") existing.read = read;

existing.merge =
typeof merge === "function" ? merge :
// Pass merge:true as a shorthand for a merge implementation
// that returns options.mergeObjects(existing, incoming).
merge === true ? mergeTrueFn :
// Pass merge:false to make incoming always replace existing
// without any warnings about data clobbering.
merge === false ? mergeFalseFn :
existing.merge;
}

if (existing.read && existing.merge) {
// If we have both a read and a merge function, assume
// keyArgs:false, because read and merge together can take
// responsibility for interpreting arguments in and out. This
// default assumption can always be overridden by specifying
// keyArgs explicitly in the FieldPolicy.
existing.keyFn = existing.keyFn || simpleKeyArgsFn;
}
});
if (hasOwn.call(this.toBeAdded, typename)) {
this.toBeAdded[typename].push(incoming);
} else {
this.toBeAdded[typename] = [incoming];
}
});
}

private updateTypePolicy(typename: string, incoming: TypePolicy) {
const existing = this.getTypePolicy(typename);
const { keyFields, fields } = incoming;

if (incoming.queryType) this.setRootTypename("Query", typename);
if (incoming.mutationType) this.setRootTypename("Mutation", typename);
if (incoming.subscriptionType) this.setRootTypename("Subscription", typename);

existing.keyFn =
// Pass false to disable normalization for this typename.
keyFields === false ? nullKeyFieldsFn :
// Pass an array of strings to use those fields to compute a
// composite ID for objects of this typename.
Array.isArray(keyFields) ? keyFieldsFnFromSpecifier(keyFields) :
// Pass a function to take full control over identification.
typeof keyFields === "function" ? keyFields :
// Leave existing.keyFn unchanged if above cases fail.
existing.keyFn;

if (fields) {
Object.keys(fields).forEach(fieldName => {
const existing = this.getFieldPolicy(typename, fieldName, true)!;
const incoming = fields[fieldName];

if (typeof incoming === "function") {
existing.read = incoming;
} else {
const { keyArgs, read, merge } = incoming;

existing.keyFn =
// Pass false to disable argument-based differentiation of
// field identities.
keyArgs === false ? simpleKeyArgsFn :
// Pass an array of strings to use named arguments to
// compute a composite identity for the field.
Array.isArray(keyArgs) ? keyArgsFnFromSpecifier(keyArgs) :
// Pass a function to take full control over field identity.
typeof keyArgs === "function" ? keyArgs :
// Leave existing.keyFn unchanged if above cases fail.
existing.keyFn;

if (typeof read === "function") existing.read = read;

existing.merge =
typeof merge === "function" ? merge :
// Pass merge:true as a shorthand for a merge implementation
// that returns options.mergeObjects(existing, incoming).
merge === true ? mergeTrueFn :
// Pass merge:false to make incoming always replace existing
// without any warnings about data clobbering.
merge === false ? mergeFalseFn :
existing.merge;
}

if (existing.read && existing.merge) {
// If we have both a read and a merge function, assume
// keyArgs:false, because read and merge together can take
// responsibility for interpreting arguments in and out. This
// default assumption can always be overridden by specifying
// keyArgs explicitly in the FieldPolicy.
existing.keyFn = existing.keyFn || simpleKeyArgsFn;
}
});
}
}

private setRootTypename(
which: "Query" | "Mutation" | "Subscription",
typename: string = which,
Expand Down Expand Up @@ -451,6 +464,12 @@ export class Policies {
this.typePolicies[typename] = Object.create(null);
policy.fields = Object.create(null);
}

const inbox = this.toBeAdded[typename];
if (inbox && inbox.length) {
this.updateTypePolicy(typename, compact(...inbox.splice(0)));
}

return this.typePolicies[typename];
}

Expand Down

0 comments on commit c696b9a

Please sign in to comment.