relay-runtime
Version:
A core runtime for building GraphQL-driven applications.
1,328 lines (1,205 loc) • 43.8 kB
TypeScript
/**
* 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.
*/
import { MutationParameters } from '../mutations/commitMutation';
import {
GraphQLResponse,
Network,
PayloadData,
PayloadError,
ReactFlightServerTree,
UploadableMap,
} from '../network/RelayNetworkTypes';
import { RelayObservable } from '../network/RelayObservable';
import { GraphQLTaggedNode } from '../query/GraphQLTag';
import { RequestIdentifier } from '../util/getRequestIdentifier';
import {
NormalizationLinkedField,
NormalizationScalarField,
NormalizationSelectableNode,
NormalizationSplitOperation,
} from '../util/NormalizationNode';
import {ReaderFragment, ReaderLinkedField} from '../util/ReaderNode';
import {ConcreteRequest, RequestParameters} from '../util/RelayConcreteNode';
import {
CacheConfig,
DataID,
Disposable,
OperationType,
RenderPolicy,
Variables,
VariablesOf,
} from '../util/RelayRuntimeTypes';
import { InvalidationState } from './RelayModernStore';
import { RelayOperationTracker } from './RelayOperationTracker';
import { RecordState } from './RelayRecordState';
export type FragmentType = unknown;
export type OperationTracker = RelayOperationTracker;
/*
* An individual cached graph object.
*/
export interface Record<T extends object = Record<string, unknown>> {
[key: string]: T;
}
/**
* A collection of records keyed by id.
*/
export interface RecordMap {
// theoretically, this should be `[dataID: DataID]`, but `DataID` is a string.
[dataID: string]: Record | null | undefined;
}
export interface FragmentMap {
[key: string]: ReaderFragment;
}
/**
* The results of a selector given a store/RecordSource.
*/
export interface SelectorData {
[key: string]: unknown;
}
export interface SingularReaderSelector {
readonly kind: 'SingularReaderSelector';
readonly dataID: DataID;
readonly isWithinUnmatchedTypeRefinement: boolean;
readonly node: ReaderFragment;
readonly owner: RequestDescriptor;
readonly variables: Variables;
}
export type ReaderSelector = SingularReaderSelector | PluralReaderSelector;
export interface PluralReaderSelector {
readonly kind: 'PluralReaderSelector';
readonly selectors: readonly SingularReaderSelector[];
}
export interface RequestDescriptor {
readonly identifier: RequestIdentifier;
readonly node: ConcreteRequest;
readonly variables: Variables;
readonly cacheConfig: CacheConfig | null;
}
/**
* A selector defines the starting point for a traversal into the graph for the
* purposes of targeting a subgraph.
*/
export interface NormalizationSelector {
readonly dataID: DataID;
readonly node: NormalizationSelectableNode;
readonly variables: Variables;
}
/**
* A representation of a selector and its results at a particular point in time.
*/
export interface TypedSnapshot<TData> {
readonly data: TData;
readonly isMissingData: boolean;
readonly seenRecords: RecordMap;
readonly selector: SingularReaderSelector;
}
export type Snapshot = TypedSnapshot<SelectorData>;
/**
* An operation selector describes a specific instance of a GraphQL operation
* with variables applied.
*
* - `root`: a selector intended for processing server results or retaining
* response data in the store.
* - `fragment`: a selector intended for use in reading or subscribing to
* the results of the the operation.
*/
export interface OperationDescriptor {
readonly fragment: SingularReaderSelector;
readonly request: RequestDescriptor;
readonly root: NormalizationSelector;
}
/**
* Arbitrary data e.g. received by a container as props.
*/
export interface Props {
[key: string]: unknown;
}
/**
* The type of the `relay` property set on React context by the React/Relay
* integration layer (e.g. QueryRenderer, FragmentContainer, etc).
*/
export interface RelayContext {
environment: Environment;
}
/**
* The results of reading the results of a FragmentMap given some input
* `Props`.
*/
export interface FragmentSpecResults {
[key: string]: unknown;
}
/**
* A utility for resolving and subscribing to the results of a fragment spec
* (key -> fragment mapping) given some "props" that determine the root ID
* and variables to use when reading each fragment. When props are changed via
* `setProps()`, the resolver will update its results and subscriptions
* accordingly. Internally, the resolver:
* - Converts the fragment map & props map into a map of `Selector`s.
* - Removes any resolvers for any props that became null.
* - Creates resolvers for any props that became non-null.
* - Updates resolvers with the latest props.
*/
export interface FragmentSpecResolver {
/**
* Stop watching for changes to the results of the fragments.
*/
dispose(): void;
/**
* Get the current results.
*/
resolve(): FragmentSpecResults;
/**
* Update the resolver with new inputs. Call `resolve()` to get the updated
* results.
*/
setProps(props: Props): void;
/**
* Override the variables used to read the results of the fragments. Call
* `resolve()` to get the updated results.
*/
setVariables(variables: Variables, node: ConcreteRequest): void;
/**
* Subscribe to resolver updates.
* Overrides existing callback (if one has been specified).
*/
setCallback(callback: () => void): void;
}
/**
* A read-only interface for accessing cached graph data.
*/
export interface RecordSource {
get<T extends object = Record<string, unknown>>(dataID: DataID): Record<T> | null | undefined;
getRecordIDs(): DataID[];
getStatus(dataID: DataID): RecordState;
has(dataID: DataID): boolean;
size(): number;
toJSON(): { [key: string]: Record };
}
/**
* A read/write interface for accessing and updating graph data.
*/
export interface MutableRecordSource extends RecordSource {
clear(): void;
delete(dataID: DataID): void;
remove(dataID: DataID): void;
set(dataID: DataID, record: Record): void;
}
export interface CheckOptions {
target: MutableRecordSource;
handlers: readonly MissingFieldHandler[];
}
export type OperationAvailability =
| {
status: 'available';
fetchTime: number | null | undefined;
}
| { status: 'stale' }
| { status: 'missing' };
export { InvalidationState } from './RelayModernStore';
/**
* An interface for keeping multiple views of data consistent across an
* application.
*/
export interface Store {
/**
* Get a read-only view of the store's internal RecordSource.
*/
getSource(): RecordSource;
/**
* Determine if the operation can be resolved with data in the store (i.e. no
* fields are missing).
*/
check(operation: OperationDescriptor, options?: CheckOptions): OperationAvailability;
/**
* Read the results of a selector from in-memory records in the store.
* Optionally takes an owner, corresponding to the operation that
* owns this selector (fragment).
*/
lookup(selector: SingularReaderSelector): Snapshot;
/**
* Notify subscribers (see `subscribe`) of any data that was published
* (`publish()`) since the last time `notify` was called.
* Optionally provide an OperationDescriptor indicating the source operation
* that was being processed to produce this run.
*
* This method should return an array of the affected fragment owners
*/
notify(sourceOperation?: OperationDescriptor, invalidateStore?: boolean): readonly RequestDescriptor[];
/**
* Publish new information (e.g. from the network) to the store, updating its
* internal record source. Subscribers are not immediately notified - this
* occurs when `notify()` is called.
*/
publish(source: RecordSource, idsMarkedForInvalidation?: DataIDSet): void;
/**
* Ensure that all the records necessary to fulfill the given selector are
* retained in-memory. The records will not be eligible for garbage collection
* until the returned reference is disposed.
*/
retain(operation: OperationDescriptor): Disposable;
/**
* Subscribe to changes to the results of a selector. The callback is called
* when `notify()` is called *and* records have been published that affect the
* selector results relative to the last `notify()`.
*/
subscribe(snapshot: Snapshot, callback: (snapshot: Snapshot) => void): Disposable;
/**
* The method should disable garbage collection until
* the returned reference is disposed.
*/
holdGC(): Disposable;
/**
* Record a backup/snapshot of the current state of the store, including
* records and derived data such as fragment and connection subscriptions.
* This state can be restored with restore().
*/
snapshot(): void;
/**
* Reset the state of the store to the point that snapshot() was last called.
*/
restore(): void;
/**
* Will return an opaque snapshot of the current invalidation state of
* the data ids that were provided.
*/
lookupInvalidationState(dataIDs: readonly DataID[]): InvalidationState;
/**
* Given the previous invalidation state for those
* ids, this function will return:
* - false, if the invalidation state for those ids is the same, meaning
* **it has not changed**
* - true, if the invalidation state for the given ids has changed
*/
checkInvalidationState(previousInvalidationState: InvalidationState): boolean;
/**
* Will subscribe the provided callback to the invalidation state of the
* given data ids. Whenever the invalidation state for any of the provided
* ids changes, the callback will be called, and provide the latest
* invalidation state.
* Disposing of the returned disposable will remove the subscription.
*/
subscribeToInvalidationState(invalidationState: InvalidationState, callback: () => void): Disposable;
}
/**
* A type that accepts a callback and schedules it to run at some future time.
* By convention, implementations should not execute the callback immediately.
*/
export type Scheduler = (callback: () => void) => void;
/**
* An interface for imperatively getting/setting properties of a `Record`. This interface
* is designed to allow the appearance of direct Record manipulation while
* allowing different implementations that may e.g. create a changeset of
* the modifications.
*/
export type Unarray<T> = T extends Array<infer U> | ReadonlyArray<infer U> ? U : T;
export type Primitive = string | number | boolean | null | undefined;
export interface RecordProxy<T = Record<string, unknown>> {
copyFieldsFrom(source: RecordProxy): void;
getDataID(): DataID;
// If a parent type is provided, provide the child type
getLinkedRecord<K extends keyof T>(name: K, args?: Variables | null): RecordProxy<NonNullable<T[K]>>;
// If a hint is provided, the return value is guaranteed to be the hint type
getLinkedRecord<H = never>(
name: string,
args?: Variables | null,
): [H] extends [never] ? RecordProxy | null : RecordProxy<H>;
getLinkedRecords<K extends keyof T>(
name: K,
args?: Variables | null,
): Array<RecordProxy<Unarray<NonNullable<T[K]>>>>;
getLinkedRecords<H = never>(
name: string,
args?: Variables | null,
): [H] extends [never] ? RecordProxy[] | null
: NonNullable<H> extends Array<infer U> ? Array<RecordProxy<U>> | (H extends null ? null : never)
: never;
getOrCreateLinkedRecord<K extends keyof T>(
name: K,
typeName: string,
args?: Variables | null,
): RecordProxy<NonNullable<T[K]>>;
getOrCreateLinkedRecord(name: string, typeName: string, args?: Variables | null): RecordProxy<T>;
getType(): string;
getValue<K extends keyof T>(name: K, args?: Variables | null): T[K];
getValue(name: string, args?: Variables | null): Primitive | Primitive[];
setLinkedRecord<K extends keyof T>(
record: RecordProxy<T[K]> | null,
name: K,
args?: Variables | null,
): RecordProxy<T>;
setLinkedRecord(record: RecordProxy | null, name: string, args?: Variables | null): RecordProxy;
setLinkedRecords<K extends keyof T>(
records: Array<RecordProxy<Unarray<T[K]>> | null> | null | undefined,
name: K,
args?: Variables | null,
): RecordProxy<T>;
setLinkedRecords(
records: Array<RecordProxy | null> | null | undefined,
name: string,
args?: Variables | null,
): RecordProxy<T>;
setValue<K extends keyof T>(value: T[K], name: K, args?: Variables | null): RecordProxy<T>;
setValue(value: Primitive | Primitive[], name: string, args?: Variables | null): RecordProxy;
invalidateRecord(): void;
}
export interface ReadOnlyRecordProxy {
getDataID(): DataID;
getLinkedRecord(name: string, args?: Variables): RecordProxy | null | undefined;
getLinkedRecords(name: string, args?: Variables): Array<RecordProxy | null | undefined> | null | undefined;
getType(): string;
getValue(name: string, args?: Variables | null): unknown;
}
/**
* An interface for imperatively getting/setting properties of a `RecordSource`. This interface
* is designed to allow the appearance of direct RecordSource manipulation while
* allowing different implementations that may e.g. create a changeset of
* the modifications.
*/
export interface RecordSourceProxy {
create(dataID: DataID, typeName: string): RecordProxy;
delete(dataID: DataID): void;
get<T = Record<string, unknown>>(dataID: DataID): RecordProxy<T> | null | undefined;
getRoot(): RecordProxy;
}
export interface ReadOnlyRecordSourceProxy {
get(dataID: DataID): ReadOnlyRecordProxy | null | undefined;
getRoot(): ReadOnlyRecordProxy;
}
/**
* Extends the RecordSourceProxy interface with methods for accessing the root
* fields of a Selector.
*/
export interface RecordSourceSelectorProxy<T = Record<string, unknown>> extends RecordSourceProxy {
getRootField<K extends keyof T>(fieldName: K): RecordProxy<NonNullable<T[K]>>;
getRootField(fieldName: string): RecordProxy | null;
getPluralRootField(fieldName: string): Array<RecordProxy<T> | null> | null;
invalidateStore(): void;
readUpdatableQuery<TQuery extends OperationType>(
gqlQuery: GraphQLTaggedNode,
variables: VariablesOf<TQuery>,
): UpdatableQueryData<TQuery>;
readUpdatableFragment<TKey extends HasUpdatableSpread>(
fragmentInput: GraphQLTaggedNode,
fragmentRef: TKey,
): UpdatableFragmentData<TKey>;
}
export type LogEvent =
| Readonly<{
name: 'suspense.fragment';
data: unknown;
fragment: ReaderFragment;
isRelayHooks: boolean;
isMissingData: boolean;
isPromiseCached: boolean;
pendingOperations: readonly RequestDescriptor[];
}>
| Readonly<{
name: 'suspense.query';
fetchPolicy: string;
isPromiseCached: boolean;
operation: OperationDescriptor;
queryAvailability?: OperationAvailability | undefined;
renderPolicy: RenderPolicy;
}>
| Readonly<{
name: 'queryresource.fetch';
/**
* ID of this query resource request and will be the same if there is an associated queryresource.retain event.
*/
resourceID: number;
operation: OperationDescriptor;
// value from ProfilerContext
profilerContext: unknown;
// FetchPolicy from Relay Hooks
fetchPolicy: string;
// RenderPolicy from Relay Hooks
renderPolicy: RenderPolicy;
queryAvailability: OperationAvailability;
shouldFetch: boolean;
}>
| Readonly<{
name: 'queryresource.retain';
resourceID: number;
// value from ProfilerContext
profilerContext: unknown;
}>
| Readonly<{
// Indicates FragmentResource is going to return a result that is missing data.
name: 'fragmentresource.missing_data';
data: unknown;
fragment: ReaderFragment;
isRelayHooks: boolean;
// Are we reading this result from the fragment resource cache?
cached: boolean;
}>
| Readonly<{
/**
* Indicates getPendingOperationForFragment identified a pending operation.
* Useful for measuring how frequently RelayOperationTracker identifies a related operation on which to suspend.
*/
name: 'pendingoperation.found';
fragment: ReaderFragment;
fragmentOwner: RequestDescriptor;
pendingOperations: ReadonlyArray<RequestDescriptor>;
}>
| Readonly<{
name: 'network.info';
networkRequestId: number;
info: unknown;
}>
| Readonly<{
name: 'network.start';
networkRequestId: number;
params: RequestParameters;
variables: Variables;
cacheConfig: CacheConfig;
}>
| Readonly<{
name: 'network.next';
networkRequestId: number;
response: GraphQLResponse;
}>
| Readonly<{
name: 'network.error';
networkRequestId: number;
error: Error;
}>
| Readonly<{
name: 'network.complete';
networkRequestId: number;
}>
| Readonly<{
name: 'network.unsubscribe';
networkRequestId: number;
}>
| Readonly<{
name: 'execute.start';
executeId: number;
params: RequestParameters;
variables: Variables;
cacheConfig: CacheConfig;
}>
| Readonly<{
name: 'execute.next.start';
executeId: number;
response: GraphQLResponse;
operation: OperationDescriptor;
}>
| Readonly<{
name: 'execute.next.end';
executeId: number;
response: GraphQLResponse;
operation: OperationDescriptor;
}>
| Readonly<{
name: 'execute.async.module';
executeId: number;
operationName: string;
duration: number;
}>
| Readonly<{
name: 'execute.error';
executeId: number;
error: Error;
}>
| Readonly<{
name: 'execute.complete';
executeId: number;
}>
| Readonly<{
name: 'execute.normalize.start';
operation: OperationDescriptor;
}>
| Readonly<{
name: 'execute.normalize.end';
operation: OperationDescriptor;
}>
| Readonly<{
name: 'store.datachecker.start';
selector: NormalizationSelector;
}>
| Readonly<{
name: 'store.datachecker.end';
selector: NormalizationSelector;
}>
| Readonly<{
name: 'store.publish';
source: RecordSource;
optimistic: boolean;
}>
| Readonly<{
name: 'store.snapshot';
}>
| Readonly<{
name: 'store.lookup.start';
selector: SingularReaderSelector;
}>
| Readonly<{
name: 'store.lookup.end';
selector: SingularReaderSelector;
}>
| Readonly<{
name: 'store.restore';
}>
| Readonly<{
name: 'store.gc.start';
}>
| Readonly<{
name: 'store.gc.interrupted';
}>
| Readonly<{
name: 'store.gc.end';
references: DataIDSet;
}>
| Readonly<{
name: 'store.notify.start';
sourceOperation?: OperationDescriptor | undefined;
}>
| Readonly<{
name: 'store.notify.complete';
sourceOperation?: OperationDescriptor | undefined;
updatedRecordIDs: DataIDSet;
invalidatedRecordIDs: DataIDSet;
subscriptionsSize: number;
updatedOwners: Array<RequestDescriptor>;
}>
| Readonly<{
name: 'store.notify.subscription';
sourceOperation?: OperationDescriptor | undefined;
snapshot: Snapshot;
nextSnapshot: Snapshot;
}>
| Readonly<{
name: 'entrypoint.root.consume';
profilerContext: unknown;
rootModuleID: string;
}>
| Readonly<{
name: 'liveresolver.batch.start';
}>
| Readonly<{
name: 'liveresolver.batch.end';
}>
| Readonly<{
name: 'useFragment.subscription.missedUpdates';
hasDataChanges: boolean;
}>;
export type LogFunction = (logEvent: LogEvent) => void;
/**
* The public API of Relay core. Represents an encapsulated environment with its
* own in-memory cache.
*/
export interface Environment {
/**
* Extra information attached to the environment instance
*/
options: unknown;
/**
* Determine if the operation can be resolved with data in the store (i.e. no
* fields are missing).
*
* Note that this operation effectively "executes" the selector against the
* cache and therefore takes time proportional to the size/complexity of the
* selector.
*/
check(operation: OperationDescriptor, options?: CheckOptions): OperationAvailability;
/**
* Subscribe to changes to the results of a selector. The callback is called
* when data has been committed to the store that would cause the results of
* the snapshot's selector to change.
*/
subscribe(snapshot: Snapshot, callback: (snapshot: Snapshot) => void): Disposable;
/**
* Ensure that all the records necessary to fulfill the given operation are
* retained in-memory. The records will not be eligible for garbage collection
* until the returned reference is disposed.
*/
retain(operation: OperationDescriptor): Disposable;
/**
* Apply an optimistic update to the environment. The mutation can be reverted
* by calling `dispose()` on the returned value.
*/
applyUpdate(optimisticUpdate: OptimisticUpdateFunction): Disposable;
/**
* Apply an optimistic mutation response and/or updater. The mutation can be
* reverted by calling `dispose()` on the returned value.
*/
applyMutation(optimisticConfig: OptimisticResponseConfig): Disposable;
/**
* Commit an updater to the environment. This mutation cannot be reverted and
* should therefore not be used for optimistic updates. This is mainly
* intended for updating fields from client schema extensions.
*/
commitUpdate(updater: StoreUpdater): void;
/**
* Commit a payload to the environment using the given operation selector.
*/
commitPayload(operationDescriptor: OperationDescriptor, payload: PayloadData): void;
/**
* Get the environment's internal Network.
*/
getNetwork(): Network;
/**
* Get the environment's internal Store.
*/
getStore(): Store;
/**
* Returns the environment specific OperationTracker.
*/
getOperationTracker(): RelayOperationTracker;
/**
* Read the results of a selector from in-memory records in the store.
* Optionally takes an owner, corresponding to the operation that
* owns this selector (fragment).
*/
lookup(selector: SingularReaderSelector): Snapshot;
/**
* Send a query to the server with Observer semantics: one or more
* responses may be returned (via `next`) over time followed by either
* the request completing (`completed`) or an error (`error`).
*
* Networks/servers that support subscriptions may choose to hold the
* subscription open indefinitely such that `complete` is not called.
*
* Note: Observables are lazy, so calling this method will do nothing until
* the result is subscribed to: environment.execute({...}).subscribe({...}).
*/
execute(config: {
operation: OperationDescriptor;
updater?: SelectorStoreUpdater | null | undefined;
}): RelayObservable<GraphQLResponse>;
/**
* Returns an Observable of GraphQLResponse resulting from executing the
* provided Mutation operation, the result of which is then normalized and
* committed to the publish queue along with an optional optimistic response
* or updater.
*
* Note: Observables are lazy, so calling this method will do nothing until
* the result is subscribed to:
* environment.executeMutation({...}).subscribe({...}).
*/
executeMutation({
operation,
optimisticUpdater,
optimisticResponse,
updater,
uploadables,
}: {
operation: OperationDescriptor;
optimisticUpdater?: SelectorStoreUpdater | null | undefined;
optimisticResponse?: { [key: string]: any } | null | undefined;
updater?: SelectorStoreUpdater | null | undefined;
uploadables?: UploadableMap | null | undefined;
}): RelayObservable<GraphQLResponse>;
/**
* Returns an Observable of GraphQLResponse resulting from executing the
* provided Query or Subscription operation responses, the result of which is
* then normalized and committed to the publish queue.
*
* Note: Observables are lazy, so calling this method will do nothing until
* the result is subscribed to:
* environment.executeWithSource({...}).subscribe({...}).
*/
executeWithSource({
operation,
source,
}: {
operation: OperationDescriptor;
source: RelayObservable<GraphQLResponse>;
}): RelayObservable<GraphQLResponse>;
/**
* Returns true if a request is currently "active", meaning it's currently
* actively receiving payloads or downloading modules, and has not received
* a final payload yet. Note that a request might still be pending (or "in flight")
* without actively receiving payload, for example a live query or an
* active GraphQL subscription
*/
isRequestActive(requestIdentifier: string): boolean;
/**
* Returns true if the environment is for use during server side rendering.
* functions like getQueryResource key off of this in order to determine
* whether we need to set up certain caches and timeout's.
*/
isServer(): boolean;
/**
* Called by Relay when it encounters a missing field that has been annotated
* with `@required(action: LOG)` or `@required(action: THROW)`.
*/
relayFieldLogger: RelayFieldLogger;
}
/**
* The results of reading data for a fragment. This is similar to a `Selector`,
* but references the (fragment) node by name rather than by value.
*/
export interface FragmentPointer {
__id: DataID;
__fragments: { [fragmentName: string]: Variables };
__fragmentOwner: RequestDescriptor;
}
// tslint:disable:no-redundant-jsdoc-2
/**
* The partial shape of an object with a '...Fragment @module(name: "...")'
* selection
*/
export interface ModuleImportPointer {
readonly __fragmentPropName: string | null | undefined;
readonly __module_component: unknown;
readonly $fragmentSpreads: unknown;
}
// tslint:enable:no-redundant-jsdoc-2
/**
* A callback for resolving a Selector from a source.
*/
export type AsyncLoadCallback = (loadingState: LoadingState) => void;
export interface LoadingState {
status: 'aborted' | 'complete' | 'error' | 'missing';
error?: Error | undefined;
}
export type DataIDSet = Set<DataID>;
/**
* A function that updates a store (via a proxy) given the results of a "handle"
* field payload.
*/
export class Handler {
update: (store: RecordSourceProxy, fieldPayload: HandleFieldPayload) => void;
}
/**
* A payload that is used to initialize or update a "handle" field with
* information from the server.
*/
export interface HandleFieldPayload {
// The arguments that were fetched.
readonly args: Variables;
// The __id of the record containing the source/handle field.
readonly dataID: DataID;
// The (storage) key at which the original server data was written.
readonly fieldKey: string;
// The name of the handle.
readonly handle: string;
// The (storage) key at which the handle's data should be written by the
// handler.
readonly handleKey: string;
}
/**
* A payload that represents data necessary to process the results of an object
* with a `@module` fragment spread:
* - data: The GraphQL response value for the @match field.
* - dataID: The ID of the store object linked to by the @match field.
* - operationReference: A reference to a generated module containing the
* SplitOperation with which to normalize the field's `data`.
* - variables: Query variables.
* - typeName: the type that matched.
*
* The dataID, variables, and fragmentName can be used to create a Selector
* which can in turn be used to normalize and publish the data. The dataID and
* typeName can also be used to construct a root record for normalization.
*/
export interface ModuleImportPayload {
readonly data: PayloadData;
readonly dataID: DataID;
readonly operationReference: any;
readonly path: readonly string[];
readonly typeName: string;
readonly variables: Variables;
}
/**
* Data emitted after processing a Defer or Stream node during normalization
* that describes how to process the corresponding response chunk when it
* arrives.
*/
export interface DeferPlaceholder {
readonly kind: 'defer';
readonly data: PayloadData;
readonly label: string;
readonly path: readonly string[];
readonly selector: NormalizationSelector;
readonly typeName: string;
}
export interface StreamPlaceholder {
readonly kind: 'stream';
readonly label: string;
readonly path: readonly string[];
readonly parentID: DataID;
readonly node: NormalizationSelectableNode;
readonly variables: Variables;
}
export type IncrementalDataPlaceholder = DeferPlaceholder | StreamPlaceholder;
/**
* A user-supplied object to load a generated operation (SplitOperation) AST
* by a module reference. The exact format of a module reference is left to
* the application, but it must be a plain JavaScript value (string, number,
* or object/array of same).
*/
export interface OperationLoader {
/**
* Synchronously load an operation, returning either the node or null if it
* cannot be resolved synchronously.
*/
get(reference: unknown): NormalizationSplitOperation | null | undefined;
/**
* Asynchronously load an operation.
*/
load(reference: unknown): Promise<NormalizationSplitOperation | null | undefined>;
}
/**
* A function that receives a proxy over the store and may trigger side-effects
* (indirectly) by calling `set*` methods on the store or its record proxies.
*/
export type StoreUpdater = (store: RecordSourceProxy) => void;
/**
* Similar to StoreUpdater, but accepts a proxy tied to a specific selector in
* order to easily access the root fields of a query/mutation as well as a
* second argument of the response object of the mutation.
*/
export type SelectorStoreUpdater<T = object> = (
store: RecordSourceSelectorProxy<T>,
data: T | null | undefined,
) => void;
/**
* A set of configs that can be used to apply an optimistic update into the
* store.
*/
export type OptimisticUpdate = OptimisticUpdateFunction | OptimisticUpdateRelayPayload;
export interface OptimisticUpdateFunction {
readonly storeUpdater: StoreUpdater;
}
export interface OptimisticUpdateRelayPayload {
readonly operation: OperationDescriptor;
readonly payload: RelayResponsePayload;
readonly updater: SelectorStoreUpdater | null | undefined;
}
export interface OptimisticResponseConfig<TMutation extends MutationParameters = any> {
readonly operation: OperationDescriptor;
readonly response: PayloadData | null | undefined;
readonly updater: SelectorStoreUpdater<TMutation> | null | undefined;
}
/**
* A set of handlers that can be used to provide substitute data for missing
* fields when reading a selector from a source.
*/
export type MissingFieldHandler =
| {
kind: 'scalar';
handle: (
field: NormalizationScalarField,
parentRecord: ReadOnlyRecordProxy | null | undefined,
args: Variables,
store: ReadOnlyRecordSourceProxy,
) => unknown;
}
| {
kind: 'linked';
handle: (
field: NormalizationLinkedField | ReaderLinkedField,
parentRecord: ReadOnlyRecordProxy | null | undefined,
args: Variables,
store: ReadOnlyRecordSourceProxy,
) => DataID | null | undefined;
}
| {
kind: 'pluralLinked';
handle: (
field: NormalizationLinkedField | ReaderLinkedField,
parentRecord: ReadOnlyRecordProxy | null | undefined,
args: Variables,
store: ReadOnlyRecordSourceProxy,
) => Array<DataID | null | undefined> | null | undefined;
};
type TRelayFieldErrorForDisplay = Readonly<{
path?: ReadonlyArray<string | number>;
severity?: 'CRITICAL' | 'ERROR' | 'WARNING';
}>;
export type TRelayFieldError =
& TRelayFieldErrorForDisplay
& Readonly<{
message: string;
}>;
/**
* Data which Relay expected to be in the store (because it was requested by
* the parent query/mutation/subscription) was missing. This can happen due
* to graph relationship changes observed by other queries/mutations, or
* imperative updates that don't provide all needed data.
*
* https://relay.dev/docs/next/debugging/why-null/#graph-relationship-change
*
* In this case Relay will render with the referenced field as `undefined`.
*
* __NOTE__: This may break with the type contract of Relay's generated types.
*
* To turn this into a hard error for a given fragment/query, you can use
* `@throwOnFieldError`.
*
* https://relay.dev/docs/next/guides/throw-on-field-error-directive/
*/
type MissingExpectedDataLogEvent = Readonly<{
kind: 'missing_expected_data.log';
owner: string;
fieldPath: string;
}>;
/**
* Data which Relay expected to be in the store (because it was requested by
* the parent query/mutation/subscription) was missing. This can happen due
* to graph relationship changes observed by other queries/mutations, or
* imperative updates that don't provide all needed data.
*
* https://relay.dev/docs/next/debugging/why-null/#graph-relationship-change
*
* This event is as `.throw` because the missing data was encountered in a
* query/fragment/mutation with `@throwOnFieldError` `@throwOnFieldError`.
*
* https://relay.dev/docs/next/guides/throw-on-field-error-directive/
*
* Relay will throw immediately after logging this event. If you wish to
* customize the error being thrown, you may throw your own error.
*
* *NOTE*: Only throw on this event if `handled` is false. Errors that have been
* handled by a `@catch` directive or by making a resolver null will have
* `handled: true` and should not trigger a throw.
*/
type MissingExpectedDataThrowEvent = Readonly<{
kind: 'missing_expected_data.throw';
owner: string;
fieldPath: string;
handled: boolean;
}>;
/**
* A field was marked as @required(action: LOG) but was null or missing in the
* store.
*/
type MissingRequiredFieldLogEvent = Readonly<{
kind: 'missing_required_field.log';
owner: string;
fieldPath: string;
}>;
/**
* A field was marked as @required(action: THROW) but was null or missing in the
* store.
*
* Relay will throw immediately after logging this event. If you wish to
* customize the error being thrown, you may throw your own error.
*
* *NOTE*: Only throw on this event if `handled` is false. Errors that have been
* handled by a `@catch` directive or by making a resolver null will have
* `handled: true` and should not trigger a throw.
*/
type MissingRequiredFieldThrowEvent = Readonly<{
kind: 'missing_required_field.throw';
owner: string;
fieldPath: string;
handled: boolean;
}>;
/**
* A Relay Resolver that is currently being read threw a JavaScript error when
* it was last evaluated. By default, the value has been coerced to null and
* passed to the product code.
*
* If `@throwOnFieldError` was used on the parent query/fragment/mutation, you
* will also receive a TODO
*
* *NOTE*: Only throw on this event if `handled` is false. Errors that have been
* handled by a `@catch` directive or by making a resolver null will have
* `handled: true` and should not trigger a throw.
*/
type RelayResolverErrorEvent = Readonly<{
kind: 'relay_resolver.error';
owner: string;
fieldPath: string;
error: Error;
shouldThrow: boolean;
handled: boolean;
}>;
/**
* A field being read by Relay was marked as being in an error state by the
* GraphQL response.
*
* https://spec.graphql.org/October2021/#sec-Errors.Field-errors
*
* If the field's parent query/fragment/mutation was annotated with
* `@throwOnFieldError` and no `@catch` directive was used to catch the error,
* Relay will throw an error immediately after logging this event.
*
* https://relay.dev/docs/next/guides/catch-directive/
* https://relay.dev/docs/next/guides/throw-on-field-error-directive/
*
* *NOTE*: Only throw on this event if `handled` is false. Errors that have been
* handled by a `@catch` directive or by making a resolver null will have
* `handled: true` and should not trigger a throw.
*/
type RelayFieldPayloadErrorEvent = Readonly<{
kind: 'relay_field_payload.error';
owner: string;
fieldPath: string;
error: TRelayFieldError;
shouldThrow: boolean;
handled: boolean;
}>;
/**
* Union of all RelayFieldLoggerEvent types
*/
export type RelayFieldLoggerEvent =
| MissingExpectedDataLogEvent
| MissingExpectedDataThrowEvent
| MissingRequiredFieldLogEvent
| MissingRequiredFieldThrowEvent
| RelayResolverErrorEvent
| RelayFieldPayloadErrorEvent;
/**
* A handler for events related to field errors.
*/
export type RelayFieldLogger = (event: RelayFieldLoggerEvent) => void;
/**
* The results of normalizing a query.
*/
export interface RelayResponsePayload {
readonly errors: PayloadError[] | null | undefined;
readonly fieldPayloads: HandleFieldPayload[] | null | undefined;
readonly incrementalPlaceholders: IncrementalDataPlaceholder[] | null | undefined;
readonly moduleImportPayloads: ModuleImportPayload[] | null | undefined;
readonly source: MutableRecordSource;
readonly isFinal: boolean;
}
/**
* Configuration on the executeMutation(...).
*/
export interface ExecuteMutationConfig<TMutation extends MutationParameters> {
operation: OperationDescriptor;
optimisticUpdater?: SelectorStoreUpdater<TMutation['response']> | null;
optimisticResponse?: { [key: string]: any } | null;
updater?: SelectorStoreUpdater<TMutation['response']> | null;
uploadables?: UploadableMap | null;
}
/**
* Public interface for Publish Queue
*/
export interface PublishQueue {
/**
* Schedule applying an optimistic updates on the next `run()`.
*/
applyUpdate(updater: OptimisticUpdate): void;
/**
* Schedule reverting an optimistic updates on the next `run()`.
*/
revertUpdate(updater: OptimisticUpdate): void;
/**
* Schedule a revert of all optimistic updates on the next `run()`.
*/
revertAll(): void;
/**
* Schedule applying a payload to the store on the next `run()`.
*/
commitPayload(
operation: OperationDescriptor,
payload: RelayResponsePayload,
updater?: SelectorStoreUpdater | null,
): void;
/**
* Schedule an updater to mutate the store on the next `run()` typically to
* update client schema fields.
*/
commitUpdate(updater: StoreUpdater): void;
/**
* Schedule a publish to the store from the provided source on the next
* `run()`. As an example, to update the store with substituted fields that
* are missing in the store.
*/
commitSource(source: RecordSource): void;
/**
* Execute all queued up operations from the other public methods.
*/
run(): readonly RequestDescriptor[];
}
/**
* ReactFlightDOMRelayClient processes a ReactFlightServerTree into a
* ReactFlightClientResponse object. readRoot() can suspend.
*/
export interface ReactFlightClientResponse {
readRoot: () => any;
}
export interface ReactFlightReachableQuery {
readonly module: any;
readonly variables: Variables;
}
export type ReactFlightPayloadDeserializer = (tree: ReactFlightServerTree) => ReactFlightClientResponse;
interface FieldLocation {
path: string;
owner: string;
}
export type MissingRequiredFields =
| Readonly<{ action: 'THROW'; field: FieldLocation }>
| Readonly<{ action: 'LOG'; fields: FieldLocation[] }>;
export interface RelayResolverError {
field: FieldLocation;
error: Error;
}
export type RelayResolverErrors = RelayResolverError[];
/**
* The return type of calls to store.readUpdatableFragment.
*/
export interface UpdatableFragmentData<TKey extends HasUpdatableSpread<TData>, TData = unknown> {
readonly updatableData: Required<TKey>[' $data'];
}
/**
* The return type of calls to store.readUpdatableQuery.
*/
export interface UpdatableQueryData<TQuery extends OperationType> {
readonly updatableData: TQuery['response'];
}
/**
* A linked field where an updatable fragment is spread has the type
* HasUpdatableSpread.
* This type is expected by store.readUpdatableFragment.
*/
export type HasUpdatableSpread<TData = unknown> = Readonly<{
' $data'?: TData | undefined;
$updatableFragmentSpreads: FragmentType;
}>;
/**
* The return type of a Live Resolver. Models an external value which can
* be read lazily and which might change over time. The subscribe method
* returns a callback which should be called when the value _may_ have changed.
*
* While over-notification (subscription notifications when the read value has
* not actually changed) is suported, for performance reasons, it is recommended
* that the provider of the LiveState value confirms that the value has indeed
* change before notifying Relay of the change.
*/
export interface LiveState<T> {
/**
* Returns the current value of the live state.
*/
read(): T;
/**
* Subscribes to changes in the live state. The state provider should
* call the callback when the value of the live state changes.
* If the returned unsubscribe function is invoked, the state provider
* should stop calling the callback for state updates.
*/
subscribe(callback: () => void): () => void;
}
export function suspenseSentinel(): never;
/**
* A placeholder type for the context that will be provided to resolvers.
* The actual type used will be determined by the `resolverContextType` type specified in your Relay project config.
*
* When set on the Relay Store, this context will be passed as the third argument to all resolvers (live and non-live).
*
* @see {@link https://relay.dev/docs/next/guides/relay-resolvers/context/#type-checking} for documentation on configuring resolverContextType
*/
export type ResolverContext = unknown;
export type KeyType<TData = unknown> = Readonly<{
' $data'?: TData | undefined;
' $fragmentSpreads': FragmentType;
}>;
export type KeyTypeData<TKey extends KeyType<TData>, TData = unknown> = Required<TKey>[' $data'];
export type ArrayKeyType<TData = unknown> = ReadonlyArray<KeyType<readonly TData[]> | null | undefined>;
export type ArrayKeyTypeData<TKey extends ArrayKeyType<TData>, TData = unknown> = KeyTypeData<
NonNullable<TKey[number]>
>;
export type FragmentState<T> =
| { state: 'ok'; value: T }
| { state: 'error'; error: Error }
| { state: 'loading' };