1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import { recordStateStoreInit } from "../Telemetry/Metrics.ts";
4import { STATE_STORE_VERSION } from "./HttpStateApi.ts";
5import type { ResourceState } from "./ResourceState.ts";
6import { State, type PersistedState } from "./State.ts";
7
8type StackId = string;
9type StageId = string;
10type Fqn = string;
11
12export const inMemoryState = (
13 initialState: Record<
14 StackId,
15 Record<StageId, Record<Fqn, ResourceState>>
16 > = {},
17 initialOutputs: Record<StackId, Record<StageId, unknown>> = {},
18) =>
19 Layer.effect(
20 State,
21 Effect.cached(
22 InMemoryService(initialState, initialOutputs).pipe(recordStateStoreInit),
23 ),
24 );
25
26export const InMemoryService = (
27 state: Record<StackId, Record<StageId, Record<Fqn, ResourceState>>> = {},
28 outputs: Record<StackId, Record<StageId, unknown>> = {},
29) =>
30 State.of(
31 Effect.succeed({
32 id: "inmemory",
33 getVersion: () => Effect.succeed(STATE_STORE_VERSION),
34 listStacks: () => Effect.succeed(Array.from(Object.keys(state))),
35 listStages: (stack: string) =>
36 Effect.succeed(
37 Array.from(stack in state ? Object.keys(state[stack]) : []),
38 ),
39 get: ({
40 stack,
41 stage,
42 fqn,
43 }: {
44 stack: string;
45 stage: string;
46 fqn: string;
47 }) => Effect.succeed(state[stack]?.[stage]?.[fqn]),
48 getReplacedResources: ({
49 stack,
50 stage,
51 }: {
52 stack: string;
53 stage: string;
54 }) =>
55 Effect.succeed(
56 Array.from(Object.values(state[stack]?.[stage] ?? {}) ?? []).filter(
57 (s) => s.status === "replaced",
58 ),
59 ),
60 set: <V extends PersistedState>({
61 stack,
62 stage,
63 fqn,
64 value,
65 }: {
66 stack: string;
67 stage: string;
68 fqn: string;
69 value: V;
70 }) =>
71 Effect.sync(() => {
72 const stackState = (state[stack] ??= {});
73 const stageState = (stackState[stage] ??= {});
74 stageState[fqn] = value as ResourceState;
75 return value;
76 }),
77 delete: ({
78 stack,
79 stage,
80 fqn,
81 }: {
82 stack: string;
83 stage: string;
84 fqn: string;
85 }) => Effect.sync(() => delete state[stack]?.[stage]?.[fqn]),
86 deleteStack: ({ stack, stage }: { stack: string; stage?: string }) =>
87 Effect.sync(() => {
88 if (stage === undefined) {
89 delete state[stack];
90 delete outputs[stack];
91 } else {
92 delete state[stack]?.[stage];
93 delete outputs[stack]?.[stage];
94 }
95 }),
96 list: ({ stack, stage }: { stack: string; stage: string }) =>
97 Effect.succeed(
98 Array.from(Object.keys(state[stack]?.[stage] ?? {}) ?? []),
99 ),
100 getOutput: ({ stack, stage }: { stack: string; stage: string }) =>
101 Effect.succeed(outputs[stack]?.[stage]),
102 setOutput: ({
103 stack,
104 stage,
105 value,
106 }: {
107 stack: string;
108 stage: string;
109 value: unknown;
110 }) =>
111 Effect.sync(() => {
112 const stackOutputs = (outputs[stack] ??= {});
113 stackOutputs[stage] = value;
114 return value;
115 }),
116 }),
117 );