UNPKG

@apollo/client

Version:

A fully-featured caching GraphQL client.

331 lines 16 kB
import { __assign } from "tslib"; import { invariant, newInvariantError } from "../../utilities/globals/index.js"; import { Kind } from "graphql"; import { wrap } from "optimism"; import { isField, resultKeyNameFromField, isReference, makeReference, shouldInclude, addTypenameToDocument, getDefaultValues, getMainDefinition, getQueryDefinition, getFragmentFromSelection, maybeDeepFreeze, mergeDeepArray, DeepMerger, isNonNullObject, canUseWeakMap, compact, canonicalStringify, cacheSizes, } from "../../utilities/index.js"; import { maybeDependOnExistenceOfEntity, supportsResultCaching, } from "./entityStore.js"; import { isArray, extractFragmentContext, getTypenameFromStoreObject, shouldCanonizeResults, } from "./helpers.js"; import { MissingFieldError } from "../core/types/common.js"; import { ObjectCanon } from "./object-canon.js"; function execSelectionSetKeyArgs(options) { return [ options.selectionSet, options.objectOrReference, options.context, // We split out this property so we can pass different values // independently without modifying options.context itself. options.context.canonizeResults, ]; } var StoreReader = /** @class */ (function () { function StoreReader(config) { var _this = this; this.knownResults = new (canUseWeakMap ? WeakMap : Map)(); this.config = compact(config, { addTypename: config.addTypename !== false, canonizeResults: shouldCanonizeResults(config), }); this.canon = config.canon || new ObjectCanon(); // memoized functions in this class will be "garbage-collected" // by recreating the whole `StoreReader` in // `InMemoryCache.resetResultsCache` // (triggered from `InMemoryCache.gc` with `resetResultCache: true`) this.executeSelectionSet = wrap(function (options) { var _a; var canonizeResults = options.context.canonizeResults; var peekArgs = execSelectionSetKeyArgs(options); // Negate this boolean option so we can find out if we've already read // this result using the other boolean value. peekArgs[3] = !canonizeResults; var other = (_a = _this.executeSelectionSet).peek.apply(_a, peekArgs); if (other) { if (canonizeResults) { return __assign(__assign({}, other), { // If we previously read this result without canonizing it, we can // reuse that result simply by canonizing it now. result: _this.canon.admit(other.result) }); } // If we previously read this result with canonization enabled, we can // return that canonized result as-is. return other; } maybeDependOnExistenceOfEntity(options.context.store, options.enclosingRef.__ref); // Finally, if we didn't find any useful previous results, run the real // execSelectionSetImpl method with the given options. return _this.execSelectionSetImpl(options); }, { max: this.config.resultCacheMaxSize || cacheSizes["inMemoryCache.executeSelectionSet"] || 50000 /* defaultCacheSizes["inMemoryCache.executeSelectionSet"] */, keyArgs: execSelectionSetKeyArgs, // Note that the parameters of makeCacheKey are determined by the // array returned by keyArgs. makeCacheKey: function (selectionSet, parent, context, canonizeResults) { if (supportsResultCaching(context.store)) { return context.store.makeCacheKey(selectionSet, isReference(parent) ? parent.__ref : parent, context.varString, canonizeResults); } }, }); this.executeSubSelectedArray = wrap(function (options) { maybeDependOnExistenceOfEntity(options.context.store, options.enclosingRef.__ref); return _this.execSubSelectedArrayImpl(options); }, { max: this.config.resultCacheMaxSize || cacheSizes["inMemoryCache.executeSubSelectedArray"] || 10000 /* defaultCacheSizes["inMemoryCache.executeSubSelectedArray"] */, makeCacheKey: function (_a) { var field = _a.field, array = _a.array, context = _a.context; if (supportsResultCaching(context.store)) { return context.store.makeCacheKey(field, array, context.varString); } }, }); } StoreReader.prototype.resetCanon = function () { this.canon = new ObjectCanon(); }; /** * Given a store and a query, return as much of the result as possible and * identify if any data was missing from the store. */ StoreReader.prototype.diffQueryAgainstStore = function (_a) { var store = _a.store, query = _a.query, _b = _a.rootId, rootId = _b === void 0 ? "ROOT_QUERY" : _b, variables = _a.variables, _c = _a.returnPartialData, returnPartialData = _c === void 0 ? true : _c, _d = _a.canonizeResults, canonizeResults = _d === void 0 ? this.config.canonizeResults : _d; var policies = this.config.cache.policies; variables = __assign(__assign({}, getDefaultValues(getQueryDefinition(query))), variables); var rootRef = makeReference(rootId); var execResult = this.executeSelectionSet({ selectionSet: getMainDefinition(query).selectionSet, objectOrReference: rootRef, enclosingRef: rootRef, context: __assign({ store: store, query: query, policies: policies, variables: variables, varString: canonicalStringify(variables), canonizeResults: canonizeResults }, extractFragmentContext(query, this.config.fragments)), }); var missing; if (execResult.missing) { // For backwards compatibility we still report an array of // MissingFieldError objects, even though there will only ever be at most // one of them, now that all missing field error messages are grouped // together in the execResult.missing tree. missing = [ new MissingFieldError(firstMissing(execResult.missing), execResult.missing, query, variables), ]; if (!returnPartialData) { throw missing[0]; } } return { result: execResult.result, complete: !missing, missing: missing, }; }; StoreReader.prototype.isFresh = function (result, parent, selectionSet, context) { if (supportsResultCaching(context.store) && this.knownResults.get(result) === selectionSet) { var latest = this.executeSelectionSet.peek(selectionSet, parent, context, // If result is canonical, then it could only have been previously // cached by the canonizing version of executeSelectionSet, so we can // avoid checking both possibilities here. this.canon.isKnown(result)); if (latest && result === latest.result) { return true; } } return false; }; // Uncached version of executeSelectionSet. StoreReader.prototype.execSelectionSetImpl = function (_a) { var _this = this; var selectionSet = _a.selectionSet, objectOrReference = _a.objectOrReference, enclosingRef = _a.enclosingRef, context = _a.context; if (isReference(objectOrReference) && !context.policies.rootTypenamesById[objectOrReference.__ref] && !context.store.has(objectOrReference.__ref)) { return { result: this.canon.empty, missing: "Dangling reference to missing ".concat(objectOrReference.__ref, " object"), }; } var variables = context.variables, policies = context.policies, store = context.store; var typename = store.getFieldValue(objectOrReference, "__typename"); var objectsToMerge = []; var missing; var missingMerger = new DeepMerger(); if (this.config.addTypename && typeof typename === "string" && !policies.rootIdsByTypename[typename]) { // Ensure we always include a default value for the __typename // field, if we have one, and this.config.addTypename is true. Note // that this field can be overridden by other merged objects. objectsToMerge.push({ __typename: typename }); } function handleMissing(result, resultName) { var _a; if (result.missing) { missing = missingMerger.merge(missing, (_a = {}, _a[resultName] = result.missing, _a)); } return result.result; } var workSet = new Set(selectionSet.selections); workSet.forEach(function (selection) { var _a, _b; // Omit fields with directives @skip(if: <truthy value>) or // @include(if: <falsy value>). if (!shouldInclude(selection, variables)) return; if (isField(selection)) { var fieldValue = policies.readField({ fieldName: selection.name.value, field: selection, variables: context.variables, from: objectOrReference, }, context); var resultName = resultKeyNameFromField(selection); if (fieldValue === void 0) { if (!addTypenameToDocument.added(selection)) { missing = missingMerger.merge(missing, (_a = {}, _a[resultName] = "Can't find field '".concat(selection.name.value, "' on ").concat(isReference(objectOrReference) ? objectOrReference.__ref + " object" : "object " + JSON.stringify(objectOrReference, null, 2)), _a)); } } else if (isArray(fieldValue)) { if (fieldValue.length > 0) { fieldValue = handleMissing(_this.executeSubSelectedArray({ field: selection, array: fieldValue, enclosingRef: enclosingRef, context: context, }), resultName); } } else if (!selection.selectionSet) { // If the field does not have a selection set, then we handle it // as a scalar value. To keep this.canon from canonicalizing // this value, we use this.canon.pass to wrap fieldValue in a // Pass object that this.canon.admit will later unwrap as-is. if (context.canonizeResults) { fieldValue = _this.canon.pass(fieldValue); } } else if (fieldValue != null) { // In this case, because we know the field has a selection set, // it must be trying to query a GraphQLObjectType, which is why // fieldValue must be != null. fieldValue = handleMissing(_this.executeSelectionSet({ selectionSet: selection.selectionSet, objectOrReference: fieldValue, enclosingRef: isReference(fieldValue) ? fieldValue : enclosingRef, context: context, }), resultName); } if (fieldValue !== void 0) { objectsToMerge.push((_b = {}, _b[resultName] = fieldValue, _b)); } } else { var fragment = getFragmentFromSelection(selection, context.lookupFragment); if (!fragment && selection.kind === Kind.FRAGMENT_SPREAD) { throw newInvariantError(9, selection.name.value); } if (fragment && policies.fragmentMatches(fragment, typename)) { fragment.selectionSet.selections.forEach(workSet.add, workSet); } } }); var result = mergeDeepArray(objectsToMerge); var finalResult = { result: result, missing: missing }; var frozen = context.canonizeResults ? this.canon.admit(finalResult) // Since this.canon is normally responsible for freezing results (only in // development), freeze them manually if canonization is disabled. : maybeDeepFreeze(finalResult); // Store this result with its selection set so that we can quickly // recognize it again in the StoreReader#isFresh method. if (frozen.result) { this.knownResults.set(frozen.result, selectionSet); } return frozen; }; // Uncached version of executeSubSelectedArray. StoreReader.prototype.execSubSelectedArrayImpl = function (_a) { var _this = this; var field = _a.field, array = _a.array, enclosingRef = _a.enclosingRef, context = _a.context; var missing; var missingMerger = new DeepMerger(); function handleMissing(childResult, i) { var _a; if (childResult.missing) { missing = missingMerger.merge(missing, (_a = {}, _a[i] = childResult.missing, _a)); } return childResult.result; } if (field.selectionSet) { array = array.filter(context.store.canRead); } array = array.map(function (item, i) { // null value in array if (item === null) { return null; } // This is a nested array, recurse if (isArray(item)) { return handleMissing(_this.executeSubSelectedArray({ field: field, array: item, enclosingRef: enclosingRef, context: context, }), i); } // This is an object, run the selection set on it if (field.selectionSet) { return handleMissing(_this.executeSelectionSet({ selectionSet: field.selectionSet, objectOrReference: item, enclosingRef: isReference(item) ? item : enclosingRef, context: context, }), i); } if (globalThis.__DEV__ !== false) { assertSelectionSetForIdValue(context.store, field, item); } return item; }); return { result: context.canonizeResults ? this.canon.admit(array) : array, missing: missing, }; }; return StoreReader; }()); export { StoreReader }; function firstMissing(tree) { try { JSON.stringify(tree, function (_, value) { if (typeof value === "string") throw value; return value; }); } catch (result) { return result; } } function assertSelectionSetForIdValue(store, field, fieldValue) { if (!field.selectionSet) { var workSet_1 = new Set([fieldValue]); workSet_1.forEach(function (value) { if (isNonNullObject(value)) { invariant( !isReference(value), 10, getTypenameFromStoreObject(store, value), field.name.value ); Object.values(value).forEach(workSet_1.add, workSet_1); } }); } } //# sourceMappingURL=readFromStore.js.map