import { inject, injectable } from "@codemation/core"; import { HttpRequestJsonBodyReader } from "../HttpRequestJsonBodyReader"; import type { CommandBus } from "../../../application/bus/CommandBus"; import type { QueryBus } from "../../../application/bus/QueryBus"; import { ReplaceMutableRunWorkflowSnapshotCommand } from "../../../application/commands/ReplaceMutableRunWorkflowSnapshotCommand"; import { ReplayWorkflowNodeCommand } from "../../../application/commands/ReplayWorkflowNodeCommand"; import { SetPinnedNodeInputCommand } from "../../../application/commands/SetPinnedNodeInputCommand"; import { StartWorkflowRunCommand } from "../../../application/commands/StartWorkflowRunCommand"; import type { CreateRunRequest, RunCommandResult, RunNodeRequest, UpdateRunNodePinRequest, UpdateRunWorkflowSnapshotRequest, } from "../../../application/contracts/RunContracts"; import { GetRunStateQuery } from "../../../application/queries/GetRunStateQuery"; import { GetWorkflowRunDetailQuery } from "../../../application/queries/GetWorkflowRunDetailQuery"; import { ApplicationTokens } from "../../../applicationTokens"; import { ServerHttpErrorResponseFactory } from "../ServerHttpErrorResponseFactory"; import type { ServerHttpRouteParams } from "../ServerHttpRouteParams"; import { TelemetrySpanSseFanout } from "../../../application/telemetry/TelemetrySpanSseFanout"; import type { RunEvent } from "@codemation/core"; import { RunSseStreamHelper } from "../sse/RunSseStreamHelper"; const SSE_HEADERS: Record = { "content-type": "text/event-stream", "cache-control": "no-cache, no-transform", connection: "keep-alive", "x-accel-buffering": "no", }; @injectable() export class RunHttpRouteHandler { constructor( @inject(ApplicationTokens.QueryBus) private readonly queryBus: QueryBus, @inject(ApplicationTokens.CommandBus) private readonly commandBus: CommandBus, @inject(RunSseStreamHelper) private readonly sseStreamHelper: RunSseStreamHelper, @inject(TelemetrySpanSseFanout) private readonly telemetryFanout: TelemetrySpanSseFanout, ) {} private acceptsSse(request: Request): boolean { return request.headers.get("accept")?.includes("text/event-stream") ?? false; } private isRunTerminal(event: RunEvent): boolean { return ( event.kind === "runSaved" && (event.state.status === "completed" || event.state.status === "failed" || event.state.status === "halted") ); } private async streamRunResult(execute: () => Promise): Promise { const ctrl = this.sseStreamHelper.createStream(); await ctrl.start(); try { const result = await execute(); const { runId } = result; ctrl.bind(result, { belongsToStream: (e: RunEvent) => "runId" in e && e.runId === runId, isTerminal: (e: RunEvent) => this.isRunTerminal(e), runId, telemetryFanout: this.telemetryFanout, }); return new Response(ctrl.stream, { headers: SSE_HEADERS }); } catch (executeError) { await ctrl.stream.cancel(); throw executeError; } } async getRun(_: Request, params: ServerHttpRouteParams): Promise { try { const state = await this.queryBus.execute(new GetRunStateQuery(params.runId!)); if (!state) { return Response.json({ error: "Unknown runId" }, { status: 404 }); } return Response.json(state); } catch (error) { return ServerHttpErrorResponseFactory.fromUnknown(error); } } async getRunDetail(_: Request, params: ServerHttpRouteParams): Promise { try { const detail = await this.queryBus.execute(new GetWorkflowRunDetailQuery(params.runId!)); if (!detail) { return Response.json({ error: "Unknown runId" }, { status: 404 }); } return Response.json(detail); } catch (error) { return ServerHttpErrorResponseFactory.fromUnknown(error); } } async runOrStream(body: CreateRunRequest, acceptsSse: boolean): Promise { if (!acceptsSse) { return Response.json(await this.commandBus.execute(new StartWorkflowRunCommand(body))); } return this.streamRunResult( async () => (await this.commandBus.execute(new StartWorkflowRunCommand(body))) as RunCommandResult, ); } async postRuns(request: Request, _: ServerHttpRouteParams): Promise { try { const body = await HttpRequestJsonBodyReader.readJsonBody(request); return await this.runOrStream(body, this.acceptsSse(request)); } catch (error) { return ServerHttpErrorResponseFactory.fromUnknown(error); } } async patchRunWorkflowSnapshot(request: Request, params: ServerHttpRouteParams): Promise { try { const body = await HttpRequestJsonBodyReader.readJsonBody(request); return Response.json( await this.commandBus.execute(new ReplaceMutableRunWorkflowSnapshotCommand(params.runId!, body)), ); } catch (error) { return ServerHttpErrorResponseFactory.fromUnknown(error); } } async patchRunNodePin(request: Request, params: ServerHttpRouteParams): Promise { try { const body = await HttpRequestJsonBodyReader.readJsonBody(request); return Response.json( await this.commandBus.execute(new SetPinnedNodeInputCommand(params.runId!, params.nodeId!, body)), ); } catch (error) { return ServerHttpErrorResponseFactory.fromUnknown(error); } } async postRunNode(request: Request, params: ServerHttpRouteParams): Promise { try { const body = await HttpRequestJsonBodyReader.readJsonBody(request); if (!this.acceptsSse(request)) { return Response.json( await this.commandBus.execute(new ReplayWorkflowNodeCommand(params.runId!, params.nodeId!, body)), ); } return await this.streamRunResult( async () => (await this.commandBus.execute( new ReplayWorkflowNodeCommand(params.runId!, params.nodeId!, body), )) as RunCommandResult, ); } catch (error) { return ServerHttpErrorResponseFactory.fromUnknown(error); } } }