/**
 * 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' };
