import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import type * as Redacted from "effect/Redacted"; import * as Scope from "effect/Scope"; import type * as SqlClient from "effect/unstable/sql/SqlClient"; import { State, type StateService } from "./State.ts"; export interface PostgresStateOptions { /** * An existing `@effect/sql` client. The caller owns its lifecycle — the * store only issues queries against it. Exactly one of `client` and `url` * must be provided. * * The client must be **pool-backed** and able to hand out at least two * concurrent connections: the store keeps one connection reserved for the * advisory lock and verifies that lock from another one. A * single-connection client (`PgClient.makeClient`) is refused at the first * lease check, and a pool capped at `maxConnections: 1` blocks forever * waiting for a free connection. */ client?: SqlClient.SqlClient; /** * Postgres connection URL, as a `Redacted` value or an Effect yielding one * — `Config.redacted("STATE_DATABASE_URL")` is itself an Effect, so it can * be passed directly. The store creates its own `@effect/sql-pg` pool from * the URL and closes that pool when the state layer is released. */ url?: Redacted.Redacted | Effect.Effect, E, R>; /** * Prefix for the advisory-lock key. The full key for a stack/stage is * `{lockKeyPrefix}:{stack}/{stage}`. * * @default "alchemy" */ lockKeyPrefix?: string; /** * State-store id reported in telemetry (`alchemy.state_store.id`). * * @default "postgres" */ id?: string; /** * How long (in milliseconds) a passing lease check is trusted before the * next state operation re-verifies the advisory lock. A lease lost inside * the window is detected within this many milliseconds rather than * instantly; in exchange, a burst of state operations does one lock * round-trip instead of one per operation. * * @default 5000 */ leaseCheckTtlMs?: number; } /** * State store backed by any Postgres database. * * Stack state lives in two tables — `alchemy_resource_state` and * `alchemy_stack_output` — created on first use with * `create table if not exists`. * * Concurrent deploys are serialized with a session-scoped Postgres advisory * lock per `(stack, stage)`: the lock is taken on a reserved connection * (`SqlClient.reserve`) when the first operation for the pair runs, and * contention fails immediately instead of queueing. Every subsequent * operation first re-verifies — from a *different* connection, by inspecting * `pg_locks` — that the backend which took the lock still holds it, so a * dropped lock connection fails loudly instead of letting operations run * unlocked. That check reports the backend it ran on as well, so a * single-connection client, which would have the lock holder vouch for * itself, is refused instead of silently trusted. If the process crashes, * Postgres releases the session lock when the connection drops; no recovery * bookkeeping is needed. * * ### Using the Postgres State Store * **Example:** Connection URL from configuration * ```typescript * import * as Alchemy from "alchemy"; * import { postgresState } from "alchemy/State/PostgresState"; * import * as Config from "effect/Config"; * * const Stack = Alchemy.Stack( * "my-stack", * { * providers: myProviders(), * state: postgresState({ url: Config.redacted("STATE_DATABASE_URL") }), * }, * Effect.gen(function* () { * // ... * }), * ); * ``` * * **Example:** Caller-owned pool * ```typescript * import * as PgClient from "@effect/sql-pg/PgClient"; * import * as Config from "effect/Config"; * * // A pool, not `PgClient.makeClient`: the store needs a second connection * // to verify the advisory lock held on the reserved one. * const sql = yield* PgClient.make({ * url: yield* Config.redacted("STATE_DATABASE_URL"), * }); * const state = postgresState({ * client: sql, * lockKeyPrefix: yield* Config.string("STATE_LOCK_PREFIX"), * }); * ``` */ export declare const postgresState: (options: PostgresStateOptions) => Layer.Layer>; /** * Construct a Postgres-backed {@link StateService}. * * Construction itself never touches the database — pool creation (when a * `url` was given), schema migration, and advisory-lock acquisition are all * deferred to the first state operation. Finalizers for the advisory locks * and any store-owned pool are registered on `scope`. */ export declare const makePostgresState: (options: PostgresStateOptions, scope: Scope.Scope) => Effect.Effect; //# sourceMappingURL=PostgresState.d.ts.map