packages/relay-runtime/store/ResolverCache.js (252 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 { ReaderRelayLiveResolver, ReaderRelayResolver, } from '../util/ReaderNode'; import type {DataID, Variables} from '../util/RelayRuntimeTypes'; import type { MutableRecordSource, Record, RelayResolverError, SingularReaderSelector, Snapshot, } from './RelayStoreTypes'; const recycleNodesInto = require('../util/recycleNodesInto'); const {RELAY_LIVE_RESOLVER} = require('../util/RelayConcreteNode'); const {generateClientID} = require('./ClientID'); const RelayModernRecord = require('./RelayModernRecord'); const { RELAY_RESOLVER_ERROR_KEY, RELAY_RESOLVER_INVALIDATION_KEY, RELAY_RESOLVER_SNAPSHOT_KEY, RELAY_RESOLVER_VALUE_KEY, getStorageKey, } = require('./RelayStoreUtils'); const invariant = require('invariant'); const warning = require('warning'); type ResolverID = string; export type EvaluationResult<T> = {| resolverResult: T, resolverID: ResolverID, snapshot: ?Snapshot, error: ?RelayResolverError, |}; export interface ResolverCache { readFromCacheOrEvaluate<T>( record: Record, field: ReaderRelayResolver | ReaderRelayLiveResolver, variables: Variables, evaluate: () => EvaluationResult<T>, getDataForResolverFragment: (SingularReaderSelector) => mixed, ): [ T /* Answer */, ?DataID /* Seen record */, ?RelayResolverError, ?Snapshot, ]; invalidateDataIDs( updatedDataIDs: Set<DataID>, // Mutated in place ): void; createClientRecord(id: string, typename: string): string; } // $FlowFixMe[unclear-type] - will always be empty const emptySet: $ReadOnlySet<any> = new Set(); class NoopResolverCache implements ResolverCache { readFromCacheOrEvaluate<T>( record: Record, field: ReaderRelayResolver | ReaderRelayLiveResolver, variables: Variables, evaluate: () => EvaluationResult<T>, getDataForResolverFragment: SingularReaderSelector => mixed, ): [ T /* Answer */, ?DataID /* Seen record */, ?RelayResolverError, ?Snapshot, ] { invariant( field.kind !== RELAY_LIVE_RESOLVER, 'This store does not support Live Resolvers', ); const {resolverResult, snapshot, error} = evaluate(); return [resolverResult, undefined, error, snapshot]; } invalidateDataIDs(updatedDataIDs: Set<DataID>): void {} createClientRecord(id: string, typeName: string): string { invariant( false, 'Client Edges to Client Objects are not supported in this version of Relay Store', ); } } function addDependencyEdge( edges: Map<ResolverID, Set<DataID>> | Map<DataID, Set<ResolverID>>, from: ResolverID | DataID, to: ResolverID | DataID, ): void { let set = edges.get(from); if (!set) { set = new Set(); edges.set(from, set); } set.add(to); } class RecordResolverCache implements ResolverCache { _resolverIDToRecordIDs: Map<ResolverID, Set<DataID>>; _recordIDToResolverIDs: Map<DataID, Set<ResolverID>>; _getRecordSource: () => MutableRecordSource; constructor(getRecordSource: () => MutableRecordSource) { this._resolverIDToRecordIDs = new Map(); this._recordIDToResolverIDs = new Map(); this._getRecordSource = getRecordSource; } readFromCacheOrEvaluate<T>( record: Record, field: ReaderRelayResolver | ReaderRelayLiveResolver, variables: Variables, evaluate: () => EvaluationResult<T>, getDataForResolverFragment: SingularReaderSelector => mixed, ): [ T /* Answer */, ?DataID /* Seen record */, ?RelayResolverError, ?Snapshot, ] { const recordSource = this._getRecordSource(); const recordID = RelayModernRecord.getDataID(record); const storageKey = getStorageKey(field, variables); let linkedID = RelayModernRecord.getLinkedRecordID(record, storageKey); let linkedRecord = linkedID == null ? null : recordSource.get(linkedID); if ( linkedRecord == null || this._isInvalid(linkedRecord, getDataForResolverFragment) ) { // Cache miss; evaluate the selector and store the result in a new record: linkedID = linkedID ?? generateClientID(recordID, storageKey); linkedRecord = RelayModernRecord.create(linkedID, '__RELAY_RESOLVER__'); const evaluationResult = evaluate(); RelayModernRecord.setValue( linkedRecord, RELAY_RESOLVER_VALUE_KEY, evaluationResult.resolverResult, ); RelayModernRecord.setValue( linkedRecord, RELAY_RESOLVER_SNAPSHOT_KEY, evaluationResult.snapshot, ); RelayModernRecord.setValue( linkedRecord, RELAY_RESOLVER_ERROR_KEY, evaluationResult.error, ); recordSource.set(linkedID, linkedRecord); // Link the resolver value record to the resolver field of the record being read: const nextRecord = RelayModernRecord.clone(record); RelayModernRecord.setLinkedRecordID(nextRecord, storageKey, linkedID); recordSource.set(RelayModernRecord.getDataID(nextRecord), nextRecord); // Put records observed by the resolver into the dependency graph: const resolverID = evaluationResult.resolverID; addDependencyEdge(this._resolverIDToRecordIDs, resolverID, linkedID); addDependencyEdge(this._recordIDToResolverIDs, recordID, resolverID); const seenRecordIds = evaluationResult.snapshot?.seenRecords; if (seenRecordIds != null) { for (const seenRecordID of seenRecordIds) { addDependencyEdge( this._recordIDToResolverIDs, seenRecordID, resolverID, ); } } } // $FlowFixMe[incompatible-type] - will always be empty const answer: T = linkedRecord[RELAY_RESOLVER_VALUE_KEY]; // $FlowFixMe[incompatible-type] - casting mixed const snapshot: ?Snapshot = linkedRecord[RELAY_RESOLVER_SNAPSHOT_KEY]; // $FlowFixMe[incompatible-type] - casting mixed const error: ?RelayResolverError = linkedRecord[RELAY_RESOLVER_ERROR_KEY]; return [answer, linkedID, error, snapshot]; } invalidateDataIDs( updatedDataIDs: Set<DataID>, // Mutated in place ): void { const recordSource = this._getRecordSource(); const visited: Set<string> = new Set(); const recordsToVisit = Array.from(updatedDataIDs); while (recordsToVisit.length) { const recordID = recordsToVisit.pop(); updatedDataIDs.add(recordID); for (const fragment of this._recordIDToResolverIDs.get(recordID) ?? emptySet) { if (!visited.has(fragment)) { for (const anotherRecordID of this._resolverIDToRecordIDs.get( fragment, ) ?? emptySet) { this._markInvalidatedResolverRecord( anotherRecordID, recordSource, updatedDataIDs, ); if (!visited.has(anotherRecordID)) { recordsToVisit.push(anotherRecordID); } } } } } } _markInvalidatedResolverRecord( dataID: DataID, recordSource: MutableRecordSource, // Written to updatedDataIDs: Set<DataID>, // Mutated in place ) { const record = recordSource.get(dataID); if (!record) { warning( false, 'Expected a resolver record with ID %s, but it was missing.', dataID, ); return; } const nextRecord = RelayModernRecord.clone(record); RelayModernRecord.setValue( nextRecord, RELAY_RESOLVER_INVALIDATION_KEY, true, ); recordSource.set(dataID, nextRecord); } _isInvalid( record: Record, getDataForResolverFragment: SingularReaderSelector => mixed, ): boolean { if (!RelayModernRecord.getValue(record, RELAY_RESOLVER_INVALIDATION_KEY)) { return false; } // $FlowFixMe[incompatible-type] - storing values in records is not typed const snapshot: ?Snapshot = RelayModernRecord.getValue( record, RELAY_RESOLVER_SNAPSHOT_KEY, ); const originalInputs = snapshot?.data; const readerSelector: ?SingularReaderSelector = snapshot?.selector; if (originalInputs == null || readerSelector == null) { warning( false, 'Expected previous inputs and reader selector on resolver record with ID %s, but they were missing.', RelayModernRecord.getDataID(record), ); return true; } const latestValues = getDataForResolverFragment(readerSelector); const recycled = recycleNodesInto(originalInputs, latestValues); if (recycled !== originalInputs) { return true; } return false; } createClientRecord(id: string, typename: string): string { invariant( false, 'Client Edges to Client Objects are not supported in this version of Relay Store', ); } } module.exports = { NoopResolverCache, RecordResolverCache, };