376 lines · 16.4 KB · TypeScript
RawDownload
import {  type GitActionProgressEvent,  type GitRunStackedActionInput,  type GitRunStackedActionResult,  type LocalApi,  ORCHESTRATION_WS_METHODS,  type RelayClientInstallProgressEvent,  type RelayClientStatus,  type ServerSettingsPatch,  type VcsStatusResult,  type VcsStatusStreamEvent,  WS_METHODS,} from "@t3tools/contracts";import { applyGitStatusStreamEvent } from "@t3tools/shared/git";import type * as Effect from "effect/Effect";import type * as Stream from "effect/Stream"; import { type WsRpcProtocolClient } from "./wsRpcProtocol.ts";import { WsTransport } from "./wsTransport.ts"; type RpcTag = keyof WsRpcProtocolClient & string;type RpcMethod<TTag extends RpcTag> = WsRpcProtocolClient[TTag];type RpcInput<TTag extends RpcTag> = Parameters<RpcMethod<TTag>>[0]; interface StreamSubscriptionOptions {  readonly onResubscribe?: () => void;} function subscriptionOptions(  options: StreamSubscriptionOptions | undefined,  tag: string,): StreamSubscriptionOptions & { readonly tag: string } {  return {    ...options,    tag,  };} type RpcUnaryMethod<TTag extends RpcTag> =  RpcMethod<TTag> extends (input: any, options?: any) => Effect.Effect<infer TSuccess, any, any>    ? (input: RpcInput<TTag>) => Promise<TSuccess>    : never; type RpcUnaryNoArgMethod<TTag extends RpcTag> =  RpcMethod<TTag> extends (input: any, options?: any) => Effect.Effect<infer TSuccess, any, any>    ? () => Promise<TSuccess>    : never; type RpcStreamMethod<TTag extends RpcTag> =  RpcMethod<TTag> extends (input: any, options?: any) => Stream.Stream<infer TEvent, any, any>    ? (listener: (event: TEvent) => void, options?: StreamSubscriptionOptions) => () => void    : never; type RpcInputStreamMethod<TTag extends RpcTag> =  RpcMethod<TTag> extends (input: any, options?: any) => Stream.Stream<infer TEvent, any, any>    ? (        input: RpcInput<TTag>,        listener: (event: TEvent) => void,        options?: StreamSubscriptionOptions,      ) => () => void    : never; interface GitRunStackedActionOptions {  readonly onProgress?: (event: GitActionProgressEvent) => void;} export interface WsRpcClient {  readonly dispose: () => Promise<void>;  readonly reconnect: () => Promise<void>;  readonly isHeartbeatFresh: () => boolean;  readonly terminal: {    readonly open: RpcUnaryMethod<typeof WS_METHODS.terminalOpen>;    readonly attach: RpcInputStreamMethod<typeof WS_METHODS.terminalAttach>;    readonly write: RpcUnaryMethod<typeof WS_METHODS.terminalWrite>;    readonly resize: RpcUnaryMethod<typeof WS_METHODS.terminalResize>;    readonly clear: RpcUnaryMethod<typeof WS_METHODS.terminalClear>;    readonly restart: RpcUnaryMethod<typeof WS_METHODS.terminalRestart>;    readonly close: RpcUnaryMethod<typeof WS_METHODS.terminalClose>;    readonly onEvent: RpcStreamMethod<typeof WS_METHODS.subscribeTerminalEvents>;    readonly onMetadata: RpcStreamMethod<typeof WS_METHODS.subscribeTerminalMetadata>;  };  readonly projects: {    readonly searchEntries: RpcUnaryMethod<typeof WS_METHODS.projectsSearchEntries>;    readonly writeFile: RpcUnaryMethod<typeof WS_METHODS.projectsWriteFile>;  };  readonly filesystem: {    readonly browse: RpcUnaryMethod<typeof WS_METHODS.filesystemBrowse>;  };  readonly sourceControl: {    readonly lookupRepository: RpcUnaryMethod<typeof WS_METHODS.sourceControlLookupRepository>;    readonly cloneRepository: RpcUnaryMethod<typeof WS_METHODS.sourceControlCloneRepository>;    readonly publishRepository: RpcUnaryMethod<typeof WS_METHODS.sourceControlPublishRepository>;  };  readonly shell: {    readonly openInEditor: (input: {      readonly cwd: Parameters<LocalApi["shell"]["openInEditor"]>[0];      readonly editor: Parameters<LocalApi["shell"]["openInEditor"]>[1];    }) => ReturnType<LocalApi["shell"]["openInEditor"]>;  };  readonly vcs: {    readonly pull: RpcUnaryMethod<typeof WS_METHODS.vcsPull>;    readonly refreshStatus: RpcUnaryMethod<typeof WS_METHODS.vcsRefreshStatus>;    readonly onStatus: (      input: RpcInput<typeof WS_METHODS.subscribeVcsStatus>,      listener: (status: VcsStatusResult) => void,      options?: StreamSubscriptionOptions,    ) => () => void;    readonly listRefs: RpcUnaryMethod<typeof WS_METHODS.vcsListRefs>;    readonly createWorktree: RpcUnaryMethod<typeof WS_METHODS.vcsCreateWorktree>;    readonly removeWorktree: RpcUnaryMethod<typeof WS_METHODS.vcsRemoveWorktree>;    readonly createRef: RpcUnaryMethod<typeof WS_METHODS.vcsCreateRef>;    readonly switchRef: RpcUnaryMethod<typeof WS_METHODS.vcsSwitchRef>;    readonly init: RpcUnaryMethod<typeof WS_METHODS.vcsInit>;  };  readonly git: {    readonly runStackedAction: (      input: GitRunStackedActionInput,      options?: GitRunStackedActionOptions,    ) => Promise<GitRunStackedActionResult>;    readonly resolvePullRequest: RpcUnaryMethod<typeof WS_METHODS.gitResolvePullRequest>;    readonly preparePullRequestThread: RpcUnaryMethod<      typeof WS_METHODS.gitPreparePullRequestThread    >;  };  readonly review: {    readonly getDiffPreview: RpcUnaryMethod<typeof WS_METHODS.reviewGetDiffPreview>;  };  readonly server: {    readonly getConfig: RpcUnaryNoArgMethod<typeof WS_METHODS.serverGetConfig>;    readonly refreshProviders: (      input?: RpcInput<typeof WS_METHODS.serverRefreshProviders>,    ) => ReturnType<RpcUnaryMethod<typeof WS_METHODS.serverRefreshProviders>>;    readonly discoverSourceControl: RpcUnaryNoArgMethod<      typeof WS_METHODS.serverDiscoverSourceControl    >;    readonly updateProvider: RpcUnaryMethod<typeof WS_METHODS.serverUpdateProvider>;    readonly upsertKeybinding: RpcUnaryMethod<typeof WS_METHODS.serverUpsertKeybinding>;    readonly removeKeybinding: RpcUnaryMethod<typeof WS_METHODS.serverRemoveKeybinding>;    readonly getSettings: RpcUnaryNoArgMethod<typeof WS_METHODS.serverGetSettings>;    readonly updateSettings: (      patch: ServerSettingsPatch,    ) => ReturnType<RpcUnaryMethod<typeof WS_METHODS.serverUpdateSettings>>;    readonly subscribeConfig: RpcStreamMethod<typeof WS_METHODS.subscribeServerConfig>;    readonly subscribeLifecycle: RpcStreamMethod<typeof WS_METHODS.subscribeServerLifecycle>;    readonly subscribeAuthAccess: RpcStreamMethod<typeof WS_METHODS.subscribeAuthAccess>;    readonly getTraceDiagnostics: RpcUnaryNoArgMethod<typeof WS_METHODS.serverGetTraceDiagnostics>;    readonly getProcessDiagnostics: RpcUnaryNoArgMethod<      typeof WS_METHODS.serverGetProcessDiagnostics    >;    readonly getProcessResourceHistory: RpcUnaryMethod<      typeof WS_METHODS.serverGetProcessResourceHistory    >;    readonly signalProcess: RpcUnaryMethod<typeof WS_METHODS.serverSignalProcess>;  };  readonly cloud: {    readonly getRelayClientStatus: RpcUnaryNoArgMethod<typeof WS_METHODS.cloudGetRelayClientStatus>;    readonly installRelayClient: (      onProgress?: (event: RelayClientInstallProgressEvent) => void,    ) => Promise<RelayClientStatus>;  };  readonly orchestration: {    readonly dispatchCommand: RpcUnaryMethod<typeof ORCHESTRATION_WS_METHODS.dispatchCommand>;    readonly getTurnDiff: RpcUnaryMethod<typeof ORCHESTRATION_WS_METHODS.getTurnDiff>;    readonly getFullThreadDiff: RpcUnaryMethod<typeof ORCHESTRATION_WS_METHODS.getFullThreadDiff>;    readonly getArchivedShellSnapshot: RpcUnaryNoArgMethod<      typeof ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot    >;    readonly subscribeShell: RpcStreamMethod<typeof ORCHESTRATION_WS_METHODS.subscribeShell>;    readonly subscribeThread: RpcInputStreamMethod<typeof ORCHESTRATION_WS_METHODS.subscribeThread>;  };} export interface CreateWsRpcClientOptions {  /** Runs immediately before `transport.reconnect()` (e.g. reset reconnect UI/backoff state). */  readonly beforeReconnect?: () => void;} export function createWsRpcClient(  transport: WsTransport,  options?: CreateWsRpcClientOptions,): WsRpcClient {  return {    dispose: () => transport.dispose(),    isHeartbeatFresh: () => transport.isHeartbeatFresh(),    reconnect: async () => {      options?.beforeReconnect?.();      await transport.reconnect();    },    terminal: {      open: (input) => transport.request((client) => client[WS_METHODS.terminalOpen](input)),      attach: (input, listener, options) =>        transport.subscribe(          (client) => client[WS_METHODS.terminalAttach](input),          listener,          subscriptionOptions(options, WS_METHODS.terminalAttach),        ),      write: (input) => transport.request((client) => client[WS_METHODS.terminalWrite](input)),      resize: (input) => transport.request((client) => client[WS_METHODS.terminalResize](input)),      clear: (input) => transport.request((client) => client[WS_METHODS.terminalClear](input)),      restart: (input) => transport.request((client) => client[WS_METHODS.terminalRestart](input)),      close: (input) => transport.request((client) => client[WS_METHODS.terminalClose](input)),      onEvent: (listener, options) =>        transport.subscribe(          (client) => client[WS_METHODS.subscribeTerminalEvents]({}),          listener,          subscriptionOptions(options, WS_METHODS.subscribeTerminalEvents),        ),      onMetadata: (listener, options) =>        transport.subscribe(          (client) => client[WS_METHODS.subscribeTerminalMetadata]({}),          listener,          subscriptionOptions(options, WS_METHODS.subscribeTerminalMetadata),        ),    },    projects: {      searchEntries: (input) =>        transport.request((client) => client[WS_METHODS.projectsSearchEntries](input)),      writeFile: (input) =>        transport.request((client) => client[WS_METHODS.projectsWriteFile](input)),    },    filesystem: {      browse: (input) => transport.request((client) => client[WS_METHODS.filesystemBrowse](input)),    },    sourceControl: {      lookupRepository: (input) =>        transport.request((client) => client[WS_METHODS.sourceControlLookupRepository](input)),      cloneRepository: (input) =>        transport.request((client) => client[WS_METHODS.sourceControlCloneRepository](input)),      publishRepository: (input) =>        transport.request((client) => client[WS_METHODS.sourceControlPublishRepository](input)),    },    shell: {      openInEditor: (input) =>        transport.request((client) => client[WS_METHODS.shellOpenInEditor](input)),    },    vcs: {      pull: (input) => transport.request((client) => client[WS_METHODS.vcsPull](input)),      refreshStatus: (input) =>        transport.request((client) => client[WS_METHODS.vcsRefreshStatus](input)),      onStatus: (input, listener, options) => {        let current: VcsStatusResult | null = null;        return transport.subscribe(          (client) => client[WS_METHODS.subscribeVcsStatus](input),          (event: VcsStatusStreamEvent) => {            current = applyGitStatusStreamEvent(current, event);            listener(current);          },          subscriptionOptions(options, WS_METHODS.subscribeVcsStatus),        );      },      listRefs: (input) => transport.request((client) => client[WS_METHODS.vcsListRefs](input)),      createWorktree: (input) =>        transport.request((client) => client[WS_METHODS.vcsCreateWorktree](input)),      removeWorktree: (input) =>        transport.request((client) => client[WS_METHODS.vcsRemoveWorktree](input)),      createRef: (input) => transport.request((client) => client[WS_METHODS.vcsCreateRef](input)),      switchRef: (input) => transport.request((client) => client[WS_METHODS.vcsSwitchRef](input)),      init: (input) => transport.request((client) => client[WS_METHODS.vcsInit](input)),    },    git: {      runStackedAction: async (input, options) => {        let result: GitRunStackedActionResult | null = null;         await transport.requestStream(          (client) => client[WS_METHODS.gitRunStackedAction](input),          (event) => {            options?.onProgress?.(event);            if (event.kind === "action_finished") {              result = event.result;            }          },        );         if (result) {          return result;        }         throw new Error("Git action stream completed without a final result.");      },      resolvePullRequest: (input) =>        transport.request((client) => client[WS_METHODS.gitResolvePullRequest](input)),      preparePullRequestThread: (input) =>        transport.request((client) => client[WS_METHODS.gitPreparePullRequestThread](input)),    },    review: {      getDiffPreview: (input) =>        transport.request((client) => client[WS_METHODS.reviewGetDiffPreview](input)),    },    server: {      getConfig: () => transport.request((client) => client[WS_METHODS.serverGetConfig]({})),      refreshProviders: (input) =>        transport.request((client) => client[WS_METHODS.serverRefreshProviders](input ?? {})),      discoverSourceControl: () =>        transport.request((client) => client[WS_METHODS.serverDiscoverSourceControl]({})),      updateProvider: (input) =>        transport.request((client) => client[WS_METHODS.serverUpdateProvider](input)),      upsertKeybinding: (input) =>        transport.request((client) => client[WS_METHODS.serverUpsertKeybinding](input)),      removeKeybinding: (input) =>        transport.request((client) => client[WS_METHODS.serverRemoveKeybinding](input)),      getSettings: () => transport.request((client) => client[WS_METHODS.serverGetSettings]({})),      updateSettings: (patch) =>        transport.request((client) => client[WS_METHODS.serverUpdateSettings]({ patch })),      subscribeConfig: (listener, options) =>        transport.subscribe(          (client) => client[WS_METHODS.subscribeServerConfig]({}),          listener,          subscriptionOptions(options, WS_METHODS.subscribeServerConfig),        ),      subscribeLifecycle: (listener, options) =>        transport.subscribe(          (client) => client[WS_METHODS.subscribeServerLifecycle]({}),          listener,          subscriptionOptions(options, WS_METHODS.subscribeServerLifecycle),        ),      subscribeAuthAccess: (listener, options) =>        transport.subscribe(          (client) => client[WS_METHODS.subscribeAuthAccess]({}),          listener,          subscriptionOptions(options, WS_METHODS.subscribeAuthAccess),        ),      getTraceDiagnostics: () =>        transport.request((client) => client[WS_METHODS.serverGetTraceDiagnostics]({})),      getProcessDiagnostics: () =>        transport.request((client) => client[WS_METHODS.serverGetProcessDiagnostics]({})),      getProcessResourceHistory: (input) =>        transport.request((client) => client[WS_METHODS.serverGetProcessResourceHistory](input)),      signalProcess: (input) =>        transport.request((client) => client[WS_METHODS.serverSignalProcess](input)),    },    cloud: {      getRelayClientStatus: () =>        transport.request((client) => client[WS_METHODS.cloudGetRelayClientStatus]({})),      installRelayClient: async (onProgress) => {        let installed: RelayClientStatus | null = null;        await transport.requestStream(          (client) => client[WS_METHODS.cloudInstallRelayClient]({}),          (event) => {            onProgress?.(event);            if (event.type === "complete") {              installed = event.status;            }          },        );        if (installed) {          return installed;        }        throw new Error("Relay client install stream completed without a final status.");      },    },    orchestration: {      dispatchCommand: (input) =>        transport.request((client) => client[ORCHESTRATION_WS_METHODS.dispatchCommand](input)),      getTurnDiff: (input) =>        transport.request((client) => client[ORCHESTRATION_WS_METHODS.getTurnDiff](input)),      getFullThreadDiff: (input) =>        transport.request((client) => client[ORCHESTRATION_WS_METHODS.getFullThreadDiff](input)),      getArchivedShellSnapshot: () =>        transport.request((client) =>          client[ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot]({}),        ),      subscribeShell: (listener, options) =>        transport.subscribe(          (client) => client[ORCHESTRATION_WS_METHODS.subscribeShell]({}),          listener,          subscriptionOptions(options, ORCHESTRATION_WS_METHODS.subscribeShell),        ),      subscribeThread: (input, listener, options) =>        transport.subscribe(          (client) => client[ORCHESTRATION_WS_METHODS.subscribeThread](input),          listener,          subscriptionOptions(options, ORCHESTRATION_WS_METHODS.subscribeThread),        ),    },  };}