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 };
}