/** @effect-diagnostics anyUnknownInErrorContext:off */ import * as Effect from "effect/Effect"; import * as Scope from "effect/Scope"; import type { ScopedPlanStatusSession } from "../Cli/Cli.ts"; import type { Platform } from "../Platform.ts"; import type { ProviderService } from "../Provider.ts"; import type { ResourceBinding, ResourceClassLike, ResourceLike } from "../Resource.ts"; /** * # LocalProvider — the standard shape of a long-running local provider * * A local provider's "physical resource" is a running process (a spawned * dev server, a workerd instance) that lives in the dev sidecar and is * invisible to the state store: a state row can say `created` while the * process is dead (the previous dev session ended), and a redundant * reconcile can arrive while the process is healthy. Every local provider * therefore needs the same machinery: * * - an **instance registry** answering "is something running for this * resource right now?", * - a **config comparison** answering "was it started with the config I'm * being asked for?", * - restart/teardown choreography that keeps the process alive across * deploys (scopes forked from the provider root scope) and kills it on * change or delete. * * {@link make} generates `diff` / `reconcile` / `delete` / `list` from that * machinery; the provider author writes {@link LocalProviderSpec.start} * (how to boot one instance) and optionally * {@link LocalProviderSpec.resolveConfig} (what the restart-relevant config * is) and {@link LocalProviderSpec.stop} (cleanup that outlives instance * scopes). * * The produced layer wraps {@link RpcProvider.effect}, so during * `alchemy dev` the provider (and its registry of running processes) lives * in the sidecar process and survives user-code hot reloads. */ /** Inputs common to every generated lifecycle call. */ export interface LocalProviderInput { id: string; fqn: string; instanceId: string; news: R["Props"]; bindings: ResourceBinding[]; } export interface StartContext extends LocalProviderInput { /** * The value produced by {@link LocalProviderSpec.resolveConfig} for this * reconcile — the same value whose canonical hash decided that a * (re)start was needed, so "what changed?" and "what starts?" can never * drift. */ config: Config; session: ScopedPlanStatusSession; /** * Removes this instance from the running registry (a no-op if it has * already been replaced), so the next `diff` reports `update` and the * next reconcile boots a fresh instance. Fork this after the process's * exit — e.g. `child.exitCode.pipe(Effect.exit, Effect.flatMap(() => * invalidate), Effect.forkScoped)` — for processes that can die on their * own. */ invalidate: Effect.Effect; } export interface StopContext { id: string; /** * Fully-qualified name (namespace path + logical id) — the key the * generated instance registry uses. Cross-restart state a provider keeps * outside instance scopes MUST be keyed by this, not `id`: two resources * in different namespaces can share a logical id (two `AWS.Website.*` * sites each declaring a `Command.Dev("Dev")`, ...). */ fqn: string; instanceId: string; } export interface StablesContext extends LocalProviderInput { config: Config; output: R["Attributes"] | undefined; } /** The default restart-relevant config when `resolveConfig` is omitted. */ export interface DefaultLocalConfig { news: R["Props"]; bindings: ResourceBinding[]; } export interface LocalProviderSpec, StartR = never> { /** * Derive the restart-relevant desired config from resolved inputs. * * MUST return plain, canonically-serializable data (no closures, no * Effects — `Redacted` values are unwrapped by the hasher) and must be * cheap and side-effect-free: it runs inside `diff` on every plan. The * helper canonically hashes the result for the noop-vs-restart decision, * records it on the registry entry, and passes it to {@link start} so a * restart consumes exactly what was compared. * * Lifecycle-scoped services (`Stack`, `Stage`, `InstanceId`, `Artifacts`) * are available ambiently (e.g. for `createPhysicalName`); resolve * anything else in the spec effect and close over it. * * @default `{ news: stripEffects(news), bindings }` */ resolveConfig?: (ctx: LocalProviderInput) => Effect.Effect; /** * Boot one instance: acquire the long-running process in the ambient * `Scope` and return the resource's Attributes once it is *ready* (URL * extracted, first serve succeeded, ...). The scope outlives the * reconcile that started it — it is forked from the provider root scope * and closed only when the instance is restarted (config change) or * deleted. The returned Effect MAY complete after readiness; liveness is * "registry entry with an open scope", not "unfinished fiber". * * Extra requirements beyond `Scope` (`StartR`) become requirements of * the provider layer — they must be present at layer build so the * lifecycle wrapper can hand them to `start` at runtime. */ start: (ctx: StartContext) => Effect.Effect; /** * Extra cleanup on delete, after the instance scope has closed — for * state that intentionally spans restarts and therefore cannot live in * the instance scope (proxy servers kept for URL stability, restart * hooks, ...). NOT called on restarts: a restarting instance keeps that * shared state. Must be idempotent; also called when nothing is running * (e.g. cleaning up a local row during a live deploy). */ stop?: (ctx: StopContext) => Effect.Effect; /** * Attributes that remain stable across the update the generated `diff` * is about to report (see `Diff.stables`). Called only when the diff is * an `update`. */ stables?: (ctx: StablesContext) => Effect.Effect[] | undefined, any, any>; precreate?: AnyReqProviderService["precreate"]; tail?: AnyReqProviderService["tail"]; logs?: AnyReqProviderService["logs"]; /** * Override the generated `list` (which joins every registered instance's * fiber to its Attributes). */ list?: AnyReqProviderService["list"]; } /** * {@link ProviderService} with every requirement channel widened to `any` — * passthrough spec hooks may use lifecycle-scoped services (`Stack`, * `InstanceId`, ...) that the RpcProvider wrapper provides per call. */ type AnyReqProviderService = ProviderService; /** * Canonical, collision-free hash of a plain-data value: SHA-256 over a * canonical JSON serialization (sorted keys, unwrapped `Redacted`, * `Uint8Array` and `bigint` encodings, cycle-safe). * * We deliberately do NOT use `Hash.structure`: Effect's structural hash * folds sibling fields together with XOR, so the same value change * appearing in two sibling subtrees cancels out and the hash is unchanged * — exactly the shape of a Worker config that mirrors `env` values into * derived `bindings`. An exact canonical serialization avoids the lossy * fingerprint; SHA-256 keeps the retained signature a fixed 64-char digest. */ export declare const canonicalHash: (value: unknown) => Effect.Effect; /** * Build a local provider layer from a {@link LocalProviderSpec}. * * Generated lifecycle: * * - **diff** — `undefined` while inputs are unresolved; otherwise resolve * the config and compare its canonical hash against the running * instance: running + equal → `noop`, anything else → `update`. This is * what encodes "the process may or may not be running" — state alone can * never answer it. * - **reconcile** — same hash + running instance → join its fiber for the * Attributes (redundant reconciles are free). Otherwise tear the old * instance down and fork {@link LocalProviderSpec.start} in a fresh * scope forked from the provider root scope, so the process outlives the * deploy and dies with the sidecar. * - **delete** — tear down only if the registered instance belongs to the * deleted `instanceId` (create-first replacement safety), then run * {@link LocalProviderSpec.stop}. Idempotent. * - **list** — join every registered instance's fiber to its Attributes. * * All reconcile/delete work for one logical resource is serialized behind * a per-id semaphore so restarts never interleave. * * @param cls - the resource class (or Platform) this provider serves. * @param serverEntryUrl - sidecar entry module (see {@link RpcProvider.effect}). * @param spec - Effect constructing the {@link LocalProviderSpec}; resolve * the services your callbacks need here and close over them. */ export declare const make: , StartR = never, Req = never>(cls: ResourceClassLike | Platform, serverEntryUrl: string, spec: Effect.Effect, never, Req>) => import("effect/Layer").Layer, never, import("../AlchemyContext.ts").AlchemyContext | import("../Artifacts.ts").ArtifactStore | import("../Stack.ts").Stack | Exclude | Exclude, import("../Artifacts.ts").Artifacts | import("../InstanceId.ts").InstanceId>>; export {}; //# sourceMappingURL=LocalProvider.d.ts.map