1import * as Context from "effect/Context";
2import * as Deferred from "effect/Deferred";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6
7/**
8 * Per-resource in-memory artifacts shared across a single `Plan.make -> apply`
9 * execution.
10 *
11 * The engine scopes this service by resource `FQN` before invoking lifecycle
12 * handlers, so providers should treat it as a resource-local bag for expensive,
13 * deterministic intermediate results that can be reused across phases.
14 *
15 * Expected usage:
16 *
17 * - `diff` computes an expensive artifact once and stores it
18 * - `create` / `update` reads the same artifact and skips recomputing it
19 * - artifacts are ephemeral and must never be required for correctness on a
20 * later deploy because the bag is reset between runs
21 *
22 * Example:
23 *
24 * ```ts
25 * const artifacts = yield* Artifacts;
26 * const cached = yield* artifacts.get<PreparedBundle>("bundle");
27 * if (cached) return cached;
28 *
29 * const bundle = yield* prepareBundle();
30 * yield* artifacts.set("bundle", bundle);
31 * return bundle;
32 * ```
33 */
34export class Artifacts extends Context.Service<
35 Artifacts,
36 {
37 /**
38 * Get an artifact by key from the current resource's bag.
39 */
40 get<T>(key: string): Effect.Effect<T | undefined>;
41 /**
42 * Store an artifact by key in the current resource's bag.
43 */
44 set<T>(key: string, value: T): Effect.Effect<void>;
45 /**
46 * Delete an artifact by key from the current resource's bag.
47 */
48 delete(key: string): Effect.Effect<void>;
49 }
50>()("Artifacts") {}
51
52type ArtifactBag = Map<string, unknown>;
53
54export class ArtifactStore extends Context.Service<
55 ArtifactStore,
56 Map<string, ArtifactBag>
57>()("Artifacts/Store") {}
58
59/**
60 * Create a fresh root store for one deploy/test run.
61 */
62export const createArtifactStore = (): ArtifactStore["Service"] =>
63 new Map<string, ArtifactBag>();
64
65const getOrCreateBag = (
66 store: Map<string, ArtifactBag>,
67 fqn: string,
68): ArtifactBag => {
69 const existing = store.get(fqn);
70 if (existing) {
71 return existing;
72 }
73 const bag = new Map<string, unknown>();
74 store.set(fqn, bag);
75 return bag;
76};
77
78export const makeScopedArtifacts = (
79 store: Map<string, ArtifactBag>,
80 fqn: string,
81): Artifacts["Service"] => {
82 const bag = getOrCreateBag(store, fqn);
83 return {
84 get: <T>(key: string) => Effect.sync(() => bag.get(key) as T | undefined),
85 set: <T>(key: string, value: T) =>
86 Effect.sync(() => {
87 bag.set(key, value);
88 }),
89 delete: (key: string) =>
90 Effect.sync(() => {
91 bag.delete(key);
92 }),
93 };
94};
95
96export const scopedArtifacts = (
97 fqn: string,
98): Layer.Layer<Artifacts, never, ArtifactStore> =>
99 Layer.effect(
100 Artifacts,
101 ArtifactStore.useSync((store) => makeScopedArtifacts(store, fqn)),
102 );
103
104/**
105 * Run an effect with a fresh artifact root, replacing any existing store.
106 * Use this at top-level entrypoints that intentionally define a new deploy run.
107 */
108export const provideFreshArtifactStore = <A, E, R>(
109 effect: Effect.Effect<A, E, R | ArtifactStore>,
110): Effect.Effect<A, E, R> =>
111 effect.pipe(
112 Effect.provideServiceEffect(
113 ArtifactStore,
114 Effect.sync(createArtifactStore),
115 ),
116 );
117
118/**
119 * Ensure an artifact root exists, reusing the ambient store when one is already
120 * present. This lets nested helpers participate in the same run-scoped cache.
121 */
122export const ensureArtifactStore = <A, E, R>(
123 effect: Effect.Effect<A, E, R | ArtifactStore>,
124): Effect.Effect<A, E, R> =>
125 Effect.serviceOption(ArtifactStore).pipe(
126 Effect.map(Option.getOrUndefined),
127 Effect.flatMap((existing) =>
128 effect.pipe(
129 Effect.provideService(ArtifactStore, existing ?? createArtifactStore()),
130 ),
131 ),
132 );
133
134export const cached =
135 (id: string) =>
136 <A, Err = never, Req = never>(
137 eff: Effect.Effect<A, Err, Req>,
138 ): Effect.Effect<A, Err, Req | Artifacts> =>
139 Effect.gen(function* () {
140 const artifacts = yield* Artifacts;
141 const deferred = yield* Deferred.make<A>();
142 const cached = yield* artifacts.get<A | Effect.Effect<A>>(id);
143 if (cached) {
144 if (Effect.isEffect(cached)) {
145 return yield* cached;
146 }
147 return cached;
148 }
149 yield* artifacts.set(id, Deferred.await(deferred));
150 const result = yield* eff;
151 yield* Deferred.succeed(deferred, result);
152 yield* artifacts.set(id, result);
153 return result;
154 });