1import * as Effect from "effect/Effect";
2import { State, type PersistedState } from "./State.ts";
3import { 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 */
9export 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 */
26export interface StateExport {
27 resources: ExportedResource[];
28}
29
30/**
31 * Optional scope narrowing for {@link exportState}.
32 */
33export 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 */
53export 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});