1import * as ConfigProvider from "effect/ConfigProvider";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Option from "effect/Option";
5import { AuthProviders } from "../../Auth/AuthProvider.ts";
6import { withProfileOverride } from "../../Auth/Resolve.ts";
7import * as AwsState from "../../AWS/StateStore/State.ts";
8import * as CloudflareState from "../../Cloudflare/StateStore/State.ts";
9import * as State from "../../State/index.ts";
10import { loadConfigProvider } from "../../Util/ConfigProvider.ts";
11import { open, StackEntrypointError, type Target } from "../Session.ts";
12
13/** Which store a state request addresses. */
14export type StateSource =
15 /** `.alchemy/` on this machine, ignoring whatever the project configures. */
16 | { readonly backend: "local" }
17 /** A provider's default state store, without loading a project entrypoint. */
18 | ({ readonly backend: "aws" | "cloudflare" } & Pick<
19 Target,
20 "profile" | "envFile"
21 >)
22 /** Whatever the project's entrypoint configures. */
23 | ({ readonly backend: "configured" } & Target);
24
25/**
26 * Resolve the state service a source addresses. The tree operations
27 * (`State.listState`, `State.readState`, `State.deleteState`) take `State`
28 * from context — provide the resolved service (or {@link layer}) to run
29 * them against the chosen store:
30 *
31 * ```ts
32 * const state = yield* Alchemist.State.store({ backend: "local" });
33 * const items = yield* State.listState({ path }).pipe(
34 * Effect.provideService(State.State, Effect.succeed(state)),
35 * );
36 * ```
37 */
38export const store = Effect.fn("Alchemist.state.store")(function* (
39 source: StateSource,
40) {
41 if (source.backend === "local") {
42 return yield* Effect.provide(
43 Effect.flatten(State.State),
44 State.localState(),
45 );
46 }
47 if (source.backend === "aws" || source.backend === "cloudflare") {
48 const config = ConfigProvider.layer(
49 withProfileOverride(
50 yield* loadConfigProvider(Option.fromNullishOr(source.envFile)),
51 source.profile,
52 ),
53 );
54 const authProviders = Layer.succeed(
55 AuthProviders,
56 {} satisfies AuthProviders["Service"],
57 );
58 if (source.backend === "aws") {
59 return yield* Effect.flatten(State.State).pipe(
60 Effect.provide(AwsState.state()),
61 Effect.provide(authProviders),
62 Effect.provide(config),
63 );
64 }
65 return yield* Effect.flatten(State.State).pipe(
66 Effect.provide(CloudflareState.state()),
67 Effect.provide(authProviders),
68 Effect.provide(config),
69 );
70 }
71 // The configured store lives in the stack entrypoint; this is the one
72 // command that can also work without one.
73 const session = yield* open(source).pipe(
74 Effect.catchIf(
75 (error): error is StackEntrypointError =>
76 error instanceof StackEntrypointError,
77 (error) =>
78 Effect.fail(
79 new StackEntrypointError({
80 message: `${error.message} For config-less state access use --backend aws or --backend cloudflare.`,
81 }),
82 ),
83 ),
84 );
85 return yield* Effect.provide(Effect.flatten(State.State), session.context);
86});
87
88/** The chosen store as a `State` layer. */
89export const layer = (source: StateSource) =>
90 Layer.effect(State.State, Effect.map(store(source), Effect.succeed));