215 lines · 7.9 KB · TypeScript
RawDownload
import { useAtomValue } from "@effect/atom-react";import {  createLinkedPullRequestSummaryAtomFamily,  createPullRequestEnvironmentAtoms,  createPullRequestStackAtomFamily,} from "@t3tools/client-runtime/state/pull-requests";import type {  EnvironmentId,  PullRequestListInput,  PullRequestListStatsInput,  PullRequestRef,  PullRequestSummary,} from "@t3tools/contracts";import * as Option from "effect/Option";import { AsyncResult, Atom } from "effect/unstable/reactivity";import { useCallback, useLayoutEffect, 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 = createLinkedPullRequestSummaryAtomFamily(  connectionAtomRuntime,  pullRequestEnvironment.refreshes,); const observedPullRequestSummaryAtom = Atom.family((key: string) =>  Atom.make<PullRequestSummary | null>(null).pipe(    Atom.setIdleTTL(5 * 60_000),    Atom.withLabel(`web-pull-requests:observed-summary:${key}`),  ),); export function newestPullRequestSummary(  current: PullRequestSummary | null,  observed: PullRequestSummary | null,): PullRequestSummary | null {  if (current === null) return observed;  if (observed === null) return current;  if (current.state === "merged") return current;  if (observed.state === "merged") return observed;  return Date.parse(observed.updatedAt) >= Date.parse(current.updatedAt) ? observed : current;} export function useSharedPullRequestSummary(  environmentId: EnvironmentId | null,  reference: PullRequestRef | null,  current: PullRequestSummary | null,): PullRequestSummary | null {  const key =    environmentId === null || reference === null      ? "none"      : JSON.stringify([          environmentId,          reference.projectId,          reference.host?.toLowerCase() ?? null,          reference.repository.toLowerCase(),          reference.number,        ]);  const atom = observedPullRequestSummaryAtom(key);  const observed = useAtomValue(atom);  useLayoutEffect(() => {    if (environmentId === null || current === null) return;    appAtomRegistry.modify(atom, (previous) => {      const next = newestPullRequestSummary(previous, current);      return next === previous ? [false, previous] : [true, next];    });  }, [atom, current, environmentId]);  return newestPullRequestSummary(current, observed);}export const pullRequestStackAtom = createPullRequestStackAtomFamily(  connectionAtomRuntime,  pullRequestEnvironment.refreshes,); export interface EnvironmentQueryTarget<Input> {  readonly environmentId: EnvironmentId;  readonly input: Input;} interface MergedEnvironmentQueryView<A> {  /** One entry per query target that has answered, in the order the targets were given. */  readonly values: ReadonlyArray<readonly [EnvironmentId, A]>;  /** 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<Input, A>(  label: string,  atomFor: (    target: EnvironmentQueryTarget<Input>,  ) => Atom.Atom<AsyncResult.AsyncResult<A, unknown>>,) {  const family = Atom.family((key: string) =>    Atom.make((get): MergedEnvironmentQueryView<A> => {      const targets = JSON.parse(key) as ReadonlyArray<EnvironmentQueryTarget<Input>>;      const values: Array<readonly [EnvironmentId, A]> = [];      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<MergedEnvironmentQueryView<A>>({    values: [],    error: null,    isPending: false,  }).pipe(Atom.withLabel(`${label}:empty`));  return function useMergedQuery(targets: ReadonlyArray<EnvironmentQueryTarget<Input>>) {    const key = JSON.stringify(targets);    const view = useAtomValue(targets.length === 0 ? empty : family(key));    const refresh = useCallback(      (override?: ReadonlyArray<EnvironmentQueryTarget<Input>>) => {        const refreshTargets =          override ?? (JSON.parse(key) as ReadonlyArray<EnvironmentQueryTarget<Input>>);        for (const atom of new Set(refreshTargets.map(atomFor))) {          appAtomRegistry.refresh(atom);        }      },      [key],    );    return { ...view, refresh };  };} const usePullRequestListsQuery = createMergedEnvironmentQuery(  "web-pull-requests:list",  pullRequestEnvironment.list,); const usePullRequestStatsQuery = createMergedEnvironmentQuery(  "web-pull-requests:list-stats",  pullRequestEnvironment.listStats,); const usePullRequestTurnRefreshQuery = createMergedEnvironmentQuery(  "web-pull-requests:turn-refreshes",  ({ environmentId }: EnvironmentQueryTarget<Readonly<Record<string, never>>>) =>    pullRequestEnvironment.refreshes({ environmentId, input: {} }),); export function usePullRequestTurnRefreshes(  environmentIds: ReadonlyArray<EnvironmentId>,): ReadonlyArray<readonly [EnvironmentId, number]> {  return usePullRequestTurnRefreshQuery(    environmentIds.map((environmentId) => ({ environmentId, input: {} })),  ).values;} export function usePullRequestTurnRefresh(environmentId: EnvironmentId): number | null {  const result = useAtomValue(pullRequestEnvironment.refreshes({ environmentId, input: {} }));  return Option.getOrNull(AsyncResult.value(result));} export interface MergedPullRequestListView {  readonly data: MergedPullRequestList | null;  readonly error: string | null;  readonly isPending: boolean;  readonly refresh: (targets?: ReadonlyArray<EnvironmentQueryTarget<PullRequestListInput>>) => void;} /** One listing per environment, merged into the single list the page renders. */export function usePullRequestList(  targets: ReadonlyArray<EnvironmentQueryTarget<PullRequestListInput>>,): 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<EnvironmentQueryTarget<PullRequestListStatsInput>>,): {  readonly stats: ReadonlyArray<EnvironmentPullRequestStat> | null;  readonly isPending: boolean;  readonly refresh: (    targets?: ReadonlyArray<EnvironmentQueryTarget<PullRequestListStatsInput>>,  ) => 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, isPending: query.isPending, refresh: query.refresh };}