Rhys Sullivanfmtc92c57d6 months agoHistory
129 lines · 5.0 KB · TypeScript
RawDownload
import { HttpApiBuilder, HttpApiSwagger } from "effect/unstable/httpapi";import { HttpRouter, HttpServer } from "effect/unstable/http";import { Context, Effect, Layer, ManagedRuntime } from "effect"; import { observabilityMiddleware } from "@executor-js/api";import {  CoreHandlers,  ExecutorService,  ExecutionEngineService,  composePluginApi,  composePluginHandlers,} from "@executor-js/api/server";import { createExecutionEngine } from "@executor-js/execution";import { makeQuickJsExecutor } from "@executor-js/runtime-quickjs";import { getExecutorBundle } from "./executor";import { createMcpRequestHandler, type McpRequestHandler } from "./mcp";import { ErrorCaptureLive } from "./observability"; // ---------------------------------------------------------------------------// Local server API.//// Every plugin contributes its `HttpApiGroup` and handler `Layer` through// the spec (`routes()` / `handlers(self)` on `PluginSpec`); the host folds// the group list into a single `HttpApi` and merges the handler layers// into the runtime. The plugin set is the union of `executor.config.ts`// (static, typed) and `executor.jsonc#plugins` (dynamic, jiti-loaded),// so `LocalApi` can't be constructed until the executor bundle resolves// — composition happens inside `createServerHandlers` instead of at// module-eval time.// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------// Server handlers// --------------------------------------------------------------------------- export type ServerHandlers = {  readonly api: {    readonly handler: (request: Request) => Promise<Response>;    readonly dispose: () => Promise<void>;  };  readonly mcp: McpRequestHandler;}; const closeServerHandlers = async (handlers: ServerHandlers): Promise<void> => {  await Effect.runPromise(    Effect.all(      [        Effect.tryPromise({          try: () => handlers.api.dispose(),          catch: (cause) => cause,        }).pipe(Effect.ignore),        Effect.tryPromise({          try: () => handlers.mcp.close(),          catch: (cause) => cause,        }).pipe(Effect.ignore),      ],      { concurrency: "unbounded" },    ),  );}; export const createServerHandlers = async (): Promise<ServerHandlers> => {  const { executor, plugins } = await getExecutorBundle();  const engine = createExecutionEngine({ executor, codeExecutor: makeQuickJsExecutor() });   const LocalApi = composePluginApi(plugins);  // `ErrorCaptureLive` logs causes to the console and returns a short  // correlation id. Provided above the handler + middleware layers so  // both the `withCapture` typed-channel translation AND the  // `observabilityMiddleware` defect catchall see the same  // implementation.  const LocalObservability = observabilityMiddleware(LocalApi);  const LocalApiBase = HttpApiBuilder.layer(LocalApi).pipe(    Layer.provide(CoreHandlers),    Layer.provide(LocalObservability),    Layer.provide(ErrorCaptureLive),  );   // Spec-based plugin handlers — each plugin's `handlers(self)` Layer is  // built against its own bundled HttpApi for full type safety inside the  // plugin, and merges into the runtime `LocalApi` by group identity.  // Each plugin's handler bodies that yield its `*ExtensionService` are  // satisfied because `composePluginHandlers` provides `executor[id]` to  // the plugin's own `Layer.succeed(*ExtensionService)(self)` wiring.  const SpecPluginHandlers = composePluginHandlers(plugins, executor);   const localApiLayer = LocalApiBase.pipe(    Layer.provideMerge(HttpApiSwagger.layer(LocalApi, { path: "/docs" })),    Layer.provideMerge(SpecPluginHandlers),    Layer.provideMerge(Layer.succeed(ExecutorService)(executor)),    Layer.provideMerge(Layer.succeed(ExecutionEngineService)(engine)),    Layer.provideMerge(HttpServer.layerServices),    Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })),  );  const api = HttpRouter.toWebHandler(localApiLayer);  const apiHandler: ServerHandlers["api"] = {    handler: (request) => api.handler(request),    dispose: api.dispose,  };   const mcp = createMcpRequestHandler({ engine });   return { api: apiHandler, mcp };}; export class ServerHandlersService extends Context.Service<ServerHandlersService, ServerHandlers>()(  "@executor-js/local/ServerHandlersService",) {} const ServerHandlersLive = Layer.effect(ServerHandlersService)(  Effect.acquireRelease(    Effect.promise(() => createServerHandlers()),    (handlers) => Effect.promise(() => closeServerHandlers(handlers)),  ),); const serverHandlersRuntime = ManagedRuntime.make(ServerHandlersLive); export const getServerHandlers = (): Promise<ServerHandlers> =>  serverHandlersRuntime.runPromise(ServerHandlersService.asEffect()); export const disposeServerHandlers = async (): Promise<void> => {  await Effect.runPromise(    Effect.tryPromise({      try: () => serverHandlersRuntime.dispose(),      catch: (cause) => cause,    }).pipe(Effect.ignore),  );};