import * as Effect from "effect/Effect";
import type { FileSystem } from "effect/FileSystem";
import * as Layer from "effect/Layer";
import type { Path } from "effect/Path";
import { defaultTeardown, type Teardown } from "effect/Runtime";
import type { Stdio } from "effect/Stdio";
import type { Terminal } from "effect/Terminal";
import type { HttpServer } from "effect/unstable/http/HttpServer";
import type { ServeError } from "effect/unstable/http/HttpServerError";
import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
import type { WebSocketConstructor } from "effect/unstable/socket/Socket";
import { disableCrossSpawnChdir } from "./Node.ts";
const isBun = typeof Bun !== "undefined";
const importPlatformPeer = (
peerDependency: "@effect/platform-bun" | "@effect/platform-node",
load: () => Promise,
): Promise =>
load().catch(() => {
console.error(
[
`Alchemy could not load the required peer dependency "${peerDependency}".`,
`Install a compatible version of "${peerDependency}" in the same project as "alchemy".`,
"If it is already installed, you may be running a globally installed Alchemy CLI; run the project-local CLI instead.",
].join("\n"),
);
process.exit(1);
});
/**
* Constructs a layer with different implementations for Bun and Node.
*/
export const platformLayer = (constructors: {
bun: () => Promise>;
node: () => Promise>;
}) =>
Layer.unwrap(
Effect.promise(async () => {
if (isBun) {
return await constructors.bun();
} else {
return await constructors.node();
}
}),
);
export type PlatformServices =
| ChildProcessSpawner
| FileSystem
| Path
| Stdio
| Terminal
// WebSocketConstructor is not included in NodeServices/BunServices, but required for Workers tail
| WebSocketConstructor;
export const PlatformServices: Layer.Layer = platformLayer({
bun: async () => {
const [BunServices, BunSocket] = await Promise.all([
importPlatformPeer(
"@effect/platform-bun",
() => import("@effect/platform-bun/BunServices"),
),
importPlatformPeer(
"@effect/platform-bun",
() => import("@effect/platform-bun/BunSocket"),
),
]);
return Layer.merge(BunServices.layer, BunSocket.layerWebSocketConstructor);
},
node: async () => {
const [NodeServices, NodeSocket] = await Promise.all([
importPlatformPeer(
"@effect/platform-node",
() => import("@effect/platform-node/NodeServices"),
),
importPlatformPeer(
"@effect/platform-node",
() => import("@effect/platform-node/NodeSocket"),
),
]);
return Layer.merge(
NodeServices.layer,
NodeSocket.layerWebSocketConstructor,
);
},
});
/**
* ALWAYS exit once the main effect completes. The platform `runMain` default
* only force-exits on failure or signal interruption — a *successful*
* `alchemy deploy`/`destroy` otherwise lingers for ~100 seconds after "Done"
* while keep-alive sockets (Cloudflare API agents, provider sidecar handles)
* pin the event loop until the remote side closes them. All Effect finalizers
* have already run by the time teardown is invoked; the macrotask hop lets
* any buffered stdout drain before the process exits.
*/
const exitingTeardown: Teardown = (exit, onExit) => {
defaultTeardown(exit, (code) => {
const finalCode = code !== 0 ? code : Number(process.exitCode ?? 0) || 0;
setTimeout(() => process.exit(finalCode), 0);
onExit(finalCode);
});
};
export const runMain = (
effect: Effect.Effect,
options?: {
readonly disableErrorReporting?: boolean | undefined;
readonly teardown?: Teardown | undefined;
},
): void => {
// Every alchemy process (CLI, exec child, sidecar, build/dev runners)
// runs concurrent effects that depend on a stable working directory —
// forbid cross-spawn's transient process-wide chdir (see Util/Node.ts).
disableCrossSpawnChdir();
const opts = {
...options,
teardown: options?.teardown ?? exitingTeardown,
};
if (isBun) {
void importPlatformPeer(
"@effect/platform-bun",
() => import("@effect/platform-bun/BunRuntime"),
).then((BunRuntime) => BunRuntime.runMain(effect, opts));
} else {
void importPlatformPeer(
"@effect/platform-node",
() => import("@effect/platform-node/NodeRuntime"),
).then((NodeRuntime) => NodeRuntime.runMain(effect, opts));
}
};
export const httpServer = (
port: number = 0,
host: string = "127.0.0.1",
): Layer.Layer =>
platformLayer({
bun: async () => {
const BunHttpServer = await importPlatformPeer(
"@effect/platform-bun",
() => import("@effect/platform-bun/BunHttpServer"),
);
// `idleTimeout: 0` disables Bun's default 10s request idle timeout.
// The RPC spawner serves a long-lived `/logs` stream (sidecar output
// forwarded to the exec child's renderer) that can legitimately sit
// idle between lines; Bun would otherwise sever it mid-session.
return BunHttpServer.layer({ hostname: host, port, idleTimeout: 0 });
},
node: async () => {
const [NodeHttpServer, Http] = await Promise.all([
importPlatformPeer(
"@effect/platform-node",
() => import("@effect/platform-node/NodeHttpServer"),
),
import("node:http"),
]);
return NodeHttpServer.layerServer(Http.createServer, { host, port });
},
});