269 lines · 10.5 KB · TypeScript
RawDownload
import * as Context from "effect/Context";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Stdio from "effect/Stdio";import * as Stream from "effect/Stream";import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"; import * as CodexRpc from "./_generated/meta.gen.ts";import * as CodexError from "./errors.ts";import * as CodexProtocol from "./protocol.ts";import {  decodeNotificationPayload,  decodeOptionalPayload,  encodeOptionalPayload,  runHandler,} from "./_internal/shared.ts";import { makeChildStdio, makeTerminationError } from "./_internal/stdio.ts"; export interface CodexAppServerClientOptions {  readonly logIncoming?: boolean;  readonly logOutgoing?: boolean;  readonly logger?: (    event: CodexProtocol.CodexAppServerProtocolLogEvent,  ) => Effect.Effect<void, never>;} interface CodexAppServerClientRaw {  readonly notifications: CodexProtocol.CodexAppServerPatchedProtocol["incomingNotifications"];  readonly requests: CodexProtocol.CodexAppServerPatchedProtocol["incomingRequests"];  readonly request: CodexProtocol.CodexAppServerPatchedProtocol["request"];  readonly notify: CodexProtocol.CodexAppServerPatchedProtocol["notify"];  readonly respond: CodexProtocol.CodexAppServerPatchedProtocol["respond"];  readonly respondError: CodexProtocol.CodexAppServerPatchedProtocol["respondError"];} export class CodexAppServerClient extends Context.Service<  CodexAppServerClient,  {    readonly raw: CodexAppServerClientRaw;    readonly request: <M extends CodexRpc.ClientRequestMethod>(      method: M,      payload: CodexRpc.ClientRequestParamsByMethod[M],    ) => Effect.Effect<CodexRpc.ClientRequestResponsesByMethod[M], CodexError.CodexAppServerError>;    readonly notify: <M extends CodexRpc.ClientNotificationMethod>(      method: M,      payload: CodexRpc.ClientNotificationParamsByMethod[M],    ) => Effect.Effect<void, CodexError.CodexAppServerError>;    readonly handleServerRequest: <M extends CodexRpc.ServerRequestMethod>(      method: M,      handler: (        payload: CodexRpc.ServerRequestParamsByMethod[M],      ) => Effect.Effect<        CodexRpc.ServerRequestResponsesByMethod[M],        CodexError.CodexAppServerError      >,    ) => Effect.Effect<void>;    readonly handleServerNotification: <M extends CodexRpc.ServerNotificationMethod>(      method: M,      handler: (        payload: CodexRpc.ServerNotificationParamsByMethod[M],      ) => Effect.Effect<void, CodexError.CodexAppServerError>,    ) => Effect.Effect<void>;    readonly handleUnknownServerRequest: (      handler: (        method: string,        params: unknown,      ) => Effect.Effect<unknown, CodexError.CodexAppServerError>,    ) => Effect.Effect<void>;    readonly handleUnknownServerNotification: (      handler: (        method: string,        params: unknown,      ) => Effect.Effect<void, CodexError.CodexAppServerError>,    ) => Effect.Effect<void>;  }>()("effect-codex-app-server/client/CodexAppServerClient") {} type ServerRequestHandler = (  payload: unknown,) => Effect.Effect<unknown, CodexError.CodexAppServerError>;type ServerNotificationHandler = (  payload: unknown,) => Effect.Effect<void, CodexError.CodexAppServerError>; export const make = Effect.fn("effect-codex-app-server/CodexAppServerClient.make")(function* (  stdio: Stdio.Stdio,  options: CodexAppServerClientOptions = {},  terminationError?: Effect.Effect<CodexError.CodexAppServerError>,): Effect.fn.Return<CodexAppServerClient["Service"], never, Scope.Scope> {  const requestHandlers = new Map<string, ServerRequestHandler>();  const notificationHandlers = new Map<string, Array<ServerNotificationHandler>>();  let unknownRequestHandler:    | ((method: string, params: unknown) => Effect.Effect<unknown, CodexError.CodexAppServerError>)    | undefined;  let unknownNotificationHandler:    | ((method: string, params: unknown) => Effect.Effect<void, CodexError.CodexAppServerError>)    | undefined;   const getServerRequestParamSchema = <M extends CodexRpc.ServerRequestMethod>(    method: M,  ):    | Schema.Codec<CodexRpc.ServerRequestParamsByMethod[M], CodexRpc.ServerRequestParamsByMethod[M]>    | undefined => CodexRpc.SERVER_REQUEST_PARAMS[method] as never;   const getServerRequestResponseSchema = <M extends CodexRpc.ServerRequestMethod>(    method: M,  ):    | Schema.Codec<        CodexRpc.ServerRequestResponsesByMethod[M],        CodexRpc.ServerRequestResponsesByMethod[M]      >    | undefined => CodexRpc.SERVER_REQUEST_RESPONSES[method] as never;   const getClientRequestParamSchema = <M extends CodexRpc.ClientRequestMethod>(    method: M,  ):    | Schema.Codec<CodexRpc.ClientRequestParamsByMethod[M], CodexRpc.ClientRequestParamsByMethod[M]>    | undefined => CodexRpc.CLIENT_REQUEST_PARAMS[method] as never;   const getClientRequestResponseSchema = <M extends CodexRpc.ClientRequestMethod>(    method: M,  ):    | Schema.Codec<        CodexRpc.ClientRequestResponsesByMethod[M],        CodexRpc.ClientRequestResponsesByMethod[M]      >    | undefined => CodexRpc.CLIENT_REQUEST_RESPONSES[method] as never;   const getClientNotificationParamSchema = <M extends CodexRpc.ClientNotificationMethod>(    method: M,  ):    | Schema.Codec<        CodexRpc.ClientNotificationParamsByMethod[M],        CodexRpc.ClientNotificationParamsByMethod[M]      >    | undefined => CodexRpc.CLIENT_NOTIFICATION_PARAMS[method] as never;   const dispatchNotification = (    notification: CodexProtocol.CodexAppServerIncomingNotification,  ): Effect.Effect<void, never> => {    const schema =      notification.method in CodexRpc.SERVER_NOTIFICATION_PARAMS        ? CodexRpc.SERVER_NOTIFICATION_PARAMS[            notification.method as CodexRpc.ServerNotificationMethod          ]        : undefined;    const handlers = notificationHandlers.get(notification.method) ?? [];     if (schema) {      return decodeNotificationPayload(notification.method, schema, notification.params).pipe(        Effect.flatMap((decoded) =>          Effect.forEach(handlers, (handler) => handler(decoded), { discard: true }),        ),        Effect.catch(() => Effect.void),      );    }     return unknownNotificationHandler      ? unknownNotificationHandler(notification.method, notification.params).pipe(          Effect.catch(() => Effect.void),        )      : Effect.void;  };   const dispatchRequest = (    request: CodexProtocol.CodexAppServerIncomingRequest,  ): Effect.Effect<unknown, CodexError.CodexAppServerError> => {    if (request.method in CodexRpc.SERVER_REQUEST_PARAMS) {      const method = request.method as CodexRpc.ServerRequestMethod;      const payloadSchema = getServerRequestParamSchema(method);      const responseSchema = getServerRequestResponseSchema(method);      const handler = requestHandlers.get(method);       return decodeOptionalPayload(method, payloadSchema, request.params).pipe(        Effect.flatMap((decoded) => runHandler(handler, decoded, method)),        Effect.flatMap((result) => encodeOptionalPayload(method, responseSchema, result)),      );    }     return unknownRequestHandler      ? unknownRequestHandler(request.method, request.params)      : Effect.fail(CodexError.CodexAppServerRequestError.methodNotFound(request.method));  };   const transport = yield* CodexProtocol.makeCodexAppServerPatchedProtocol({    stdio,    ...(terminationError ? { terminationError } : {}),    ...(options.logIncoming !== undefined ? { logIncoming: options.logIncoming } : {}),    ...(options.logOutgoing !== undefined ? { logOutgoing: options.logOutgoing } : {}),    ...(options.logger ? { logger: options.logger } : {}),    onNotification: dispatchNotification,    onRequest: dispatchRequest,  });   const request = <M extends CodexRpc.ClientRequestMethod>(    method: M,    payload: CodexRpc.ClientRequestParamsByMethod[M],  ): Effect.Effect<CodexRpc.ClientRequestResponsesByMethod[M], CodexError.CodexAppServerError> =>    encodeOptionalPayload(method, getClientRequestParamSchema(method), payload).pipe(      Effect.flatMap((encoded) => transport.request(method, encoded)),      Effect.flatMap(        (          raw,        ): Effect.Effect<          CodexRpc.ClientRequestResponsesByMethod[M],          CodexError.CodexAppServerError        > => decodeOptionalPayload(method, getClientRequestResponseSchema(method), raw),      ),    );   const notify = <M extends CodexRpc.ClientNotificationMethod>(    method: M,    payload: CodexRpc.ClientNotificationParamsByMethod[M],  ) =>    encodeOptionalPayload(method, getClientNotificationParamSchema(method), payload).pipe(      Effect.flatMap((encoded) => transport.notify(method, encoded)),    );   return CodexAppServerClient.of({    raw: {      notifications: transport.incomingNotifications,      requests: transport.incomingRequests,      request: transport.request,      notify: transport.notify,      respond: transport.respond,      respondError: transport.respondError,    },    request,    notify,    handleServerRequest: (method, handler) =>      Effect.sync(() => {        requestHandlers.set(method, handler as ServerRequestHandler);      }),    handleServerNotification: (method, handler) =>      Effect.sync(() => {        const current = notificationHandlers.get(method) ?? [];        current.push(handler as ServerNotificationHandler);        notificationHandlers.set(method, current);      }),    handleUnknownServerRequest: (handler) =>      Effect.sync(() => {        unknownRequestHandler = handler;      }),    handleUnknownServerNotification: (handler) =>      Effect.sync(() => {        unknownNotificationHandler = handler;      }),  });}); export const layer = (  stdio: Stdio.Stdio,  options: CodexAppServerClientOptions = {},): Layer.Layer<CodexAppServerClient> => Layer.effect(CodexAppServerClient, make(stdio, options)); export const layerChildProcess = (  handle: ChildProcessSpawner.ChildProcessHandle,  options: CodexAppServerClientOptions = {},): Layer.Layer<CodexAppServerClient> =>  Layer.effect(CodexAppServerClient, makeChildProcessClient(handle, options)); const makeChildProcessClient = Effect.fn(  "effect-codex-app-server/CodexAppServerClient.makeChildProcessClient",)(function* (handle: ChildProcessSpawner.ChildProcessHandle, options: CodexAppServerClientOptions) {  yield* Stream.runDrain(handle.stderr).pipe(Effect.ignore, Effect.forkScoped);  return yield* make(makeChildStdio(handle), options, makeTerminationError(handle));});