import { useAtomValue } from "@effect/atom-react"; import { createLinkedPullRequestDetailAtomFamily, createPullRequestEnvironmentAtoms, } from "@t3tools/client-runtime/state/pull-requests"; import type { EnvironmentId, PullRequestListInput, PullRequestListStatsInput, } from "@t3tools/contracts"; import * as Option from "effect/Option"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { useCallback, useMemo } from "react"; import { connectionAtomRuntime } from "../connection/runtime"; import { appAtomRegistry } from "../rpc/atomRegistry"; import { mergePullRequestLists, type EnvironmentPullRequestStat, type MergedPullRequestList, } from "../components/pullRequest/pullRequestList.logic"; import { formatEnvironmentQueryError } from "./query"; export const pullRequestEnvironment = createPullRequestEnvironmentAtoms(connectionAtomRuntime); export const linkedPullRequestDetailAtom = createLinkedPullRequestDetailAtomFamily(connectionAtomRuntime); export interface EnvironmentQueryTarget { readonly environmentId: EnvironmentId; readonly input: Input; } interface MergedEnvironmentQueryView { /** One entry per environment that has answered, in the order the targets were given. */ readonly values: ReadonlyArray; /** The first environment that failed. Others may still have answered — this is not fatal. */ readonly error: string | null; readonly isPending: boolean; } /** * The same per-environment query read across several environments at once. React cannot subscribe * to a list of atoms whose length changes, so the fan-out happens inside one derived atom keyed by * the targets — the same shape the cross-environment thread search uses. * * An environment that fails contributes nothing rather than blanking the page: the pull request * list is a union, and one unreachable machine should not hide the others' rows. */ function createMergedEnvironmentQuery( label: string, atomFor: ( target: EnvironmentQueryTarget, ) => Atom.Atom>, ) { const family = Atom.family((key: string) => Atom.make((get): MergedEnvironmentQueryView => { const targets = JSON.parse(key) as ReadonlyArray>; const values: Array = []; let error: string | null = null; let isPending = false; for (const target of targets) { const result = get(atomFor(target)); isPending ||= result.waiting; if (result._tag === "Failure" && error === null) { error = formatEnvironmentQueryError(result.cause); } const value = Option.getOrNull(AsyncResult.value(result)); if (value !== null) values.push([target.environmentId, value]); } return { values, error, isPending }; }).pipe(Atom.withLabel(`${label}:${key}`)), ); const empty = Atom.make>({ values: [], error: null, isPending: false, }).pipe(Atom.withLabel(`${label}:empty`)); return function useMergedQuery(targets: ReadonlyArray>) { const key = JSON.stringify(targets); const view = useAtomValue(targets.length === 0 ? empty : family(key)); const refresh = useCallback(() => { for (const target of JSON.parse(key) as ReadonlyArray>) { appAtomRegistry.refresh(atomFor(target)); } }, [key]); return { ...view, refresh }; }; } const usePullRequestListsQuery = createMergedEnvironmentQuery( "web-pull-requests:list", pullRequestEnvironment.list, ); const usePullRequestStatsQuery = createMergedEnvironmentQuery( "web-pull-requests:list-stats", pullRequestEnvironment.listStats, ); export interface MergedPullRequestListView { readonly data: MergedPullRequestList | null; readonly error: string | null; readonly isPending: boolean; readonly refresh: () => void; } /** One listing per environment, merged into the single list the page renders. */ export function usePullRequestList( targets: ReadonlyArray>, ): MergedPullRequestListView { const query = usePullRequestListsQuery(targets); const data = useMemo(() => mergePullRequestLists(query.values), [query.values]); return { data, error: query.error, isPending: query.isPending, refresh: query.refresh }; } /** The line counts for the rows on screen, asked of each environment for its own rows. */ export function usePullRequestListStats( targets: ReadonlyArray>, ): { readonly stats: ReadonlyArray | null; readonly refresh: () => void; } { const query = usePullRequestStatsQuery(targets); const stats = useMemo( () => query.values.length === 0 ? null : query.values.flatMap(([environmentId, result]) => result.stats.map((stat) => ({ ...stat, environmentId })), ), [query.values], ); return { stats, refresh: query.refresh }; }