1/**
2 * Shared core of the generated process bootstraps (ECS, App Runner, Batch,
3 * EC2, Cloudflare Containers, MicroVM, Docker, Fly, Hetzner, Prisma).
4 *
5 * Each bundler used to emit its bootstrap as an inline TypeScript string —
6 * a virtual module with no location on disk, whose imports mixed alchemy's
7 * own dependencies (`@distilled.cloud/*`, `@effect/platform-*`) with the
8 * consumer's (`alchemy`, `effect`). No directory can resolve both under an
9 * isolated install (bun `--linker=isolated`, pnpm), so the imports were
10 * left external and the process died at boot with `Cannot find module`.
11 *
12 * Now the generated entry is a three-line shim that imports ONLY
13 * `alchemy/Runtime/Bootstrap/<Platform>` (resolvable from any consumer —
14 * `alchemy` is its direct dependency) plus the user's `main`. Everything
15 * alchemy needs lives in these real modules, whose imports resolve by the
16 * ordinary rule: from the file that wrote them.
17 *
18 * The helpers here are the pieces every platform module composes; the
19 * layer ORDER (which service is provided to which) is platform-specific and
20 * stays in each module.
21 */
22import * as Config from "effect/Config";
23import * as Context from "effect/Context";
24import * as Effect from "effect/Effect";
25import * as Layer from "effect/Layer";
26import { makeEntrypointLayer } from "../../Runtime.ts";
27import { Self } from "../../Self.ts";
28import { Stack } from "../../Stack.ts";
29import { provideProcessTelemetry } from "../../Telemetry.ts";
30import { withManagedHttpShutdown } from "./ManagedHttpShutdown.ts";
31
32/**
33 * The tag every bundled platform program registers itself under
34 * (`Self`'s key). Typed loosely on purpose: the shape is the platform
35 * instance, which differs per platform and is only ever read dynamically.
36 */
37export const entrypointTag = Context.Service<any, any>(Self.key);
38
39/**
40 * Normalize the entrypoint export into the layer providing
41 * {@link entrypointTag}: an inline-effect class default export is an Effect
42 * resolving the platform instance, while the tagged form
43 * (`X.make(props, impl)`) exports a Layer providing the `Self` tag.
44 */
45export const entrypointLayer = (entrypoint: unknown): Layer.Layer<any> =>
46 makeEntrypointLayer(entrypointTag, entrypoint);
47
48/** `Stack` from the `ALCHEMY_STACK_NAME` / `ALCHEMY_STAGE` the host injects. */
49export const stackFromEnv: Layer.Layer<Stack, Config.ConfigError> =
50 Layer.effect(
51 Stack,
52 Effect.all([
53 Config.String("ALCHEMY_STACK_NAME"),
54 Config.String("ALCHEMY_STAGE"),
55 ]).pipe(
56 Effect.map(([name, stage]) => ({
57 name,
58 stage,
59 bindings: {},
60 resources: {},
61 actions: {},
62 })),
63 ),
64 );
65
66/** `Stack` from constants baked in at deploy time. */
67export const stackConstant = (
68 name: string,
69 stage: string,
70): Layer.Layer<Stack> =>
71 Layer.succeed(Stack, {
72 name,
73 stage,
74 bindings: {},
75 resources: {},
76 actions: {},
77 });
78
79/**
80 * Resolve the program the bundled platform registered under `exportKey`
81 * (`program` for the server/one-shot hosts, `default` for containers,
82 * `handler` for Lambda), optionally under process-lifetime telemetry: built
83 * once into the root scope; exporters batch on their intervals and flush
84 * when the scope closes on graceful shutdown.
85 */
86export const resolveProgram = (
87 exportKey: string,
88 options?: { readonly telemetry?: boolean },
89): Effect.Effect<any, any, any> =>
90 entrypointTag.pipe(
91 Effect.flatMap((self) => {
92 const program: Effect.Effect<any, any, any> =
93 self.RuntimeContext.exports.pipe(
94 Effect.flatMap((exports: any) => exports[exportKey]),
95 );
96 return options?.telemetry
97 ? program.pipe(provideProcessTelemetry(self.RuntimeContext))
98 : program;
99 }),
100 );
101
102/**
103 * Run `program` as the process entry: log the start, exit 1 on failure.
104 * With `exitOnComplete`, exit 0 explicitly once it completes (one-shot jobs
105 * whose host waits on the process, e.g. Batch).
106 */
107export const runProcess = (
108 label: string,
109 program: Effect.Effect<unknown, unknown>,
110 options?: {
111 readonly exitOnComplete?: boolean;
112 /**
113 * Enable process-wide SIGTERM/SIGINT handling and bounded cleanup for the
114 * managed Fly bootstrap. May force process exit; see {@link withManagedHttpShutdown}.
115 */
116 readonly managedHttpShutdownTimeoutMs?: number;
117 },
118): Promise<void> => {
119 console.log(`${label} bootstrap starting...`);
120 // Node 26 exits 13 (unsettled TLA) when the event loop is empty while
121 // `await bootstrap(...)` is still pending. `host.run(Effect.never)` has
122 // no native handle; an HTTP `listen` does. Hold a timer so run-only
123 // Services stay up the same way Bun does.
124 const keepAlive = setInterval(() => undefined, 1 << 30);
125 const managed = options?.managedHttpShutdownTimeoutMs;
126 const execution =
127 managed === undefined
128 ? program.pipe(Effect.as(false))
129 : withManagedHttpShutdown(program, managed);
130 return Effect.runPromise(execution).then(
131 (shutdown) => {
132 clearInterval(keepAlive);
133 if (shutdown || options?.exitOnComplete) {
134 console.log(`${label} completed.`);
135 process.exit(0);
136 }
137 },
138 (err) => {
139 clearInterval(keepAlive);
140 console.error(`${label} bootstrap failed:`, err);
141 process.exit(1);
142 },
143 );
144};