| 1 | import * as ConfigProvider from "effect/ConfigProvider"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as Option from "effect/Option"; |
| 5 | import { AuthProviders } from "../../Auth/AuthProvider.ts"; |
| 6 | import { withProfileOverride } from "../../Auth/Resolve.ts"; |
| 7 | import * as AwsState from "../../AWS/StateStore/State.ts"; |
| 8 | import * as CloudflareState from "../../Cloudflare/StateStore/State.ts"; |
| 9 | import * as State from "../../State/index.ts"; |
| 10 | import { loadConfigProvider } from "../../Util/ConfigProvider.ts"; |
| 11 | import { open, StackEntrypointError, type Target } from "../Session.ts"; |
| 12 | |
| 13 | /** Which store a state request addresses. */ |
| 14 | export 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 | */ |
| 38 | export 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. */ |
| 89 | export const layer = (source: StateSource) => |
| 90 | Layer.effect(State.State, Effect.map(store(source), Effect.succeed)); |