import { __assign, __rest } from "tslib"; import { invariant, newInvariantError } from "../../utilities/globals/index.js"; import { storeKeyNameFromField, argumentsObjectFromField, isReference, getStoreKeyName, isNonNullObject, stringifyForDisplay, } from "../../utilities/index.js"; import { hasOwn, fieldNameFromStoreName, storeValueIsStoreObject, selectionSetMatchesResult, TypeOrFieldNameRegExp, defaultDataIdFromObject, isArray, } from "./helpers.js"; import { cacheSlot } from "./reactiveVars.js"; import { keyArgsFnFromSpecifier, keyFieldsFnFromSpecifier, } from "./key-extractor.js"; function argsFromFieldSpecifier(spec) { return (spec.args !== void 0 ? spec.args : spec.field ? argumentsObjectFromField(spec.field, spec.variables) : null); } var nullKeyFieldsFn = function () { return void 0; }; var simpleKeyArgsFn = function (_args, context) { return context.fieldName; }; // These merge functions can be selected by specifying merge:true or // merge:false in a field policy. var mergeTrueFn = function (existing, incoming, _a) { var mergeObjects = _a.mergeObjects; return mergeObjects(existing, incoming); }; var mergeFalseFn = function (_, incoming) { return incoming; }; var Policies = /** @class */ (function () { function Policies(config) { this.config = config; this.typePolicies = Object.create(null); this.toBeAdded = 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 // to be much more efficient to search upwards than downwards. this.supertypeMap = new Map(); // Any fuzzy subtypes specified by possibleTypes will be converted to // RegExp objects and recorded here. Every key of this map can also be // found in supertypeMap. In many cases this Map will be empty, which // means no fuzzy subtype checking will happen in fragmentMatches. this.fuzzySubtypes = new Map(); this.rootIdsByTypename = Object.create(null); this.rootTypenamesById = Object.create(null); this.usingPossibleTypes = false; this.config = __assign({ dataIdFromObject: defaultDataIdFromObject }, config); this.cache = this.config.cache; this.setRootTypename("Query"); this.setRootTypename("Mutation"); this.setRootTypename("Subscription"); if (config.possibleTypes) { this.addPossibleTypes(config.possibleTypes); } if (config.typePolicies) { this.addTypePolicies(config.typePolicies); } } Policies.prototype.identify = function (object, partialContext) { var _a; var policies = this; var typename = (partialContext && (partialContext.typename || ((_a = partialContext.storeObject) === null || _a === void 0 ? void 0 : _a.__typename))) || object.__typename; // It should be possible to write root Query fields with writeFragment, // using { __typename: "Query", ... } as the data, but it does not make // sense to allow the same identification behavior for the Mutation and // Subscription types, since application code should never be writing // directly to (or reading directly from) those root objects. if (typename === this.rootTypenamesById.ROOT_QUERY) { return ["ROOT_QUERY"]; } // Default context.storeObject to object if not otherwise provided. var storeObject = (partialContext && partialContext.storeObject) || object; var context = __assign(__assign({}, partialContext), { typename: typename, storeObject: storeObject, readField: (partialContext && partialContext.readField) || function () { var options = normalizeReadFieldOptions(arguments, storeObject); return policies.readField(options, { store: policies.cache["data"], variables: options.variables, }); } }); var id; var policy = typename && this.getTypePolicy(typename); var keyFn = (policy && policy.keyFn) || this.config.dataIdFromObject; while (keyFn) { var specifierOrId = keyFn(__assign(__assign({}, object), storeObject), context); if (isArray(specifierOrId)) { keyFn = keyFieldsFnFromSpecifier(specifierOrId); } else { id = specifierOrId; break; } } id = id ? String(id) : void 0; return context.keyObject ? [id, context.keyObject] : [id]; }; Policies.prototype.addTypePolicies = function (typePolicies) { var _this = this; Object.keys(typePolicies).forEach(function (typename) { var _a = typePolicies[typename], queryType = _a.queryType, mutationType = _a.mutationType, subscriptionType = _a.subscriptionType, incoming = __rest(_a, ["queryType", "mutationType", "subscriptionType"]); // Though {query,mutation,subscription}Type configurations are rare, // it's important to call setRootTypename as early as possible, // since these configurations should apply consistently for the // entire lifetime of the cache. Also, since only one __typename can // qualify as one of these root types, these three properties cannot // be inherited, unlike the rest of the incoming properties. That // restriction is convenient, because the purpose of this.toBeAdded // is to delay the processing of type/field policies until the first // time they're used, allowing policies to be added in any order as // long as all relevant policies (including policies for supertypes) // have been added by the time a given policy is used for the first // time. In other words, since inheritance doesn't matter for these // properties, there's also no need to delay their processing using // the this.toBeAdded queue. if (queryType) _this.setRootTypename("Query", typename); if (mutationType) _this.setRootTypename("Mutation", typename); if (subscriptionType) _this.setRootTypename("Subscription", typename); if (hasOwn.call(_this.toBeAdded, typename)) { _this.toBeAdded[typename].push(incoming); } else { _this.toBeAdded[typename] = [incoming]; } }); }; Policies.prototype.updateTypePolicy = function (typename, incoming) { var _this = this; var existing = this.getTypePolicy(typename); var keyFields = incoming.keyFields, fields = incoming.fields; function setMerge(existing, merge) { 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; } // Type policies can define merge functions, as an alternative to // using field policies to merge child objects. setMerge(existing, incoming.merge); 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. : 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(function (fieldName) { var existing = _this.getFieldPolicy(typename, fieldName, true); var incoming = fields[fieldName]; if (typeof incoming === "function") { existing.read = incoming; } else { var keyArgs = incoming.keyArgs, read = incoming.read, merge = incoming.merge; 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. : 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; } setMerge(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; } }); } }; Policies.prototype.setRootTypename = function (which, typename) { if (typename === void 0) { typename = which; } var rootId = "ROOT_" + which.toUpperCase(); var old = this.rootTypenamesById[rootId]; if (typename !== old) { invariant(!old || old === which, 5, which); // First, delete any old __typename associated with this rootId from // rootIdsByTypename. if (old) delete this.rootIdsByTypename[old]; // Now make this the only __typename that maps to this rootId. this.rootIdsByTypename[typename] = rootId; // Finally, update the __typename associated with this rootId. this.rootTypenamesById[rootId] = typename; } }; Policies.prototype.addPossibleTypes = function (possibleTypes) { var _this = this; this.usingPossibleTypes = true; Object.keys(possibleTypes).forEach(function (supertype) { // Make sure all types have an entry in this.supertypeMap, even if // their supertype set is empty, so we can return false immediately // from policies.fragmentMatches for unknown supertypes. _this.getSupertypeSet(supertype, true); possibleTypes[supertype].forEach(function (subtype) { _this.getSupertypeSet(subtype, true).add(supertype); var match = subtype.match(TypeOrFieldNameRegExp); if (!match || match[0] !== subtype) { // TODO Don't interpret just any invalid typename as a RegExp. _this.fuzzySubtypes.set(subtype, new RegExp(subtype)); } }); }); }; Policies.prototype.getTypePolicy = function (typename) { var _this = this; if (!hasOwn.call(this.typePolicies, typename)) { var policy_1 = (this.typePolicies[typename] = Object.create(null)); policy_1.fields = Object.create(null); // When the TypePolicy for typename is first accessed, instead of // starting with an empty policy object, inherit any properties or // fields from the type policies of the supertypes of typename. // // Any properties or fields defined explicitly within the TypePolicy // for typename will take precedence, and if there are multiple // supertypes, the properties of policies whose types were added // later via addPossibleTypes will take precedence over those of // earlier supertypes. TODO Perhaps we should warn about these // conflicts in development, and recommend defining the property // explicitly in the subtype policy? // // Field policy inheritance is atomic/shallow: you can't inherit a // field policy and then override just its read function, since read // and merge functions often need to cooperate, so changing only one // of them would be a recipe for inconsistency. // // Once the TypePolicy for typename has been accessed, its properties can // still be updated directly using addTypePolicies, but future changes to // inherited supertype policies will not be reflected in this subtype // policy, because this code runs at most once per typename. var supertypes_1 = this.supertypeMap.get(typename); if (!supertypes_1 && this.fuzzySubtypes.size) { // To make the inheritance logic work for unknown typename strings that // may have fuzzy supertypes, we give this typename an empty supertype // set and then populate it with any fuzzy supertypes that match. supertypes_1 = this.getSupertypeSet(typename, true); // This only works for typenames that are directly matched by a fuzzy // supertype. What if there is an intermediate chain of supertypes? // While possible, that situation can only be solved effectively by // specifying the intermediate relationships via possibleTypes, manually // and in a non-fuzzy way. this.fuzzySubtypes.forEach(function (regExp, fuzzy) { if (regExp.test(typename)) { // The fuzzy parameter is just the original string version of regExp // (not a valid __typename string), but we can look up the // associated supertype(s) in this.supertypeMap. var fuzzySupertypes = _this.supertypeMap.get(fuzzy); if (fuzzySupertypes) { fuzzySupertypes.forEach(function (supertype) { return supertypes_1.add(supertype); }); } } }); } if (supertypes_1 && supertypes_1.size) { supertypes_1.forEach(function (supertype) { var _a = _this.getTypePolicy(supertype), fields = _a.fields, rest = __rest(_a, ["fields"]); Object.assign(policy_1, rest); Object.assign(policy_1.fields, fields); }); } } var inbox = this.toBeAdded[typename]; if (inbox && inbox.length) { // Merge the pending policies into this.typePolicies, in the order they // were originally passed to addTypePolicy. inbox.splice(0).forEach(function (policy) { _this.updateTypePolicy(typename, policy); }); } return this.typePolicies[typename]; }; Policies.prototype.getFieldPolicy = function (typename, fieldName, createIfMissing) { if (typename) { var fieldPolicies = this.getTypePolicy(typename).fields; return (fieldPolicies[fieldName] || (createIfMissing && (fieldPolicies[fieldName] = Object.create(null)))); } }; Policies.prototype.getSupertypeSet = function (subtype, createIfMissing) { var supertypeSet = this.supertypeMap.get(subtype); if (!supertypeSet && createIfMissing) { this.supertypeMap.set(subtype, (supertypeSet = new Set())); } return supertypeSet; }; Policies.prototype.fragmentMatches = function (fragment, typename, result, variables) { var _this = this; if (!fragment.typeCondition) return true; // If the fragment has a type condition but the object we're matching // against does not have a __typename, the fragment cannot match. if (!typename) return false; var supertype = fragment.typeCondition.name.value; // Common case: fragment type condition and __typename are the same. if (typename === supertype) return true; if (this.usingPossibleTypes && this.supertypeMap.has(supertype)) { var typenameSupertypeSet = this.getSupertypeSet(typename, true); var workQueue_1 = [typenameSupertypeSet]; var maybeEnqueue_1 = function (subtype) { var supertypeSet = _this.getSupertypeSet(subtype, false); if (supertypeSet && supertypeSet.size && workQueue_1.indexOf(supertypeSet) < 0) { workQueue_1.push(supertypeSet); } }; // We need to check fuzzy subtypes only if we encountered fuzzy // subtype strings in addPossibleTypes, and only while writing to // the cache, since that's when selectionSetMatchesResult gives a // strong signal of fragment matching. The StoreReader class calls // policies.fragmentMatches without passing a result object, so // needToCheckFuzzySubtypes is always false while reading. var needToCheckFuzzySubtypes = !!(result && this.fuzzySubtypes.size); var checkingFuzzySubtypes = false; // It's important to keep evaluating workQueue.length each time through // the loop, because the queue can grow while we're iterating over it. for (var i = 0; i < workQueue_1.length; ++i) { var supertypeSet = workQueue_1[i]; if (supertypeSet.has(supertype)) { if (!typenameSupertypeSet.has(supertype)) { if (checkingFuzzySubtypes) { globalThis.__DEV__ !== false && invariant.warn(6, typename, supertype); } // Record positive results for faster future lookup. // Unfortunately, we cannot safely cache negative results, // because new possibleTypes data could always be added to the // Policies class. typenameSupertypeSet.add(supertype); } return true; } supertypeSet.forEach(maybeEnqueue_1); if (needToCheckFuzzySubtypes && // Start checking fuzzy subtypes only after exhausting all // non-fuzzy subtypes (after the final iteration of the loop). i === workQueue_1.length - 1 && // We could wait to compare fragment.selectionSet to result // after we verify the supertype, but this check is often less // expensive than that search, and we will have to do the // comparison anyway whenever we find a potential match. selectionSetMatchesResult(fragment.selectionSet, result, variables)) { // We don't always need to check fuzzy subtypes (if no result // was provided, or !this.fuzzySubtypes.size), but, when we do, // we only want to check them once. needToCheckFuzzySubtypes = false; checkingFuzzySubtypes = true; // If we find any fuzzy subtypes that match typename, extend the // workQueue to search through the supertypes of those fuzzy // subtypes. Otherwise the for-loop will terminate and we'll // return false below. this.fuzzySubtypes.forEach(function (regExp, fuzzyString) { var match = typename.match(regExp); if (match && match[0] === typename) { maybeEnqueue_1(fuzzyString); } }); } } } return false; }; Policies.prototype.hasKeyArgs = function (typename, fieldName) { var policy = this.getFieldPolicy(typename, fieldName, false); return !!(policy && policy.keyFn); }; Policies.prototype.getStoreFieldName = function (fieldSpec) { var typename = fieldSpec.typename, fieldName = fieldSpec.fieldName; var policy = this.getFieldPolicy(typename, fieldName, false); var storeFieldName; var keyFn = policy && policy.keyFn; if (keyFn && typename) { var context = { typename: typename, fieldName: fieldName, field: fieldSpec.field || null, variables: fieldSpec.variables, }; var args = argsFromFieldSpecifier(fieldSpec); while (keyFn) { var specifierOrString = keyFn(args, context); if (isArray(specifierOrString)) { keyFn = keyArgsFnFromSpecifier(specifierOrString); } else { // If the custom keyFn returns a falsy value, fall back to // fieldName instead. storeFieldName = specifierOrString || fieldName; break; } } } if (storeFieldName === void 0) { storeFieldName = fieldSpec.field ? storeKeyNameFromField(fieldSpec.field, fieldSpec.variables) : getStoreKeyName(fieldName, argsFromFieldSpecifier(fieldSpec)); } // Returning false from a keyArgs function is like configuring // keyArgs: false, but more dynamic. if (storeFieldName === false) { return fieldName; } // Make sure custom field names start with the actual field.name.value // of the field, so we can always figure out which properties of a // StoreObject correspond to which original field names. return fieldName === fieldNameFromStoreName(storeFieldName) ? storeFieldName : fieldName + ":" + storeFieldName; }; Policies.prototype.readField = function (options, context) { var objectOrReference = options.from; if (!objectOrReference) return; var nameOrField = options.field || options.fieldName; if (!nameOrField) return; if (options.typename === void 0) { var typename = context.store.getFieldValue(objectOrReference, "__typename"); if (typename) options.typename = typename; } var storeFieldName = this.getStoreFieldName(options); var fieldName = fieldNameFromStoreName(storeFieldName); var existing = context.store.getFieldValue(objectOrReference, storeFieldName); var policy = this.getFieldPolicy(options.typename, fieldName, false); var read = policy && policy.read; if (read) { var readOptions = makeFieldFunctionOptions(this, objectOrReference, options, context, context.store.getStorage(isReference(objectOrReference) ? objectOrReference.__ref : objectOrReference, storeFieldName)); // Call read(existing, readOptions) with cacheSlot holding this.cache. return cacheSlot.withValue(this.cache, read, [ existing, readOptions, ]); } return existing; }; Policies.prototype.getReadFunction = function (typename, fieldName) { var policy = this.getFieldPolicy(typename, fieldName, false); return policy && policy.read; }; Policies.prototype.getMergeFunction = function (parentTypename, fieldName, childTypename) { var policy = this.getFieldPolicy(parentTypename, fieldName, false); var merge = policy && policy.merge; if (!merge && childTypename) { policy = this.getTypePolicy(childTypename); merge = policy && policy.merge; } return merge; }; Policies.prototype.runMergeFunction = function (existing, incoming, _a, context, storage) { var field = _a.field, typename = _a.typename, merge = _a.merge; if (merge === mergeTrueFn) { // Instead of going to the trouble of creating a full // FieldFunctionOptions object and calling mergeTrueFn, we can // simply call mergeObjects, as mergeTrueFn would. return makeMergeObjectsFunction(context.store)(existing, incoming); } if (merge === mergeFalseFn) { // Likewise for mergeFalseFn, whose implementation is even simpler. return incoming; } // If cache.writeQuery or cache.writeFragment was called with // options.overwrite set to true, we still call merge functions, but // the existing data is always undefined, so the merge function will // not attempt to combine the incoming data with the existing data. if (context.overwrite) { existing = void 0; } return merge(existing, incoming, makeFieldFunctionOptions(this, // Unlike options.readField for read functions, we do not fall // back to the current object if no foreignObjOrRef is provided, // because it's not clear what the current object should be for // merge functions: the (possibly undefined) existing object, or // the incoming object? If you think your merge function needs // to read sibling fields in order to produce a new value for // the current field, you might want to rethink your strategy, // because that's a recipe for making merge behavior sensitive // to the order in which fields are written into the cache. // However, readField(name, ref) is useful for merge functions // that need to deduplicate child objects and references. void 0, { typename: typename, fieldName: field.name.value, field: field, variables: context.variables, }, context, storage || Object.create(null))); }; return Policies; }()); export { Policies }; function makeFieldFunctionOptions(policies, objectOrReference, fieldSpec, context, storage) { var storeFieldName = policies.getStoreFieldName(fieldSpec); var fieldName = fieldNameFromStoreName(storeFieldName); var variables = fieldSpec.variables || context.variables; var _a = context.store, toReference = _a.toReference, canRead = _a.canRead; return { args: argsFromFieldSpecifier(fieldSpec), field: fieldSpec.field || null, fieldName: fieldName, storeFieldName: storeFieldName, variables: variables, isReference: isReference, toReference: toReference, storage: storage, cache: policies.cache, canRead: canRead, readField: function () { return policies.readField(normalizeReadFieldOptions(arguments, objectOrReference, variables), context); }, mergeObjects: makeMergeObjectsFunction(context.store), }; } export function normalizeReadFieldOptions(readFieldArgs, objectOrReference, variables) { var fieldNameOrOptions = readFieldArgs[0], from = readFieldArgs[1], argc = readFieldArgs.length; var options; if (typeof fieldNameOrOptions === "string") { options = { fieldName: fieldNameOrOptions, // Default to objectOrReference only when no second argument was // passed for the from parameter, not when undefined is explicitly // passed as the second argument. from: argc > 1 ? from : objectOrReference, }; } else { options = __assign({}, fieldNameOrOptions); // Default to objectOrReference only when fieldNameOrOptions.from is // actually omitted, rather than just undefined. if (!hasOwn.call(options, "from")) { options.from = objectOrReference; } } if (globalThis.__DEV__ !== false && options.from === void 0) { globalThis.__DEV__ !== false && invariant.warn(7, stringifyForDisplay(Array.from(readFieldArgs))); } if (void 0 === options.variables) { options.variables = variables; } return options; } function makeMergeObjectsFunction(store) { return function mergeObjects(existing, incoming) { if (isArray(existing) || isArray(incoming)) { throw newInvariantError(8); } // These dynamic checks are necessary because the parameters of a // custom merge function can easily have the any type, so the type // system cannot always enforce the StoreObject | Reference parameter // types of options.mergeObjects. if (isNonNullObject(existing) && isNonNullObject(incoming)) { var eType = store.getFieldValue(existing, "__typename"); var iType = store.getFieldValue(incoming, "__typename"); var typesDiffer = eType && iType && eType !== iType; if (typesDiffer) { return incoming; } if (isReference(existing) && storeValueIsStoreObject(incoming)) { // Update the normalized EntityStore for the entity identified by // existing.__ref, preferring/overwriting any fields contributed by the // newer incoming StoreObject. store.merge(existing.__ref, incoming); return existing; } if (storeValueIsStoreObject(existing) && isReference(incoming)) { // Update the normalized EntityStore for the entity identified by // incoming.__ref, taking fields from the older existing object only if // those fields are not already present in the newer StoreObject // identified by incoming.__ref. store.merge(existing, incoming.__ref); return incoming; } if (storeValueIsStoreObject(existing) && storeValueIsStoreObject(incoming)) { return __assign(__assign({}, existing), incoming); } } return incoming; }; } //# sourceMappingURL=policies.js.map