import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import { FileSystem } from "effect/FileSystem"; import type { Path } from "effect/Path"; import type { Stdio } from "effect/Stdio"; import type { Terminal } from "effect/Terminal"; import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner"; import type { HttpEffect } from "../Http.ts"; import * as Http from "../Http.ts"; import * as Output from "../Output.ts"; import { packEnvValue, unpackEnvValue, type BaseRuntimeContext, } from "../RuntimeContext.ts"; export type ProcessServices = | ChildProcessSpawner | FileSystem | Path | Stdio | Terminal; export interface ProcessContext extends BaseRuntimeContext { run: ( effect: Effect.Effect, ) => Effect.Effect; } /** * Long-running host loop registration (`run`). Provided by `Platform` when the * execution context implements {@link ProcessContext} (i.e. carries `run`). * * `Platform` wires this automatically for every host runtime context that * implements `run` (EC2 instances, ECS tasks, processes), so an inline program * can `yield* ServerHost` and call `host.run(...)` during plan/deploy without * the caller providing the layer itself. */ export class ServerHost extends Context.Service< ServerHost, Pick >()("Alchemy::ServerHost") {} /** * Deploy-time / plan-time host context for platforms that bundle a long-lived * program. It collects background work registered via `run` and HTTP handlers * registered via `serve` into a single `exports.program` effect that the * generated container/instance entrypoint runs. */ export interface HostRuntimeContext extends ProcessContext { serve: ( handler: HttpEffect | Effect.Effect>, options?: { shape?: Record }, ) => Effect.Effect; exports: Effect.Effect<{ readonly program: Effect.Effect; }>; } /** * Build a {@link HostRuntimeContext} for a hosted platform of the given * resource `type`. Both `run` (background loops) and `serve` (HTTP handlers) * append to a single list of runners; `exports.program` runs them all * concurrently. This is the shared host context used by `AWS.EC2.Instance` and * `AWS.ECS.Task`. */ export const createHostRuntimeContext = (type: string) => (id: string): HostRuntimeContext => { const runners: Effect.Effect[] = []; const env: Record = {}; return { Type: type, id, env, set: (bindingId: string, output: Output.Output) => Effect.sync(() => { const key = bindingId.replaceAll(/[^a-zA-Z0-9]/g, "_"); // `packEnvValue` marker-packs Redacted values so they survive the // Output → env round-trip. env[key] = output.pipe(Output.map(packEnvValue)); return key; }), get: (key: string) => // Read straight from `process.env` — see `unpackEnvValue` for why // this must never resolve through `Config.string`. Effect.sync(() => unpackEnvValue(process.env[key]) as T), run: (effect: Effect.Effect) => Effect.sync(() => { runners.push(effect); }), serve: ((handler) => Effect.sync(() => { // Register the HTTP handler as a runner. At container runtime the // ambient `HttpServer` (if provided) serves it; `Http.serve` is a // no-op when no server is bound, so this never crashes plan/deploy. runners.push(Http.serve(handler as HttpEffect)); })) as HostRuntimeContext["serve"], exports: Effect.sync(() => ({ program: Effect.all(runners, { concurrency: "unbounded" }), })), } satisfies HostRuntimeContext; }; /** * Host runtime context for container platforms (`AWS.ECS.Task`, * `AWS.ECS.Service`, `Docker.Service`): extends the shared process host * context so an impl shape's `run` effect is registered as a one-shot runner * (the container exits when it completes) and the HTTP server only boots when * the impl actually declares a `fetch` handler. */ export const createContainerRuntimeContext = (type: string) => (id: string): HostRuntimeContext => { const base = createHostRuntimeContext(type)(id); // Capture the host serve BEFORE Object.assign overwrites `base.serve` // with the wrapper below — calling `base.serve` inside the wrapper would // resolve to the wrapper itself (property lookup happens at call time) // and recurse without bound the moment an impl declares `fetch`. const serveBase = base.serve; const serve: HostRuntimeContext["serve"] = (handler, options) => Effect.gen(function* () { const shape = options?.shape; const run = shape?.run; if (Effect.isEffect(run)) { yield* base.run(run as Effect.Effect); } // Boot the HTTP server only for an impl that declared `fetch` — a // pure one-shot `{ run }` program must exit when `run` completes // rather than parking behind the 404 fallback server forever. if (shape === undefined || shape.fetch !== undefined) { yield* serveBase(handler, options); } }) as Effect.Effect; return Object.assign(base, { serve }); };