263 lines · 10.3 KB · TypeScript
RawDownload
import {  WS_METHODS,  type PullRequestDetail,  type PullRequestDiffInput,  type PullRequestSummary,  type VcsStatusResult,} from "@t3tools/contracts";import * as Data from "effect/Data";import * as Effect from "effect/Effect";import * as Option from "effect/Option";import * as SubscriptionRef from "effect/SubscriptionRef";import { Atom } from "effect/unstable/reactivity"; import {  createAtomCommandScheduler,  createEnvironmentRpcCommand,  createEnvironmentRpcQueryAtomFamily,  createEnvironmentRpcSubscriptionAtomFamily,  createEnvironmentQueryAtomFamily,} from "./runtime.ts";import { PullRequestDiffLoader } from "./pullRequestDiffHttp.ts";import type { EnvironmentRegistry } from "../connection/registry.ts";import { EnvironmentSupervisor } from "../connection/supervisor.ts"; export {  type PullRequestDiffLoadError,  PullRequestDiffCredentialRejectedError,  PullRequestDiffLoader,  pullRequestDiffLoaderLayer,} from "./pullRequestDiffHttp.ts"; export class EnvironmentHttpConnectionNotReadyError extends Data.TaggedError(  "EnvironmentHttpConnectionNotReadyError",)<{ readonly message: string }> {} export const LINKED_PULL_REQUEST_IDLE_TTL_MS = 5_000; function createPullRequestRefreshAtomFamily<R, E>(  runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,) {  return createEnvironmentRpcSubscriptionAtomFamily(runtime, {    label: "environment-data:pull-requests:turn-refreshes",    tag: WS_METHODS.pullRequestsSubscribeRefreshes,  });} /** Refresh only the live fields a linked thread renders. */export function createLinkedPullRequestSummaryAtomFamily<R, E>(  runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,  refreshes = createPullRequestRefreshAtomFamily(runtime),) {  return createEnvironmentRpcQueryAtomFamily(runtime, {    label: "environment-data:pull-requests:linked-summary",    tag: WS_METHODS.pullRequestsSummary,    staleTimeMs: 60_000,    refreshIntervalMs: 60_000,    idleTtlMs: LINKED_PULL_REQUEST_IDLE_TTL_MS,    refreshTrigger: ({ environmentId }) => refreshes({ environmentId, input: {} }),  });} export function pullRequestDetailToVcsStatus(  detail: PullRequestDetail | PullRequestSummary,): NonNullable<VcsStatusResult["pr"]> {  return {    number: detail.number,    title: detail.title,    url: detail.url,    baseRef: detail.baseBranch,    headRef: detail.headBranch,    state: detail.state,    ...(detail.isDraft === true ? { isDraft: true } : {}),    updatedAt: detail.updatedAt,  };} /** * Reopening a PR within a minute reuses detail and activity. Explicit refreshes and * turn notifications still revalidate. Mutations run serially per environment: actions on the same * pull request are order-sensitive, and the detail view refetches after each one. */export function createPullRequestEnvironmentAtoms<R, E>(  runtime: Atom.AtomRuntime<EnvironmentRegistry | PullRequestDiffLoader | R, E>,) {  const refreshes = createPullRequestRefreshAtomFamily(runtime);  const commandScheduler = createAtomCommandScheduler();  const serialPerEnvironment = {    mode: "serial",    key: ({ environmentId }: { readonly environmentId: string }) => environmentId,  } as const;  const activity = createEnvironmentRpcQueryAtomFamily(runtime, {    label: "environment-data:pull-requests:activity",    tag: WS_METHODS.pullRequestsActivity,    staleTimeMs: 60_000,    refreshTrigger: ({ environmentId }) => refreshes({ environmentId, input: {} }),  });  return {    refreshes,    list: createEnvironmentRpcQueryAtomFamily(runtime, {      label: "environment-data:pull-requests:list",      tag: WS_METHODS.pullRequestsList,      staleTimeMs: 30_000,      refreshTrigger: ({ environmentId, input }) =>        input.cursors === undefined ? refreshes({ environmentId, input: {} }) : undefined,    }),    /**     * The line counts for rows the listing has already handed over. Its own query because the     * listing is quicker without them — measured over twelve repositories, ~4.0s against ~7.1s —     * so the rows arrive first and their stats a moment later. Kept longer than the listing:     * a change request's size only moves when somebody pushes to it.     */    listStats: createEnvironmentRpcQueryAtomFamily(runtime, {      label: "environment-data:pull-requests:list-stats",      tag: WS_METHODS.pullRequestsListStats,      staleTimeMs: 60_000,      refreshTrigger: ({ environmentId }) => refreshes({ environmentId, input: {} }),    }),    detail: createEnvironmentRpcQueryAtomFamily(runtime, {      label: "environment-data:pull-requests:detail",      tag: WS_METHODS.pullRequestsDetail,      staleTimeMs: 60_000,      refreshTrigger: ({ environmentId }) => refreshes({ environmentId, input: {} }),    }),    activity,    threadComments: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:thread-comments",      tag: WS_METHODS.pullRequestsThreadComments,      scheduler: commandScheduler,      concurrency: {        mode: "singleFlight",        key: ({ environmentId, input }) =>          JSON.stringify([environmentId, input.threadId, input.cursor]),      },    }),    diff: createEnvironmentQueryAtomFamily(runtime, {      label: "environment-data:pull-requests:diff",      staleTimeMs: 60_000,      execute: (input: PullRequestDiffInput) =>        Effect.gen(function* () {          const supervisor = yield* EnvironmentSupervisor;          const loader = yield* PullRequestDiffLoader;          const prepared = yield* SubscriptionRef.get(supervisor.prepared);          if (Option.isNone(prepared)) {            return yield* new EnvironmentHttpConnectionNotReadyError({              message: "The environment HTTP connection is not ready.",            });          }          return yield* loader.load(prepared.value, input);        }),    }),    diffFileContents: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:diff-file-contents",      tag: WS_METHODS.pullRequestsDiffFileContents,      scheduler: commandScheduler,      concurrency: {        mode: "singleFlight",        key: ({ environmentId, input }) =>          JSON.stringify([            environmentId,            input.projectId,            input.repository,            input.number,            input.commit ?? null,            input.changeType,            input.oldPath,            input.newPath,          ]),      },    }),    runAction: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:run-action",      tag: WS_METHODS.pullRequestsRunAction,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,    }),    update: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:update",      tag: WS_METHODS.pullRequestsUpdate,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,    }),    comment: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:comment",      tag: WS_METHODS.pullRequestsComment,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,    }),    updateComment: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:update-comment",      tag: WS_METHODS.pullRequestsUpdateComment,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,      onSuccess: ({ environmentId, input: { projectId, repository, number } }, registry) =>        Effect.sync(() =>          registry.refresh(activity({ environmentId, input: { projectId, repository, number } })),        ),    }),    submitReview: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:submit-review",      tag: WS_METHODS.pullRequestsSubmitReview,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,    }),    replyToThread: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:reply-to-thread",      tag: WS_METHODS.pullRequestsReplyToThread,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,    }),    /**     * Its own query rather than part of the detail: the people who may be asked are only wanted     * once somebody opens the reviewer menu, so this atom is read then and not before. Kept fresh     * for a minute, because who has access to a repository changes far more slowly than the     * change request it is being read for.     */    reviewerCandidates: createEnvironmentRpcQueryAtomFamily(runtime, {      label: "environment-data:pull-requests:reviewer-candidates",      tag: WS_METHODS.pullRequestsReviewerCandidates,      staleTimeMs: 60_000,    }),    requestReviewers: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:request-reviewers",      tag: WS_METHODS.pullRequestsRequestReviewers,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,    }),    /** Read when the label menu opens, and kept for a minute, like the reviewer candidates. */    labelCandidates: createEnvironmentRpcQueryAtomFamily(runtime, {      label: "environment-data:pull-requests:label-candidates",      tag: WS_METHODS.pullRequestsLabelCandidates,      staleTimeMs: 60_000,    }),    setLabels: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:set-labels",      tag: WS_METHODS.pullRequestsSetLabels,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,    }),    setThreadResolution: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:set-thread-resolution",      tag: WS_METHODS.pullRequestsSetThreadResolution,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,    }),    setReaction: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:set-reaction",      tag: WS_METHODS.pullRequestsSetReaction,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,    }),    /**     * Explicit refresh: forget the server's cached answers, then re-run the reads. A separate     * request rather than a flag on a read, so only a person's refresh spends host requests     * while every silent re-read shares the cache.     */    invalidate: createEnvironmentRpcCommand(runtime, {      label: "environment-data:pull-requests:invalidate",      tag: WS_METHODS.pullRequestsInvalidate,      scheduler: commandScheduler,      concurrency: serialPerEnvironment,    }),  };}