1import * as Effect from "effect/Effect";
2import * as FileSystem from "effect/FileSystem";
3import * as Path from "effect/Path";
4import * as Schedule from "effect/Schedule";
5import * as Layer from "effect/Layer";
6import * as HttpServer from "effect/http/HttpServer";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import * as ChildProcess from "effect/process/ChildProcess";
9import {
10 PlatformServices,
11 runMain,
12 httpServer,
13} from "alchemy/Util/PlatformServices";
14
15const mode = process.env.LIFECYCLE_MODE ?? "cooperative";
16const leaf = process.env.LIFECYCLE_LEAF === "1";
17const directory = process.env.LIFECYCLE_DIR!;
18
19const program = Effect.gen(function* () {
20 const fs = yield* FileSystem.FileSystem;
21 const path = yield* Path.Path;
22 const prefix = path.join(directory, leaf ? "leaf" : "wrapper");
23 if (mode !== "cooperative") {
24 yield* Effect.acquireRelease(
25 Effect.sync(() => {
26 const onTerm = () =>
27 Effect.runFork(
28 fs
29 .writeFileString(`${prefix}.term`, "TERM")
30 .pipe(
31 Effect.andThen(
32 mode === "early-exit" && !leaf
33 ? Effect.sync(() => process.exit(0))
34 : Effect.void,
35 ),
36 ),
37 );
38 process.on("SIGTERM", onTerm);
39 return onTerm;
40 }),
41 (listener) =>
42 Effect.sync(() => {
43 process.removeListener("SIGTERM", listener);
44 }),
45 );
46 }
47 if (leaf) {
48 yield* fs.writeFileString(`${prefix}.ready`, "ready");
49 } else {
50 const entry = yield* path.fromFileUrl(new URL(import.meta.url));
51 const child = yield* ChildProcess.make("bun", ["run", entry], {
52 detached: mode === "cooperative",
53 env: { LIFECYCLE_LEAF: "1" },
54 extendEnv: true,
55 stdout: "ignore",
56 stderr: "inherit",
57 forceKillAfter: "200 millis",
58 });
59 const pid = yield* Effect.sync(() => process.pid);
60 yield* fs.writeFileString(
61 path.join(directory, "pids.json"),
62 JSON.stringify({ wrapper: pid, leaf: child.pid }),
63 );
64 yield* fs.exists(path.join(directory, "leaf.ready")).pipe(
65 Effect.repeat({
66 schedule: Schedule.spaced("10 millis"),
67 until: Boolean,
68 times: 500,
69 }),
70 );
71 if (mode === "cooperative") {
72 yield* Effect.addFinalizer(() =>
73 Effect.gen(function* () {
74 yield* fs.writeFileString(`${prefix}.term`, "TERM");
75 yield* Effect.sleep("150 millis");
76 yield* child
77 .kill({ forceKillAfter: "200 millis" })
78 .pipe(Effect.ignore);
79 yield* fs.writeFileString(`${prefix}.clean`, "clean");
80 }).pipe(Effect.orDie),
81 );
82 }
83 yield* fs.writeFileString(path.join(directory, "wrapper.ready"), "ready");
84 const server = yield* HttpServer.HttpServer;
85 yield* server.serve(Effect.succeed(HttpServerResponse.text("ready")));
86 yield* fs.writeFileString(
87 path.join(directory, "url"),
88 HttpServer.formatAddress(server.address),
89 );
90 yield* Effect.sync(() =>
91 console.log(HttpServer.formatAddress(server.address)),
92 );
93 }
94 if (mode === "crash" && !leaf) {
95 yield* fs.exists(path.join(directory, "crash")).pipe(
96 Effect.repeat({
97 schedule: Schedule.spaced("10 millis"),
98 until: Boolean,
99 times: 1000,
100 }),
101 );
102 yield* Effect.sync(() => process.exit(2));
103 }
104 yield* Effect.never;
105});
106const runnable = program.pipe(
107 Effect.scoped,
108 Effect.provide(
109 Layer.provideMerge(
110 httpServer(0, "127.0.0.1", { gracefulShutdownTimeout: 0 }),
111 PlatformServices,
112 ),
113 ),
114);
115if (mode === "cooperative") runMain(runnable);
116else
117 void Effect.runPromise(runnable).catch((error) => {
118 console.error(error);
119 process.exitCode = 1;
120 });