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 executorConfig from "../../executor.config"; 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. No per-plugin imports here — adding a plugin to // `executor.config.ts` is sufficient. // // `plugins({})` is safe at module-eval time: `.routes()` doesn't touch // host deps; the `configFile` parameter only matters at runtime // invocation. The schema-gen CLI relies on the same property. // --------------------------------------------------------------------------- const LocalApi = composePluginApi(executorConfig.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), ); // --------------------------------------------------------------------------- // Server handlers // --------------------------------------------------------------------------- export type ServerHandlers = { readonly api: { readonly handler: (request: Request) => Promise; readonly dispose: () => Promise; }; readonly mcp: McpRequestHandler; }; const closeServerHandlers = async (handlers: ServerHandlers): Promise => { await Promise.all([ handlers.api.dispose().catch(() => undefined), handlers.mcp.close().catch(() => undefined), ]); }; export const createServerHandlers = async (): Promise => { const { executor, plugins } = await getExecutorBundle(); const engine = createExecutionEngine({ executor, codeExecutor: makeQuickJsExecutor() }); // 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()("@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 => serverHandlersRuntime.runPromise(ServerHandlersService.asEffect()); export const disposeServerHandlers = async (): Promise => { await serverHandlersRuntime.dispose().catch(() => undefined); };