packages/relay-runtime/store/RelayReader.js (838 lines of code) (raw):

/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local * @format */ // flowlint ambiguous-object-type:error 'use strict'; import type { ReaderActorChange, ReaderClientEdgeToClientObject, ReaderClientEdgeToServerObject, ReaderFlightField, ReaderFragment, ReaderFragmentSpread, ReaderInlineDataFragmentSpread, ReaderLinkedField, ReaderModuleImport, ReaderNode, ReaderRelayLiveResolver, ReaderRelayResolver, ReaderRequiredField, ReaderScalarField, ReaderSelection, } from '../util/ReaderNode'; import type {DataID, Variables} from '../util/RelayRuntimeTypes'; import type { ClientEdgeTraversalInfo, DataIDSet, MissingClientEdgeRequestInfo, MissingRequiredFields, Record, RecordSource, RelayResolverErrors, RequestDescriptor, SelectorData, SingularReaderSelector, Snapshot, } from './RelayStoreTypes'; import type {EvaluationResult, ResolverCache} from './ResolverCache'; const { ACTOR_CHANGE, CLIENT_EDGE_TO_CLIENT_OBJECT, CLIENT_EDGE_TO_SERVER_OBJECT, CLIENT_EXTENSION, CONDITION, DEFER, FLIGHT_FIELD, FRAGMENT_SPREAD, INLINE_DATA_FRAGMENT_SPREAD, INLINE_FRAGMENT, LINKED_FIELD, MODULE_IMPORT, RELAY_LIVE_RESOLVER, RELAY_RESOLVER, REQUIRED_FIELD, SCALAR_FIELD, STREAM, } = require('../util/RelayConcreteNode'); const RelayFeatureFlags = require('../util/RelayFeatureFlags'); const ClientID = require('./ClientID'); const RelayModernRecord = require('./RelayModernRecord'); const {getReactFlightClientResponse} = require('./RelayStoreReactFlightUtils'); const { CLIENT_EDGE_TRAVERSAL_PATH, FRAGMENT_OWNER_KEY, FRAGMENT_PROP_NAME_KEY, FRAGMENTS_KEY, ID_KEY, IS_WITHIN_UNMATCHED_TYPE_REFINEMENT, MODULE_COMPONENT_KEY, ROOT_ID, getArgumentValues, getModuleComponentKey, getStorageKey, } = require('./RelayStoreUtils'); const {NoopResolverCache} = require('./ResolverCache'); const {withResolverContext} = require('./ResolverFragments'); const {generateTypeID} = require('./TypeID'); const invariant = require('invariant'); function read( recordSource: RecordSource, selector: SingularReaderSelector, resolverCache?: ResolverCache, ): Snapshot { const reader = new RelayReader( recordSource, selector, resolverCache ?? new NoopResolverCache(), ); return reader.read(); } /** * @private */ class RelayReader { _clientEdgeTraversalPath: Array<ClientEdgeTraversalInfo | null>; _isMissingData: boolean; _missingClientEdges: Array<MissingClientEdgeRequestInfo>; _isWithinUnmatchedTypeRefinement: boolean; _missingRequiredFields: ?MissingRequiredFields; _owner: RequestDescriptor; _recordSource: RecordSource; _seenRecords: DataIDSet; _selector: SingularReaderSelector; _variables: Variables; _resolverCache: ResolverCache; _resolverErrors: RelayResolverErrors; _fragmentName: string; constructor( recordSource: RecordSource, selector: SingularReaderSelector, resolverCache: ResolverCache, ) { this._clientEdgeTraversalPath = RelayFeatureFlags.ENABLE_CLIENT_EDGES && selector.clientEdgeTraversalPath?.length ? [...selector.clientEdgeTraversalPath] : []; this._missingClientEdges = []; this._isMissingData = false; this._isWithinUnmatchedTypeRefinement = false; this._missingRequiredFields = null; this._owner = selector.owner; this._recordSource = recordSource; this._seenRecords = new Set(); this._selector = selector; this._variables = selector.variables; this._resolverCache = resolverCache; this._resolverErrors = []; this._fragmentName = selector.node.name; } read(): Snapshot { const {node, dataID, isWithinUnmatchedTypeRefinement} = this._selector; const {abstractKey} = node; const record = this._recordSource.get(dataID); // Relay historically allowed child fragments to be read even if the root object // did not match the type of the fragment: either the root object has a different // concrete type than the fragment (for concrete fragments) or the root object does // not conform to the interface/union for abstract fragments. // For suspense purposes, however, we want to accurately compute whether any data // is missing: but if the fragment type doesn't match (or a parent type didn't // match), then no data is expected to be present. // By default data is expected to be present unless this selector was read out // from within a non-matching type refinement in a parent fragment: let isDataExpectedToBePresent = !isWithinUnmatchedTypeRefinement; // If this is a concrete fragment and the concrete type of the record does not // match, then no data is expected to be present. if (isDataExpectedToBePresent && abstractKey == null && record != null) { const recordType = RelayModernRecord.getType(record); if ( recordType !== node.type && // The root record type is a special `__Root` type and may not match the // type on the ast, so ignore type mismatches at the root. // We currently detect whether we're at the root by checking against ROOT_ID, // but this does not work for mutations/subscriptions which generate unique // root ids. This is acceptable in practice as we don't read data for mutations/ // subscriptions in a situation where we would use isMissingData to decide whether // to suspend or not. // TODO T96653810: Correctly detect reading from root of mutation/subscription dataID !== ROOT_ID ) { isDataExpectedToBePresent = false; } } // If this is an abstract fragment (and the precise refinement GK is enabled) // then data is only expected to be present if the record type is known to // implement the interface. If we aren't sure whether the record implements // the interface, that itself constitutes "expected" data being missing. if (isDataExpectedToBePresent && abstractKey != null && record != null) { const recordType = RelayModernRecord.getType(record); const typeID = generateTypeID(recordType); const typeRecord = this._recordSource.get(typeID); const implementsInterface = typeRecord != null ? RelayModernRecord.getValue(typeRecord, abstractKey) : null; if (implementsInterface === false) { // Type known to not implement the interface isDataExpectedToBePresent = false; } else if (implementsInterface == null) { // Don't know if the type implements the interface or not this._isMissingData = true; } } this._isWithinUnmatchedTypeRefinement = !isDataExpectedToBePresent; const data = this._traverse(node, dataID, null); return { data, isMissingData: this._isMissingData && isDataExpectedToBePresent, missingClientEdges: RelayFeatureFlags.ENABLE_CLIENT_EDGES && this._missingClientEdges.length ? this._missingClientEdges : null, seenRecords: this._seenRecords, selector: this._selector, missingRequiredFields: this._missingRequiredFields, relayResolverErrors: this._resolverErrors, }; } _markDataAsMissing(): void { this._isMissingData = true; if ( RelayFeatureFlags.ENABLE_CLIENT_EDGES && this._clientEdgeTraversalPath.length ) { const top = this._clientEdgeTraversalPath[this._clientEdgeTraversalPath.length - 1]; // Top can be null if we've traversed past a client edge into an ordinary // client extension field; we never want to fetch in response to missing // data off of a client extension field. if (top !== null) { this._missingClientEdges.push({ request: top.readerClientEdge.operation, clientEdgeDestinationID: top.clientEdgeDestinationID, }); } } } _traverse( node: ReaderNode, dataID: DataID, prevData: ?SelectorData, ): ?SelectorData { const record = this._recordSource.get(dataID); this._seenRecords.add(dataID); if (record == null) { if (record === undefined) { this._markDataAsMissing(); } return record; } const data = prevData || {}; const hadRequiredData = this._traverseSelections( node.selections, record, data, ); return hadRequiredData ? data : null; } _getVariableValue(name: string): mixed { invariant( this._variables.hasOwnProperty(name), 'RelayReader(): Undefined variable `%s`.', name, ); return this._variables[name]; } _maybeReportUnexpectedNull( fieldPath: string, action: 'LOG' | 'THROW', record: Record, ) { if (this._missingRequiredFields?.action === 'THROW') { // Chained @required directives may cause a parent `@required(action: // THROW)` field to become null, so the first missing field we // encounter is likely to be the root cause of the error. return; } const owner = this._fragmentName; switch (action) { case 'THROW': this._missingRequiredFields = {action, field: {path: fieldPath, owner}}; return; case 'LOG': if (this._missingRequiredFields == null) { this._missingRequiredFields = { action, fields: [{path: fieldPath, owner}], }; } else { this._missingRequiredFields = { action, fields: [ ...this._missingRequiredFields.fields, {path: fieldPath, owner}, ], }; } return; default: (action: empty); } } _traverseSelections( selections: $ReadOnlyArray<ReaderSelection>, record: Record, data: SelectorData, ): boolean /* had all expected data */ { for (let i = 0; i < selections.length; i++) { const selection = selections[i]; switch (selection.kind) { case REQUIRED_FIELD: const fieldValue = this._readRequiredField(selection, record, data); if (fieldValue == null) { const {action} = selection; if (action !== 'NONE') { this._maybeReportUnexpectedNull(selection.path, action, record); } // We are going to throw, or our parent is going to get nulled out. // Either way, sibling values are going to be ignored, so we can // bail early here as an optimization. return false; } break; case SCALAR_FIELD: this._readScalar(selection, record, data); break; case LINKED_FIELD: if (selection.plural) { this._readPluralLink(selection, record, data); } else { this._readLink(selection, record, data); } break; case CONDITION: const conditionValue = Boolean( this._getVariableValue(selection.condition), ); if (conditionValue === selection.passingValue) { const hasExpectedData = this._traverseSelections( selection.selections, record, data, ); if (!hasExpectedData) { return false; } } break; case INLINE_FRAGMENT: { const {abstractKey} = selection; if (abstractKey == null) { // concrete type refinement: only read data if the type exactly matches const typeName = RelayModernRecord.getType(record); if (typeName != null && typeName === selection.type) { const hasExpectedData = this._traverseSelections( selection.selections, record, data, ); if (!hasExpectedData) { return false; } } } else { // Similar to the logic in read(): data is only expected to be present // if the record is known to conform to the interface. If we don't know // whether the type conforms or not, that constitutes missing data. // store flags to reset after reading const parentIsMissingData = this._isMissingData; const parentIsWithinUnmatchedTypeRefinement = this._isWithinUnmatchedTypeRefinement; const typeName = RelayModernRecord.getType(record); const typeID = generateTypeID(typeName); const typeRecord = this._recordSource.get(typeID); const implementsInterface = typeRecord != null ? RelayModernRecord.getValue(typeRecord, abstractKey) : null; this._isWithinUnmatchedTypeRefinement = parentIsWithinUnmatchedTypeRefinement || implementsInterface === false; this._traverseSelections(selection.selections, record, data); this._isWithinUnmatchedTypeRefinement = parentIsWithinUnmatchedTypeRefinement; if (implementsInterface === false) { // Type known to not implement the interface, no data expected this._isMissingData = parentIsMissingData; } else if (implementsInterface == null) { // Don't know if the type implements the interface or not this._markDataAsMissing(); } } break; } case RELAY_LIVE_RESOLVER: case RELAY_RESOLVER: { if (!RelayFeatureFlags.ENABLE_RELAY_RESOLVERS) { throw new Error('Relay Resolver fields are not yet supported.'); } this._readResolverField(selection, record, data); break; } case FRAGMENT_SPREAD: this._createFragmentPointer(selection, record, data); break; case MODULE_IMPORT: this._readModuleImport(selection, record, data); break; case INLINE_DATA_FRAGMENT_SPREAD: this._createInlineDataOrResolverFragmentPointer( selection, record, data, ); break; case DEFER: case CLIENT_EXTENSION: { const isMissingData = this._isMissingData; const alreadyMissingClientEdges = this._missingClientEdges.length; if (RelayFeatureFlags.ENABLE_CLIENT_EDGES) { this._clientEdgeTraversalPath.push(null); } const hasExpectedData = this._traverseSelections( selection.selections, record, data, ); // The only case where we want to suspend due to missing data off of // a client extension is if we reached a client edge that we might be // able to fetch: this._isMissingData = isMissingData || this._missingClientEdges.length > alreadyMissingClientEdges; if (RelayFeatureFlags.ENABLE_CLIENT_EDGES) { this._clientEdgeTraversalPath.pop(); } if (!hasExpectedData) { return false; } break; } case STREAM: { const hasExpectedData = this._traverseSelections( selection.selections, record, data, ); if (!hasExpectedData) { return false; } break; } case FLIGHT_FIELD: if (RelayFeatureFlags.ENABLE_REACT_FLIGHT_COMPONENT_FIELD) { this._readFlightField(selection, record, data); } else { throw new Error('Flight fields are not yet supported.'); } break; case ACTOR_CHANGE: this._readActorChange(selection, record, data); break; case CLIENT_EDGE_TO_CLIENT_OBJECT: case CLIENT_EDGE_TO_SERVER_OBJECT: if (RelayFeatureFlags.ENABLE_CLIENT_EDGES) { this._readClientEdge(selection, record, data); } else { throw new Error('Client edges are not yet supported.'); } break; default: (selection: empty); invariant( false, 'RelayReader(): Unexpected ast kind `%s`.', selection.kind, ); } } return true; } _readRequiredField( selection: ReaderRequiredField, record: Record, data: SelectorData, ): ?mixed { switch (selection.field.kind) { case SCALAR_FIELD: return this._readScalar(selection.field, record, data); case LINKED_FIELD: if (selection.field.plural) { return this._readPluralLink(selection.field, record, data); } else { return this._readLink(selection.field, record, data); } case RELAY_RESOLVER: if (!RelayFeatureFlags.ENABLE_RELAY_RESOLVERS) { throw new Error('Relay Resolver fields are not yet supported.'); } return this._readResolverField(selection.field, record, data); case RELAY_LIVE_RESOLVER: if (!RelayFeatureFlags.ENABLE_RELAY_RESOLVERS) { throw new Error('Relay Resolver fields are not yet supported.'); } return this._readResolverField(selection.field, record, data); default: (selection.field.kind: empty); invariant( false, 'RelayReader(): Unexpected ast kind `%s`.', selection.kind, ); } } _readResolverField( field: ReaderRelayResolver | ReaderRelayLiveResolver, record: Record, data: SelectorData, ): mixed { const {resolverModule, fragment} = field; const storageKey = getStorageKey(field, this._variables); const resolverID = ClientID.generateClientID( RelayModernRecord.getDataID(record), storageKey, ); // Found when reading the resolver fragment, which can happen either when // evaluating the resolver and it calls readFragment, or when checking if the // inputs have changed since a previous evaluation: let snapshot: ?Snapshot; const getDataForResolverFragment = singularReaderSelector => { if (snapshot != null) { // It was already read when checking for input staleness; no need to read it again. // Note that the variables like fragmentSeenRecordIDs in the outer closure will have // already been set and will still be used in this case. return snapshot.data; } snapshot = read( this._recordSource, singularReaderSelector, this._resolverCache, ); return snapshot.data; }; const resolverContext = {getDataForResolverFragment}; const evaluate = (): EvaluationResult<mixed> => { const key = { __id: RelayModernRecord.getDataID(record), __fragmentOwner: this._owner, __fragments: { [fragment.name]: {}, // Arguments to this fragment; not yet supported. }, }; return withResolverContext(resolverContext, () => { let resolverResult = null; let resolverError = null; try { // $FlowFixMe[prop-missing] - resolver module's type signature is a lie resolverResult = resolverModule(key); } catch (e) { // `field.path` is typed as nullable while we rollout compiler changes. const path = field.path ?? '[UNKNOWN]'; resolverError = { field: {path, owner: this._fragmentName}, error: e, }; } return { resolverResult, snapshot: snapshot, resolverID, error: resolverError, }; }); }; const [result, seenRecord, resolverError, cachedSnapshot] = this._resolverCache.readFromCacheOrEvaluate( record, field, this._variables, evaluate, getDataForResolverFragment, ); if (cachedSnapshot != null) { if (cachedSnapshot.missingRequiredFields != null) { this._addMissingRequiredFields(cachedSnapshot.missingRequiredFields); } if (cachedSnapshot.missingClientEdges != null) { for (const missing of cachedSnapshot.missingClientEdges) { this._missingClientEdges.push(missing); } } for (const error of cachedSnapshot.relayResolverErrors) { this._resolverErrors.push(error); } this._isMissingData = this._isMissingData || cachedSnapshot.isMissingData; } if (resolverError) { this._resolverErrors.push(resolverError); } if (seenRecord != null) { this._seenRecords.add(seenRecord); } const applicationName = field.alias ?? field.name; data[applicationName] = result; return result; } _readClientEdge( field: ReaderClientEdgeToServerObject | ReaderClientEdgeToClientObject, record: Record, data: SelectorData, ): void { const backingField = field.backingField; // Because ReaderClientExtension doesn't have `alias` or `name` and so I don't know // how to get its applicationName or storageKey yet: invariant( backingField.kind !== 'ClientExtension', 'Client extension client edges are not yet implemented.', ); const applicationName = backingField.alias ?? backingField.name; const backingFieldData = {}; this._traverseSelections([backingField], record, backingFieldData); let destinationDataID = backingFieldData[applicationName]; if (destinationDataID == null) { data[applicationName] = destinationDataID; return; } invariant( typeof destinationDataID === 'string', 'Plural client edges not are yet implemented', ); // FIXME support plural if (field.kind === CLIENT_EDGE_TO_CLIENT_OBJECT) { // Client objects might use ids that are not gobally unique and instead are just // local within their type. ResolverCache will derive a namespaced ID for us. destinationDataID = this._resolverCache.createClientRecord( destinationDataID, field.concreteType, ); this._clientEdgeTraversalPath.push(null); } else { // Not wrapping the push/pop in a try/finally because if we throw, the // Reader object is not usable after that anyway. this._clientEdgeTraversalPath.push({ readerClientEdge: field, clientEdgeDestinationID: destinationDataID, }); } const prevData = data[applicationName]; invariant( prevData == null || typeof prevData === 'object', 'RelayReader(): Expected data for field `%s` on record `%s` ' + 'to be an object, got `%s`.', applicationName, RelayModernRecord.getDataID(record), prevData, ); const value = this._traverse( field.linkedField, destinationDataID, // $FlowFixMe[incompatible-variance] prevData, ); data[applicationName] = value; this._clientEdgeTraversalPath.pop(); } _readFlightField( field: ReaderFlightField, record: Record, data: SelectorData, ): ?mixed { const applicationName = field.alias ?? field.name; const storageKey = getStorageKey(field, this._variables); const reactFlightClientResponseRecordID = RelayModernRecord.getLinkedRecordID(record, storageKey); if (reactFlightClientResponseRecordID == null) { data[applicationName] = reactFlightClientResponseRecordID; if (reactFlightClientResponseRecordID === undefined) { this._markDataAsMissing(); } return reactFlightClientResponseRecordID; } const reactFlightClientResponseRecord = this._recordSource.get( reactFlightClientResponseRecordID, ); this._seenRecords.add(reactFlightClientResponseRecordID); if (reactFlightClientResponseRecord == null) { data[applicationName] = reactFlightClientResponseRecord; if (reactFlightClientResponseRecord === undefined) { this._markDataAsMissing(); } return reactFlightClientResponseRecord; } const clientResponse = getReactFlightClientResponse( reactFlightClientResponseRecord, ); data[applicationName] = clientResponse; return clientResponse; } _readScalar( field: ReaderScalarField, record: Record, data: SelectorData, ): ?mixed { const applicationName = field.alias ?? field.name; const storageKey = getStorageKey(field, this._variables); const value = RelayModernRecord.getValue(record, storageKey); if (value === undefined) { this._markDataAsMissing(); } data[applicationName] = value; return value; } _readLink( field: ReaderLinkedField, record: Record, data: SelectorData, ): ?mixed { const applicationName = field.alias ?? field.name; const storageKey = getStorageKey(field, this._variables); const linkedID = RelayModernRecord.getLinkedRecordID(record, storageKey); if (linkedID == null) { data[applicationName] = linkedID; if (linkedID === undefined) { this._markDataAsMissing(); } return linkedID; } const prevData = data[applicationName]; invariant( prevData == null || typeof prevData === 'object', 'RelayReader(): Expected data for field `%s` on record `%s` ' + 'to be an object, got `%s`.', applicationName, RelayModernRecord.getDataID(record), prevData, ); // $FlowFixMe[incompatible-variance] const value = this._traverse(field, linkedID, prevData); data[applicationName] = value; return value; } _readActorChange( field: ReaderActorChange, record: Record, data: SelectorData, ): ?mixed { const applicationName = field.alias ?? field.name; const storageKey = getStorageKey(field, this._variables); const externalRef = RelayModernRecord.getActorLinkedRecordID( record, storageKey, ); if (externalRef == null) { data[applicationName] = externalRef; if (externalRef === undefined) { this._markDataAsMissing(); } return data[applicationName]; } const [actorIdentifier, dataID] = externalRef; const fragmentRef = {}; this._createFragmentPointer( field.fragmentSpread, { __id: dataID, }, fragmentRef, ); data[applicationName] = { __fragmentRef: fragmentRef, __viewer: actorIdentifier, }; return data[applicationName]; } _readPluralLink( field: ReaderLinkedField, record: Record, data: SelectorData, ): ?mixed { const applicationName = field.alias ?? field.name; const storageKey = getStorageKey(field, this._variables); const linkedIDs = RelayModernRecord.getLinkedRecordIDs(record, storageKey); if (linkedIDs == null) { data[applicationName] = linkedIDs; if (linkedIDs === undefined) { this._markDataAsMissing(); } return linkedIDs; } const prevData = data[applicationName]; invariant( prevData == null || Array.isArray(prevData), 'RelayReader(): Expected data for field `%s` on record `%s` ' + 'to be an array, got `%s`.', applicationName, RelayModernRecord.getDataID(record), prevData, ); const linkedArray = prevData || []; linkedIDs.forEach((linkedID, nextIndex) => { if (linkedID == null) { if (linkedID === undefined) { this._markDataAsMissing(); } // $FlowFixMe[cannot-write] linkedArray[nextIndex] = linkedID; return; } const prevItem = linkedArray[nextIndex]; invariant( prevItem == null || typeof prevItem === 'object', 'RelayReader(): Expected data for field `%s` on record `%s` ' + 'to be an object, got `%s`.', applicationName, RelayModernRecord.getDataID(record), prevItem, ); // $FlowFixMe[cannot-write] // $FlowFixMe[incompatible-variance] linkedArray[nextIndex] = this._traverse(field, linkedID, prevItem); }); data[applicationName] = linkedArray; return linkedArray; } /** * Reads a ReaderModuleImport, which was generated from using the @module * directive. */ _readModuleImport( moduleImport: ReaderModuleImport, record: Record, data: SelectorData, ): void { // Determine the component module from the store: if the field is missing // it means we don't know what component to render the match with. const componentKey = getModuleComponentKey(moduleImport.documentName); const component = RelayModernRecord.getValue(record, componentKey); if (component == null) { if (component === undefined) { this._markDataAsMissing(); } return; } // Otherwise, read the fragment and module associated to the concrete // type, and put that data with the result: // - For the matched fragment, create the relevant fragment pointer and add // the expected fragmentPropName // - For the matched module, create a reference to the module this._createFragmentPointer( { kind: 'FragmentSpread', name: moduleImport.fragmentName, args: moduleImport.args, }, record, data, ); data[FRAGMENT_PROP_NAME_KEY] = moduleImport.fragmentPropName; data[MODULE_COMPONENT_KEY] = component; } _createFragmentPointer( fragmentSpread: ReaderFragmentSpread, record: Record, data: SelectorData, ): void { let fragmentPointers = data[FRAGMENTS_KEY]; if (fragmentPointers == null) { fragmentPointers = data[FRAGMENTS_KEY] = {}; } invariant( typeof fragmentPointers === 'object' && fragmentPointers != null, 'RelayReader: Expected fragment spread data to be an object, got `%s`.', fragmentPointers, ); if (data[ID_KEY] == null) { data[ID_KEY] = RelayModernRecord.getDataID(record); } // $FlowFixMe[cannot-write] - writing into read-only field fragmentPointers[fragmentSpread.name] = fragmentSpread.args ? getArgumentValues(fragmentSpread.args, this._variables) : {}; data[FRAGMENT_OWNER_KEY] = this._owner; data[IS_WITHIN_UNMATCHED_TYPE_REFINEMENT] = this._isWithinUnmatchedTypeRefinement; if (RelayFeatureFlags.ENABLE_CLIENT_EDGES) { if ( this._clientEdgeTraversalPath.length > 0 && this._clientEdgeTraversalPath[ this._clientEdgeTraversalPath.length - 1 ] !== null ) { data[CLIENT_EDGE_TRAVERSAL_PATH] = [...this._clientEdgeTraversalPath]; } } } _createInlineDataOrResolverFragmentPointer( fragmentSpreadOrFragment: ReaderInlineDataFragmentSpread | ReaderFragment, record: Record, data: SelectorData, ): void { let fragmentPointers = data[FRAGMENTS_KEY]; if (fragmentPointers == null) { fragmentPointers = data[FRAGMENTS_KEY] = {}; } invariant( typeof fragmentPointers === 'object' && fragmentPointers != null, 'RelayReader: Expected fragment spread data to be an object, got `%s`.', fragmentPointers, ); if (data[ID_KEY] == null) { data[ID_KEY] = RelayModernRecord.getDataID(record); } const inlineData = {}; const parentFragmentName = this._fragmentName; this._fragmentName = fragmentSpreadOrFragment.name; this._traverseSelections( fragmentSpreadOrFragment.selections, record, inlineData, ); this._fragmentName = parentFragmentName; // $FlowFixMe[cannot-write] - writing into read-only field fragmentPointers[fragmentSpreadOrFragment.name] = inlineData; } _addMissingRequiredFields(additional: MissingRequiredFields) { if (this._missingRequiredFields == null) { this._missingRequiredFields = additional; return; } if (this._missingRequiredFields.action === 'THROW') { return; } if (additional.action === 'THROW') { this._missingRequiredFields = additional; return; } this._missingRequiredFields = { action: 'LOG', fields: [...this._missingRequiredFields.fields, ...additional.fields], }; } } module.exports = {read};