import type { ItemCleanupPair } from '@isograph/disposable-types'; import { UNASSIGNED_STATE, useUpdatableDisposableState, } from '@isograph/react-disposable-state'; import type { ReferenceCountedPointer } from '@isograph/reference-counted-pointer'; import { createReferenceCountedPointer } from '@isograph/reference-counted-pointer'; import { useState } from 'react'; import { subscribeToAnyChange } from '../core/cache'; import type { FetchOptions } from '../core/check'; import type { FragmentReference, UnknownTReadFromStore, } from '../core/FragmentReference'; import { getPromiseState, readPromise } from '../core/PromiseWrapper'; import { readButDoNotEvaluate, type WithEncounteredRecords, } from '../core/read'; import type { LoadableField, ReaderAst } from '../core/reader'; import { getOrCreateCachedStartUpdate } from '../core/startUpdate'; import { useIsographEnvironment } from '../react/IsographEnvironmentProvider'; import { maybeUnwrapNetworkRequest } from '../react/maybeUnwrapNetworkRequest'; import { useSubscribeToMultiple } from '../react/useReadAndSubscribe'; export type UsePaginationReturnValue< TReadFromStore extends UnknownTReadFromStore, TItem, > = | { kind: 'Pending'; pendingFragment: FragmentReference>; results: ReadonlyArray; } | { kind: 'HasMoreRecords'; fetchMore: ( count: number, fetchOptions?: FetchOptions, never>, ) => void; results: ReadonlyArray; } | { kind: 'NoMoreRecords'; results: ReadonlyArray; }; type LoadedFragmentReferences< TReadFromStore extends { parameters: object; data: object }, TItem, > = ReadonlyArray>; type LoadedFragmentReference< TReadFromStore extends { parameters: object; data: object }, TItem, > = ItemCleanupPair< ReferenceCountedPointer> >; function flatten(arr: ReadonlyArray>): ReadonlyArray { let outArray: Array = []; for (const subarr of arr) { for (const item of subarr) { outArray.push(item); } } return outArray; } export type PageInfo = { readonly hasNextPage: boolean; readonly endCursor: string | null; }; export type Connection = { readonly edges: ReadonlyArray | null; readonly pageInfo: PageInfo; }; type NonNullConnection = { readonly edges: ReadonlyArray; readonly pageInfo: PageInfo; }; export type UseConnectionSpecPaginationArgs = { first: number; after: string | null; }; export function useConnectionSpecPagination< TReadFromStore extends UnknownTReadFromStore, TItem, >( loadableField: LoadableField< TReadFromStore, Connection, UseConnectionSpecPaginationArgs >, initialState?: PageInfo, ): UsePaginationReturnValue { const networkRequestOptions = { suspendIfInFlight: true, throwOnNetworkError: true, }; const { state, setState } = useUpdatableDisposableState< LoadedFragmentReferences> >(); const environment = useIsographEnvironment(); // TODO move this out of useSkipLimitPagination, and pass environment and networkRequestOptions // as parameters (or recreate networkRequestOptions) function readCompletedFragmentReferences( completedReferences: FragmentReference>[], ): NonNullConnection { const results = completedReferences.map((fragmentReference, i) => { const readerWithRefetchQueries = readPromise( fragmentReference.readerWithRefetchQueries, ); // invariant: readOutDataAndRecords.length === completedReferences.length const data = readOutDataAndRecords[i]?.item; if (data == null) { throw new Error( 'Parameter data is unexpectedly null. This is indicative of a bug in Isograph.', ); } const firstParameter = { data, parameters: fragmentReference.variables, ...(readerWithRefetchQueries.readerArtifact.hasUpdatable ? { startUpdate: getOrCreateCachedStartUpdate( environment, fragmentReference, networkRequestOptions, ), } : undefined), }; if ( readerWithRefetchQueries.readerArtifact.kind !== 'EagerReaderArtifact' ) { throw new Error( `@loadable field of kind "${readerWithRefetchQueries.readerArtifact.kind}" is not supported by useSkipLimitPagination`, ); } return readerWithRefetchQueries.readerArtifact.resolver(firstParameter); }); const items = flatten(results.map((result) => result.edges ?? [])); return { edges: items, pageInfo: results[results.length - 1]?.pageInfo ?? { endCursor: null, hasNextPage: true, }, }; } function subscribeCompletedFragmentReferences( completedReferences: FragmentReference>[], ) { return completedReferences.map( ( fragmentReference, i, ): { records: WithEncounteredRecords; callback: ( updatedRecords: WithEncounteredRecords, ) => void; fragmentReference: FragmentReference>; readerAst: ReaderAst>; } => { maybeUnwrapNetworkRequest( fragmentReference.networkRequest, networkRequestOptions, ); const readerWithRefetchQueries = readPromise( fragmentReference.readerWithRefetchQueries, ); const records = readOutDataAndRecords[i]; if (records == null) { throw new Error( 'subscribeCompletedFragmentReferences records is unexpectedly null', ); } return { fragmentReference, readerAst: readerWithRefetchQueries.readerArtifact.readerAst, records, callback(_data) { rerender({}); }, }; }, ); } const getFetchMore = (after: string | null) => ( count: number, fetchOptions?: FetchOptions, never>, ): void => { const loadedField = loadableField( { after: after, first: count, }, fetchOptions ?? {}, )[1](); const newPointer = createReferenceCountedPointer(loadedField); const clonedPointers = loadedReferences.map(([refCountedPointer]) => { const clonedRefCountedPointer = refCountedPointer.cloneIfNotDisposed(); if (clonedRefCountedPointer == null) { throw new Error( 'This reference counted pointer has already been disposed. \ This is indicative of a bug in useSkipLimitPagination.', ); } return clonedRefCountedPointer; }); clonedPointers.push(newPointer); const totalItemCleanupPair: ItemCleanupPair< ReadonlyArray< ItemCleanupPair< ReferenceCountedPointer< FragmentReference> > > > > = [ clonedPointers, () => { clonedPointers.forEach(([, dispose]) => { dispose(); }); }, ]; setState(totalItemCleanupPair); }; const [, rerender] = useState({}); const loadedReferences = state === UNASSIGNED_STATE ? [] : state; const mostRecentItem: | LoadedFragmentReference> | undefined = loadedReferences[loadedReferences.length - 1]; const mostRecentFragmentReference = mostRecentItem?.[0].getItemIfNotDisposed(); if (mostRecentItem != null && mostRecentFragmentReference == null) { throw new Error( 'FragmentReference is unexpectedly disposed. \ This is indicative of a bug in Isograph.', ); } const networkRequestStatus = mostRecentFragmentReference != null ? { mostRecentFragmentReference, state: getPromiseState(mostRecentFragmentReference.networkRequest), } : null; const slicedFragmentReferences = networkRequestStatus?.state?.kind === 'Ok' ? loadedReferences : loadedReferences.slice(0, loadedReferences.length - 1); const completedFragmentReferences = slicedFragmentReferences.map( ([pointer]) => { const fragmentReference = pointer.getItemIfNotDisposed(); if (fragmentReference == null) { throw new Error( 'FragmentReference is unexpectedly disposed. \ This is indicative of a bug in Isograph.', ); } return fragmentReference; }, ); const readOutDataAndRecords = completedFragmentReferences.map( (fragmentReference) => readButDoNotEvaluate( environment, fragmentReference, networkRequestOptions, ), ); useSubscribeToMultiple( subscribeCompletedFragmentReferences(completedFragmentReferences), ); if (networkRequestStatus == null) { if (initialState?.hasNextPage ?? true) { return { kind: 'HasMoreRecords', fetchMore: getFetchMore(initialState?.endCursor ?? null), results: [], }; } else { return { kind: 'NoMoreRecords', results: [], }; } } switch (networkRequestStatus.state.kind) { case 'Pending': { const unsubscribe = subscribeToAnyChange(environment, () => { unsubscribe(); rerender({}); }); const results = readCompletedFragmentReferences( completedFragmentReferences, ); return { results: results.edges, kind: 'Pending', pendingFragment: networkRequestStatus.mostRecentFragmentReference, }; } case 'Err': { throw networkRequestStatus.state.error; } case 'Ok': { const results = readCompletedFragmentReferences( completedFragmentReferences, ); if (results.pageInfo.hasNextPage) { return { kind: 'HasMoreRecords', fetchMore: getFetchMore(results.pageInfo.endCursor), results: results.edges, }; } else { return { kind: 'NoMoreRecords', results: results.edges, }; } } } }