1import * as Cloudflare from "alchemy/Cloudflare";
2import { Stack } from "alchemy/Stack";
3import * as Effect from "effect/Effect";
4import type { PlatformError } from "effect/PlatformError";
5import * as Stream from "effect/Stream";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import * as ChildProcess from "effect/process/ChildProcess";
9import { ChildProcessSpawner } from "effect/process/ChildProcessSpawner";
10
11export class Sandbox extends Cloudflare.Container<
12 Sandbox,
13 {
14 /**
15 * Execute a command in a sandbox.
16 */
17 exec: (command: string) => Effect.Effect<
18 {
19 exitCode: number;
20 stdout: string;
21 stderr: string;
22 },
23 PlatformError
24 >;
25 }
26>()("Sandbox") {}
27
28export const SandboxLive = /* @__PURE__ */ Sandbox.make(
29 Stack.useSync((stack) => ({
30 main: import.meta.url,
31 instanceType: stack.stage === "prod" ? "standard-1" : "dev",
32 observability: {
33 logs: {
34 enabled: true,
35 },
36 },
37 })),
38 Effect.gen(function* () {
39 //
40 const cp = yield* ChildProcessSpawner;
41
42 let counter = 0;
43
44 return Sandbox.of({
45 exec: (command) =>
46 cp
47 .spawn(
48 ChildProcess.make(command, {
49 shell: true,
50 }),
51 )
52 .pipe(
53 Effect.flatMap((handle) =>
54 Effect.all(
55 [
56 handle.exitCode,
57 handle.stdout.pipe(Stream.decodeText, Stream.mkString),
58 handle.stderr.pipe(Stream.decodeText, Stream.mkString),
59 ],
60 { concurrency: "unbounded" },
61 ),
62 ),
63 Effect.map(([exitCode, stdout, stderr]) => ({
64 exitCode,
65 stdout,
66 stderr,
67 })),
68 Effect.scoped,
69 ),
70 fetch: Effect.gen(function* () {
71 const request = yield* HttpServerRequest;
72 const url = new URL(request.url, "http://localhost");
73
74 if (url.pathname === "/increment") {
75 counter++;
76 return yield* HttpServerResponse.json({ counter });
77 }
78
79 return HttpServerResponse.text("Hello from Sandbox container!");
80 }),
81 });
82 }),
83);
84
85export default SandboxLive;