1/**
2 * Process bootstrap core for `Cloudflare.Container` images. The runtime
3 * (`bun` | `node`) is chosen at deploy time, and each is an optional peer,
4 * so the platform-specific layers are supplied by the thin entry modules
5 * {@link ./CloudflareContainerBun.ts} / {@link ./CloudflareContainerNode.ts}
6 * rather than imported here. The generated entry imports one of those and
7 * the user's `main`, nothing else — see {@link ./Process.ts} for why.
8 */
9import * as ConfigProvider from "effect/ConfigProvider";
10import * as Effect from "effect/Effect";
11import * as Layer from "effect/Layer";
12import * as Logger from "effect/Logger";
13import { MinimumLogLevel } from "effect/References";
14import * as FetchHttpClient from "effect/http/FetchHttpClient";
15import { CloudflareEnvironment } from "../../Cloudflare/CloudflareEnvironment.ts";
16import { reifyBoundConfigProvider } from "../../Runtime.ts";
17import {
18 entrypointLayer,
19 resolveProgram,
20 runProcess,
21 stackConstant,
22} from "./Process.ts";
23
24export interface ContainerBootstrapOptions {
25 /** Stack identity baked in at deploy time. */
26 readonly stack: { readonly name: string; readonly stage: string };
27}
28
29export interface ContainerRuntime {
30 /** `BunServices.layer` / `NodeServices.layer`. */
31 readonly services: Layer.Layer<any>;
32 /** `BunHttpServer()` / `NodeHttpServer()`. */
33 readonly httpServer: Layer.Layer<any, any, any>;
34}
35
36/** Serve the bundled container program on the runtime's HTTP server. */
37export const bootstrapContainer = (
38 runtime: ContainerRuntime,
39 entrypoint: unknown,
40 options: ContainerBootstrapOptions,
41): Promise<void> => {
42 const platform = Layer.mergeAll(
43 runtime.services,
44 FetchHttpClient.layer,
45 // TODO(sam): wire this up to telemetry more directly
46 Logger.layer([Logger.consolePretty()]),
47 );
48
49 const program = resolveProgram("default", { telemetry: true }).pipe(
50 Effect.provide(
51 entrypointLayer(entrypoint).pipe(
52 Layer.provideMerge(
53 stackConstant(options.stack.name, options.stack.stage),
54 ),
55 Layer.provideMerge(runtime.httpServer),
56 // Capability bindings that talk to Cloudflare's HTTP API from inside
57 // the container (e.g. R2/KV/Queue `*Http` bindings) resolve their
58 // account via `CloudflareEnvironment` at runtime, exactly like the
59 // Worker bridge does (the service value is an `Effect` of the
60 // resolved credentials). The per-operation account/token are read
61 // from the container's env (the bound token outputs), so an absent
62 // account id here is harmless.
63 Layer.provideMerge(
64 Layer.succeed(
65 CloudflareEnvironment,
66 Effect.succeed({
67 account: process.env.ALCHEMY_CLOUDFLARE_ACCOUNT_ID,
68 }) as any,
69 ),
70 ),
71 Layer.provideMerge(platform),
72 Layer.provideMerge(
73 Layer.succeed(
74 ConfigProvider.ConfigProvider,
75 // Auto-bound `Config` values arrive in the env as
76 // `{"_tag":"Redacted","value":...}` markers; reify them so a
77 // `Config` re-read inside a handler decodes the raw source value.
78 reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env),
79 ),
80 ),
81 Layer.provideMerge(
82 Layer.succeed(MinimumLogLevel, process.env.DEBUG ? "Debug" : "Info"),
83 ),
84 ),
85 ),
86 Effect.scoped,
87 );
88
89 return runProcess("Container", program);
90};