import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { AlchemyContext } from "../AlchemyContext.ts"; import { Platform, type Main, type PlatformProps } from "../Platform.ts"; import * as Provider from "../Provider.ts"; import type { Resource } from "../Resource.ts"; import type { ServerHost } from "../Server/Process.ts"; import { Stack } from "../Stack.ts"; import { App } from "./App.ts"; import type { MachineGuest, MachineImageRef, MachineService } from "./Machine.ts"; import type { MountedDisk, ServiceBinding } from "./MountVolume.ts"; import type { Providers } from "./Providers.ts"; import { type FlyBuildOptions, type FlyHostRuntimeContext } from "./hosted.ts"; import { type Replica } from "./replicas.ts"; /** * A resource-valued prop: the resource itself, or an Effect that produces * it (so `yield* App(...)` and `App(...)` both type-check). */ type Ref = T | Effect.Effect; export interface ServiceProps extends PlatformProps { /** * Parent Fly App. Accepts a `Fly.App` or an Effect that produces one * (module-scope `const Site = Fly.App("Site")` is valid). Changing the * App replaces the Service. */ app: Ref; /** * Module entrypoint bundled with rolldown and baked into a Docker * image pushed to `registry.fly.io`. Typically `import.meta.url`. * A content-hash change updates the Machine in place. */ main: string; /** * Region to start the Machine in (`iad`, `ewr`, `ord`, …). Changing * it replaces the Service. * * @default "iad" */ region?: string; /** * Number of Machines to keep running. Fly's proxy load-balances * `{app}.fly.dev` across them. Each replica gets its own Volume * from every `MountVolume` binding. * * @default 1 */ count?: number; /** * Guest size. Defaults to shared-cpu-1x 256 MB. */ guest?: MachineGuest; /** * Port the hosted HTTP server listens on. Written to `PORT` and used * as the Fly proxy `internal_port`. * * @default 3000 */ port?: number; /** * Named export to load from `main`. * * @default "default" */ handler?: string; /** * Additional environment variables for the hosted process. Merged * after binding-injected `env`. */ env?: Record; /** * Bundler configuration for `main`: rolldown `input`/`output` * overrides, pure-annotation options (`pure`), and `install` for * packages that must ship as real `node_modules` (see {@link FlyBuildOptions}). */ build?: FlyBuildOptions; /** * Environment image used as the generated Dockerfile's `FROM`. Must * be able to run the bun runtime. * * @default "oven/bun:1" */ image?: string; /** * Fly proxy services. Defaults to HTTP 80 + HTTPS 443 → {@link port}. */ services?: MachineService[]; /** * Machine name. Unique per App. If omitted, a unique name is generated * from the stack, stage and logical ID. Changing it replaces the Service. */ name?: string; } export type Service = Resource<"Fly.Service", ServiceProps, { /** Parent Fly App name. */ appName: string; /** Fly Machine id of replica 0. */ machineId: string; /** Fly Machine ids of every replica. */ machineIds: string[]; /** Machine name of replica 0 (unique per App). */ name: string; /** Region the Machines are running in. */ region: string; /** Observed state of replica 0 (`created`, `started`, `stopped`, …). */ state: string; /** * Public `https://{appName}.fly.dev` URL when a proxy service is * configured. */ url: string | undefined; /** Parsed image reference from Fly. */ imageRef: MachineImageRef | undefined; /** Number of Machines in the replica set. */ count: number; /** Disks mounted on replica 0. */ mounts: MountedDisk[]; /** Every replica in the set. */ replicas: Replica[]; /** Content hash of the bundled program's image. */ code: { hash: string; }; }, ServiceBinding, Providers>; export declare const isService: (value: unknown) => value is Service; export type ServiceServices = ServerHost; export type ServiceShape = Main; export type ServiceRuntimeContext = FlyHostRuntimeContext; /** * A Service is an Effect program running in a Fly.io Machine. Set * `count` to scale it up or down. Several Services share one {@link App}. * * @see https://fly.io/docs/machines/api/machines-resource/ * * ### Declare a Service * A Service is a class. Props describe the Machine. The Effect is the * program that runs on it. * * `app` is the parent {@link App}. Pass the declaration directly, * yielded or module-scope. `main: import.meta.url` is the bundle * entrypoint. Alchemy bundles this file with Rolldown, builds a * Docker image (default `oven/bun:1`), and pushes it to * `registry.fly.io/{app}:{id}-{hash}`. * * **Example:** Class + App + main * ```typescript * // src/api.ts * import * as Fly from "alchemy/Fly"; * import * as Effect from "effect/Effect"; * import { Site } from "./app.ts"; * * export default class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url }, * Effect.gen(function* () { * return {}; * }), * ) {} * ``` * * :::caution[Changing `app` replaces the Service] * The new App gets a new replica set. The old Machines are deleted. * ::: * * ### Serve HTTP with fetch * Return `fetch` from the init Effect to boot an HTTP server. Omit * `fetch` for a background service. * * **Example:** Hello * ```typescript * export default class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * ``` * * ### Pin a region * Fly Machines live in a region. Default is `iad`. See * [Regions](/fly/compute/regions) for the list of codes. * * **Example:** Region * ```typescript * export default class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url, region: "iad" }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * ``` * * :::caution[Changing `region` replaces the Service] * The replica set is created in the new region. The old Machines are * deleted. * ::: * * ### Set the port * `port` is the port the process listens on inside the Machine. * Alchemy writes it to `PORT`. Default is `3000`. * * **Example:** Port 3000 * ```typescript * export default class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url, region: "iad", port: 3000 }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * ``` * * ### The public URL * Yield the Service in the Stack. `api.url` is * `https://{appName}.fly.dev`. Alchemy does not create this hostname. * It is the parent {@link App}'s fly.dev name. The Service does not * get its own URL. * * **Example:** Stack output * ```typescript * export default Alchemy.Stack( * "MyApp", * { providers: Fly.providers(), state: Alchemy.localState() }, * Effect.gen(function* () { * const api = yield* Api; * return { url: api.url }; * }), * ); * ``` * * `url` is `undefined` when you pass `services: []` (nothing is * published). * * :::note[One fly.dev hostname per App] * Every published Service on the App shares `{appName}.fly.dev`. Put * one public Service on an App. Use `services: []` for workers. * ::: * * ### Fly's proxy is the load balancer * There is no LoadBalancer resource. Fly runs an Anycast proxy at * the edge. * * **Example:** Published ports * ```typescript * export default class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url, region: "iad", port: 3000 }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * ``` * * Unless you override `services`, Alchemy publishes HTTP 80 and * HTTPS 443 on that proxy and points them at `port` inside each * Machine (`internal_port`). A request to * `https://{appName}.fly.dev` lands on Fly's edge. Fly terminates * TLS on 443, picks one started Machine that published this service, * and forwards to `port` where `fetch` runs. * * ### An address so it answers * `{app}.fly.dev` does not answer over IPv4 until the App has an * {@link IpAssignment}. Allocate a shared Anycast IPv4 on the same * App and yield it next to the Service. * * **Example:** shared_v4 * ```typescript * export const PublicIp = Fly.IpAssignment("Shared", { * app: Site, * type: "shared_v4", * }); * ``` * * ```typescript * Effect.gen(function* () { * const api = yield* Api; * const ip = yield* PublicIp; * return { url: api.url, ip: ip.ip }; * }); * ``` * * ### Scale with count * `count` is how many Machines to keep running. Default is `1`. They * all publish the same proxy service, so they all sit behind * `{app}.fly.dev`. Fly's proxy picks one Machine per request. Each * replica gets its own Volume from every {@link MountVolume} binding. * * **Example:** Three replicas * ```typescript * export default class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url, region: "iad", count: 3, port: 3000 }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * ``` * * ### Config * Yield `Config` in init. Alchemy reads the value from the env of * whoever deploys and writes it onto the Machine. Do not pass * `env: { ... }` on a Service. * * `Config.redacted("API_KEY")` is `Redacted`. Unwrap with * `Redacted.value` only where you need the raw string. * * Alchemy also injects `PORT` (when `port` is set) and stack metadata. * For a secret Fly should own and inject into every Machine on the * App, use {@link Secret}. * * **Example:** Config.redacted * ```typescript * import * as Config from "effect/Config"; * import * as Redacted from "effect/Redacted"; * * export default class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url, port: 3000 }, * Effect.gen(function* () { * const apiKey = yield* Config.redacted("API_KEY"); * * return { * fetch: Effect.gen(function* () { * const token = Redacted.value(apiKey); * return HttpServerResponse.text("ok"); * }), * }; * }), * ) {} * ``` * * ### Mount a disk * Bind {@link MountVolume} inside init. App and region come from the * Service. `count: 3` creates three Volumes, one per replica. Provide * {@link MountVolumeLive}. * * **Example:** Per-replica disk * ```typescript * export default class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url, region: "iad", count: 3, port: 3000 }, * Effect.gen(function* () { * const disk = yield* Fly.MountVolume({ path: "/data", sizeGb: 1 }); * const fs = yield* FileSystem.FileSystem; * return { * fetch: Effect.gen(function* () { * const text = yield* fs.readFileString(`${disk.path}/hello.txt`); * return HttpServerResponse.text(text); * }), * }; * }).pipe(Effect.provide(Fly.MountVolumeLive)), * ) {} * ``` * * ### Guest size * `guest` is CPU kind, CPU count, and memory. Default is shared-cpu, * 1 CPU, 256 MB. Set `gpuKind` and `gpus` for a GPU. Guest updates in * place. * * **Example:** Bigger guest * ```typescript * export default class Api extends Fly.Service()( * "Api", * { * app: Site, * main: import.meta.url, * region: "iad", * port: 3000, * guest: { cpuKind: "shared", cpus: 2, memoryMb: 512 }, * }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * ``` * * ### A stable name * Machine names are unique per App. Omit `name` and Alchemy generates * one from the stack, stage, and logical ID. * * **Example:** Explicit name * ```typescript * export default class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url, name: "api", port: 3000 }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * ``` * * :::caution[Changing `name` replaces the Service] * Fly cannot rename a Machine. Alchemy creates the new name, then * deletes the old replica set. * ::: * * ### Named export * `handler` is the named export to load from `main`. Default is * `"default"`. * * **Example:** Custom handler * ```typescript * export default class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url, handler: "api", port: 3000 }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * ``` * * ### Base image * `image` is the generated Dockerfile's `FROM`. Default is * `oven/bun:1`. It must still run bun. A content-hash change of * `main` updates the Machine in place. * * **Example:** Override FROM * ```typescript * export default class Api extends Fly.Service()( * "Api", * { * app: Site, * main: import.meta.url, * image: "oven/bun:1.2", * port: 3000, * }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * ``` * * ### Custom proxy services * `services` defaults to HTTP 80 + HTTPS 443 toward `port`. Pass a * custom list to change handlers or autostop. Pass `[]` so Fly does * not publish a proxy. * * **Example:** Unpublished process * ```typescript * export default class Worker extends Fly.Service()( * "Worker", * { app: Site, main: import.meta.url, region: "iad", services: [] }, * Effect.gen(function* () { * return {}; * }), * ) {} * ``` * * ### Background services * Omit `port` and `fetch`. Pass `services: []`. Use `ServerHost.run` * for a long-running loop. If the process exits, Fly restarts it. * * **Example:** ServerHost.run * ```typescript * import { ServerHost } from "alchemy/Server"; * * export default class Worker extends Fly.Service()( * "Worker", * { app: Site, main: import.meta.url, region: "iad", services: [] }, * Effect.gen(function* () { * const host = yield* ServerHost; * * yield* host.run( * Effect.gen(function* () { * return yield* Effect.never; * }).pipe(Effect.orDie), * ); * }), * ) {} * ``` * * ### Bundle config * `build` is Rolldown `input` / `output` overrides plus * pure-annotation options. Use it when `main` needs extra entry * points or externals. * * **Example:** Externals * ```typescript * export default class Api extends Fly.Service()( * "Api", * { * app: Site, * main: import.meta.url, * port: 3000, * build: { input: { external: ["sharp"] } }, * }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * ``` * * **Example:** Install `pg` unbundled * `pg` is CommonJS. Rolldown's interop turns `Client` into a namespace. * Install it into the image so `@effect/sql-pg` / `Drizzle.Postgres` load * it with Node's CJS semantics — same `build.install` as Lambda. * ```typescript * export default class Api extends Fly.Service()( * "Api", * { * app: Site, * main: import.meta.url, * port: 3000, * build: { install: ["pg"] }, * }, * Effect.gen(function* () { * const conn = yield* Fly.ConnectPostgres(Db); * const db = yield* Drizzle.Postgres(conn.connectionString); * return { * fetch: Effect.gen(function* () { * const rows = yield* db.execute("select 1 as ok"); * return HttpServerResponse.json({ rows }); * }), * }; * }).pipe(Effect.provide(Fly.ConnectPostgresHttp)), * ) {} * ``` * * ### Multiple Services, one App * Each Service has its own Machines, image, and lifecycle. Point * several at the same `app`. * * **Example:** API and worker * ```typescript * class Api extends Fly.Service()( * "Api", * { app: Site, main: import.meta.url, port: 3000 }, * Effect.gen(function* () { * return { * fetch: Effect.succeed(HttpServerResponse.text("hello")), * }; * }), * ) {} * * class Worker extends Fly.Service()( * "Worker", * { app: Site, main: import.meta.url, services: [] }, * Effect.gen(function* () { * return {}; * }), * ) {} * ``` * * @resource */ export declare const Service: Platform; declare const ServiceNotCreated_base: new = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & { readonly _tag: "Fly.ServiceNotCreated"; } & Readonly; export declare class ServiceNotCreated extends ServiceNotCreated_base<{ name: string; appName: string; }> { } declare const ServiceAppNotResolved_base: new = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & { readonly _tag: "Fly.ServiceAppNotResolved"; } & Readonly; export declare class ServiceAppNotResolved extends ServiceAppNotResolved_base<{ message: string; }> { } export declare const ServiceProvider: () => Layer.Layer, never, AlchemyContext | import("effect/unstable/process/ChildProcessSpawner").ChildProcessSpawner | import("effect/FileSystem").FileSystem | import("effect/Path").Path | Stack | import("../Stage.ts").Stage | import("@distilled.cloud/fly-io").FlyIoOpContext>; export {}; //# sourceMappingURL=Service.d.ts.map