| 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 | */ |
| 22 | import * as Config from "effect/Config"; |
| 23 | import * as Context from "effect/Context"; |
| 24 | import * as Effect from "effect/Effect"; |
| 25 | import * as Layer from "effect/Layer"; |
| 26 | import { makeEntrypointLayer } from "../../Runtime.ts"; |
| 27 | import { Self } from "../../Self.ts"; |
| 28 | import { Stack } from "../../Stack.ts"; |
| 29 | import { provideProcessTelemetry } from "../../Telemetry.ts"; |
| 30 | import { 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 | */ |
| 37 | export 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 | */ |
| 45 | export const entrypointLayer = (entrypoint: unknown): Layer.Layer<any> => |
| 46 | makeEntrypointLayer(entrypointTag, entrypoint); |
| 47 | |
| 48 | /** `Stack` from the `ALCHEMY_STACK_NAME` / `ALCHEMY_STAGE` the host injects. */ |
| 49 | export 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. */ |
| 67 | export 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 | */ |
| 86 | export 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 | */ |
| 107 | export 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 | }; |