1import * as Context from "effect/Context";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import type { ActionState } from "./ActionState.ts";
5import type { ReplacedResourceState, ResourceState } from "./ResourceState.ts";
6
7/**
8 * Anything persistable under an FQN. Resources are discriminated by status
9 * strings ("creating", "created", …) and Tasks by `kind: "action"`.
10 */
11export type PersistedState = ResourceState | ActionState;
12
13export const isActionState = (
14 s: PersistedState | undefined,
15): s is ActionState => !!s && (s as any).kind === "action";
16
17export const isResourceState = (
18 s: PersistedState | undefined,
19): s is ResourceState => !!s && (s as any).kind !== "task";
20
21export class StateStoreError extends Data.TaggedError("StateStoreError")<{
22 message: string;
23 cause?: Error;
24 /** HTTP failure metadata without the request, response body, or credentials. */
25 http?: {
26 /** Undefined when the HTTP client failed before receiving a response. */
27 status?: number;
28 };
29}> {}
30
31export class State extends Context.Service<
32 State,
33 Effect.Effect<StateService>
34>()("alchemy/State") {}
35
36/**
37 * State service interface.
38 *
39 * Resources are keyed by FQN (namespace-qualified key) which includes
40 * the full namespace path plus the logical ID. The FQN is used as the
41 * storage key while logicalId remains available in the persisted state
42 * for provider operations.
43 */
44export interface StateService {
45 /**
46 * Stable identifier for the State store implementation, used for
47 * telemetry tagging (`alchemy.state_store.id`) so we can answer
48 * "which backends are people using" without hard-coding a closed
49 * union. Examples: `"local"`, `"inmemory"`, `"http"`,
50 * `"cloudflare-http"`. Third-party state stores should pick a short,
51 * stable, kebab-case slug.
52 */
53 readonly id: string;
54 /**
55 * Wire / behavioural contract version of this state-store
56 * implementation. For local / in-process stores this is the
57 * `STATE_STORE_VERSION` the CLI was built against; for HTTP-backed
58 * stores it is the version reported by the deployed `/version`
59 * probe.
60 */
61 getVersion(): Effect.Effect<number, StateStoreError, never>;
62 listStacks(): Effect.Effect<readonly string[], StateStoreError, never>;
63 listStages(
64 stack: string,
65 ): Effect.Effect<readonly string[], StateStoreError, never>;
66 /**
67 * Get a resource by its FQN (namespace-qualified key).
68 */
69 get(request: {
70 stack: string;
71 stage: string;
72 fqn: string;
73 }): Effect.Effect<PersistedState | undefined, StateStoreError, never>;
74 /**
75 * List top-level resources that are still in replacement cleanup.
76 *
77 * Any additional backlog from repeated replacements is stored recursively
78 * in the returned state's `old` chain.
79 */
80 getReplacedResources(request: {
81 stack: string;
82 stage: string;
83 }): Effect.Effect<
84 ReadonlyArray<ReplacedResourceState>,
85 StateStoreError,
86 never
87 >;
88 /**
89 * Set a resource by its FQN (namespace-qualified key).
90 */
91 set<V extends PersistedState>(request: {
92 stack: string;
93 stage: string;
94 fqn: string;
95 value: V;
96 }): Effect.Effect<V, StateStoreError, never>;
97 /**
98 * Delete a resource by its FQN (namespace-qualified key).
99 */
100 delete(request: {
101 stack: string;
102 stage: string;
103 fqn: string;
104 }): Effect.Effect<void, StateStoreError, never>;
105 /**
106 * Delete an entire stack, or a single stage when `stage` is provided.
107 */
108 deleteStack(request: {
109 stack: string;
110 stage?: string;
111 }): Effect.Effect<void, StateStoreError, never>;
112 /**
113 * List all resource FQNs in a stack/stage.
114 */
115 list(request: {
116 stack: string;
117 stage: string;
118 }): Effect.Effect<readonly string[], StateStoreError, never>;
119 /**
120 * Read the persisted stack output for `(stack, stage)`. Returns
121 * `undefined` when the stack has not been deployed (or has been
122 * destroyed) at this stage.
123 *
124 * Stack outputs are written by `apply` once the deploy succeeds and
125 * read by cross-stack references (`yield* OtherStack` /
126 * `OtherStack.stage.<name>` / `Output.stackRef(...)`).
127 */
128 getOutput(request: {
129 stack: string;
130 stage: string;
131 }): Effect.Effect<unknown, StateStoreError, never>;
132 /**
133 * Persist the resolved stack output for `(stack, stage)`.
134 */
135 setOutput(request: {
136 stack: string;
137 stage: string;
138 value: unknown;
139 }): Effect.Effect<unknown, StateStoreError, never>;
140}