import { WS_METHODS, type PullRequestDetail, type PullRequestDiffInput, 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, createEnvironmentQueryAtomFamily,} from "./runtime.ts";import { PullRequestDiffLoader } from "./pullRequestDiffHttp.ts";import type { EnvironmentRegistry } from "../connection/registry.ts";import { EnvironmentSupervisor } from "../connection/supervisor.ts"; export { PullRequestDiffLoader, pullRequestDiffLoaderLayer } from "./pullRequestDiffHttp.ts"; export class EnvironmentHttpConnectionNotReadyError extends Data.TaggedError( "EnvironmentHttpConnectionNotReadyError",)<{ readonly message: string }> {} /** Refresh a linked PR while its thread is visible so merges update the sidebar. */export function createLinkedPullRequestDetailAtomFamily<R, E>( runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,) { return createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:pull-requests:linked-detail", tag: WS_METHODS.pullRequestsDetail, staleTimeMs: 15_000, refreshIntervalMs: 30_000, });} export function pullRequestDetailToVcsStatus( detail: PullRequestDetail,): NonNullable<VcsStatusResult["pr"]> { return { number: detail.number, title: detail.title, url: detail.url, baseRef: detail.baseBranch, headRef: detail.headBranch, state: detail.state, updatedAt: detail.updatedAt, };} /** * Every read shells out to the GitHub CLI, so results are reused for a short while and * refreshed explicitly. Mutations run serially per environment: `gh` 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 commandScheduler = createAtomCommandScheduler(); const serialPerEnvironment = { mode: "serial", key: ({ environmentId }: { readonly environmentId: string }) => environmentId, } as const; return { list: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:pull-requests:list", tag: WS_METHODS.pullRequestsList, staleTimeMs: 30_000, }), /** * 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, }), detail: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:pull-requests:detail", tag: WS_METHODS.pullRequestsDetail, staleTimeMs: 15_000, }), activity: createEnvironmentRpcQueryAtomFamily(runtime, { label: "environment-data:pull-requests:activity", tag: WS_METHODS.pullRequestsActivity, staleTimeMs: 15_000, }), 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, }), 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, }), 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, }), };}
import {
WS_METHODS,
type PullRequestDetail,
type PullRequestDiffInput,
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,
createEnvironmentQueryAtomFamily,
} from "./runtime.ts";
import { PullRequestDiffLoader } from "./pullRequestDiffHttp.ts";
import type { EnvironmentRegistry } from "../connection/registry.ts";
import { EnvironmentSupervisor } from "../connection/supervisor.ts";
export { PullRequestDiffLoader, pullRequestDiffLoaderLayer } from "./pullRequestDiffHttp.ts";
export class EnvironmentHttpConnectionNotReadyError extends Data.TaggedError(
"EnvironmentHttpConnectionNotReadyError",
)<{ readonly message: string }> {}
/** Refresh a linked PR while its thread is visible so merges update the sidebar. */
export function createLinkedPullRequestDetailAtomFamily<R, E>(
runtime: Atom.AtomRuntime<EnvironmentRegistry | R, E>,
) {
return createEnvironmentRpcQueryAtomFamily(runtime, {
label: "environment-data:pull-requests:linked-detail",
tag: WS_METHODS.pullRequestsDetail,
staleTimeMs: 15_000,
refreshIntervalMs: 30_000,
});
}
export function pullRequestDetailToVcsStatus(
detail: PullRequestDetail,
): NonNullable<VcsStatusResult["pr"]> {
return {
number: detail.number,
title: detail.title,
url: detail.url,
baseRef: detail.baseBranch,
headRef: detail.headBranch,
state: detail.state,
updatedAt: detail.updatedAt,
};
}
/**
* Every read shells out to the GitHub CLI, so results are reused for a short while and
* refreshed explicitly. Mutations run serially per environment: `gh` 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 commandScheduler = createAtomCommandScheduler();
const serialPerEnvironment = {
mode: "serial",
key: ({ environmentId }: { readonly environmentId: string }) => environmentId,
} as const;
return {
list: createEnvironmentRpcQueryAtomFamily(runtime, {
label: "environment-data:pull-requests:list",
tag: WS_METHODS.pullRequestsList,
staleTimeMs: 30_000,
}),
/**
* 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,
}),
detail: createEnvironmentRpcQueryAtomFamily(runtime, {
label: "environment-data:pull-requests:detail",
tag: WS_METHODS.pullRequestsDetail,
staleTimeMs: 15_000,
}),
activity: createEnvironmentRpcQueryAtomFamily(runtime, {
label: "environment-data:pull-requests:activity",
tag: WS_METHODS.pullRequestsActivity,
staleTimeMs: 15_000,
}),
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,
}),
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,
}),
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,
}),
};
}