1import * as Cloudflare from "@/Cloudflare";
2import * as Alchemy from "@/index.ts";
3import type { RuntimeContext } from "@/RuntimeContext.ts";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import { ProbeEnv, ProbeEnvBinding } from "./env-binding.ts";
9import { Storage } from "./storage.ts";
10
11export class MyContainer extends Cloudflare.Container<
12 MyContainer,
13 {
14 ping: () => Effect.Effect<string>;
15 /** The value a `Binding.Service` injected into the container's env. */
16 boundEnv: () => Effect.Effect<string | undefined, never, RuntimeContext>;
17 /** Read an object's text body from R2 (or `null` when absent). */
18 readObject: (
19 key: string,
20 ) => Effect.Effect<string | null, never, RuntimeContext>;
21 }
22>()("EffectfulContainer") {}
23
24export default MyContainer.make(
25 {
26 main: import.meta.url,
27 image: "oven/bun:latest",
28 },
29 Effect.gen(function* () {
30 // The container reads R2 over a scoped HTTP API token (not the native
31 // Worker binding) — this proves the HTTP R2 client works from inside a
32 // container process. `Alchemy.remote()` pins the bucket (and the minted
33 // token) live even under `alchemy dev`: the HTTP client talks to real
34 // Cloudflare, which a `dev:` local bucket can't serve. This is the
35 // FIRST registration site (the container layer builds before the stack
36 // body), so the DO's later undecorated native-binding site inherits the
37 // pin and both paths converge on the same live bucket.
38 const bucket = yield* Cloudflare.R2.ReadWriteBucket(Storage).pipe(
39 Alchemy.remote(),
40 );
41
42 // A capability whose only deploy-time contribution is an env var. A
43 // container has no workerd bindings, so this is the channel every
44 // env-shaped capability (Prisma.Connect, …) reaches it through.
45 const boundEnv = yield* ProbeEnv(Storage);
46
47 const read = (key: string) =>
48 bucket.get(key).pipe(
49 Effect.flatMap((object) =>
50 object ? object.text() : Effect.succeed(null),
51 ),
52 Effect.orDie,
53 );
54
55 return {
56 ping: () => Effect.succeed("pong"),
57 boundEnv: () => boundEnv,
58 readObject: read,
59 fetch: Effect.gen(function* () {
60 const request = yield* HttpServerRequest;
61 const url = new URL(request.url, "http://container");
62 if (url.pathname === "/bound-env") {
63 return yield* HttpServerResponse.json({ value: yield* boundEnv });
64 }
65 if (url.pathname === "/health") {
66 return yield* HttpServerResponse.json({ ok: true });
67 }
68 if (url.pathname === "/object") {
69 const key = url.searchParams.get("key") ?? "";
70 const value = yield* read(key);
71 return yield* HttpServerResponse.json({ value });
72 }
73 return HttpServerResponse.text("hello from effectful container");
74 }),
75 };
76 }).pipe(
77 Effect.provide(
78 Layer.mergeAll(Cloudflare.R2.ReadWriteBucketHttp, ProbeEnvBinding),
79 ),
80 ),
81);