| 1 | import * as Effect from "effect/Effect"; |
| 2 | import { State, type PersistedState } from "./State.ts"; |
| 3 | import { allStages } from "./Tree.ts"; |
| 4 | |
| 5 | /** |
| 6 | * One resource record in a state export, addressed by the |
| 7 | * `(stack, stage, fqn)` triple so a flat list spans the whole estate. |
| 8 | */ |
| 9 | export interface ExportedResource { |
| 10 | /** Stack the record belongs to. */ |
| 11 | stack: string; |
| 12 | /** Stage the record belongs to. */ |
| 13 | stage: string; |
| 14 | /** Fully-qualified resource name (namespace path + logical ID). */ |
| 15 | fqn: string; |
| 16 | /** The persisted record — the same value `state.get` returns. */ |
| 17 | state: PersistedState; |
| 18 | } |
| 19 | |
| 20 | /** |
| 21 | * A bulk read of the state store: every matching resource record in a |
| 22 | * single document. Shaped as a flat list so consumers can filter |
| 23 | * locally (e.g. `jq '.resources[] | select(...)'`) instead of issuing |
| 24 | * one read per resource. |
| 25 | */ |
| 26 | export interface StateExport { |
| 27 | resources: ExportedResource[]; |
| 28 | } |
| 29 | |
| 30 | /** |
| 31 | * Optional scope narrowing for {@link exportState}. |
| 32 | */ |
| 33 | export interface ExportStateFilter { |
| 34 | /** Only export this stack. Omit to export every stack in the store. */ |
| 35 | stack?: string; |
| 36 | /** Only export this stage. Omit to export every stage. */ |
| 37 | stage?: string; |
| 38 | } |
| 39 | |
| 40 | /** |
| 41 | * Read every resource record in scope from the state store as one |
| 42 | * document. |
| 43 | * |
| 44 | * Fans out over every `(stack, stage)` the store holds — see |
| 45 | * {@link allStages} — with the per-resource reads bounded, so a whole |
| 46 | * estate is fetched in a single pass instead of a call per resource. |
| 47 | * Records that disappear between `list` and `get` (a concurrent |
| 48 | * deploy/destroy) are skipped rather than failing the export. |
| 49 | * |
| 50 | * Results are ordered deterministically: by stack, then stage, then |
| 51 | * FQN. |
| 52 | */ |
| 53 | export const exportState = Effect.fn("exportState")(function* ( |
| 54 | filter: ExportStateFilter = {}, |
| 55 | ) { |
| 56 | const state = yield* yield* State; |
| 57 | const perStage = yield* Effect.forEach( |
| 58 | yield* allStages(filter), |
| 59 | ({ stack, stage }) => |
| 60 | Effect.gen(function* () { |
| 61 | const fqns = [...(yield* state.list({ stack, stage }))].sort(); |
| 62 | const records = yield* Effect.forEach( |
| 63 | fqns, |
| 64 | (fqn) => |
| 65 | Effect.map(state.get({ stack, stage, fqn }), (value) => |
| 66 | value === undefined |
| 67 | ? undefined |
| 68 | : ({ stack, stage, fqn, state: value } as const), |
| 69 | ), |
| 70 | { concurrency: 16 }, |
| 71 | ); |
| 72 | return records.filter((r): r is ExportedResource => r !== undefined); |
| 73 | }), |
| 74 | { concurrency: "unbounded" }, |
| 75 | ); |
| 76 | return { resources: perStage.flat() } satisfies StateExport; |
| 77 | }); |