alchemy-effect 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/bin/alchemy-effect.js +539 -223
  2. package/bin/alchemy-effect.js.map +1 -1
  3. package/lib/apply.d.ts +4 -4
  4. package/lib/apply.d.ts.map +1 -1
  5. package/lib/apply.js +411 -131
  6. package/lib/apply.js.map +1 -1
  7. package/lib/aws/dynamodb/table.provider.d.ts.map +1 -1
  8. package/lib/aws/dynamodb/table.provider.js +1 -0
  9. package/lib/aws/dynamodb/table.provider.js.map +1 -1
  10. package/lib/aws/ec2/index.d.ts +8 -0
  11. package/lib/aws/ec2/index.d.ts.map +1 -1
  12. package/lib/aws/ec2/index.js +8 -0
  13. package/lib/aws/ec2/index.js.map +1 -1
  14. package/lib/aws/ec2/internet-gateway.d.ts +65 -0
  15. package/lib/aws/ec2/internet-gateway.d.ts.map +1 -0
  16. package/lib/aws/ec2/internet-gateway.js +4 -0
  17. package/lib/aws/ec2/internet-gateway.js.map +1 -0
  18. package/lib/aws/ec2/internet-gateway.provider.d.ts +6 -0
  19. package/lib/aws/ec2/internet-gateway.provider.d.ts.map +1 -0
  20. package/lib/aws/ec2/internet-gateway.provider.js +193 -0
  21. package/lib/aws/ec2/internet-gateway.provider.js.map +1 -0
  22. package/lib/aws/ec2/route-table-association.d.ts +63 -0
  23. package/lib/aws/ec2/route-table-association.d.ts.map +1 -0
  24. package/lib/aws/ec2/route-table-association.js +4 -0
  25. package/lib/aws/ec2/route-table-association.js.map +1 -0
  26. package/lib/aws/ec2/route-table-association.provider.d.ts +4 -0
  27. package/lib/aws/ec2/route-table-association.provider.d.ts.map +1 -0
  28. package/lib/aws/ec2/route-table-association.provider.js +121 -0
  29. package/lib/aws/ec2/route-table-association.provider.js.map +1 -0
  30. package/lib/aws/ec2/route-table.d.ts +159 -0
  31. package/lib/aws/ec2/route-table.d.ts.map +1 -0
  32. package/lib/aws/ec2/route-table.js +4 -0
  33. package/lib/aws/ec2/route-table.js.map +1 -0
  34. package/lib/aws/ec2/route-table.provider.d.ts +6 -0
  35. package/lib/aws/ec2/route-table.provider.d.ts.map +1 -0
  36. package/lib/aws/ec2/route-table.provider.js +213 -0
  37. package/lib/aws/ec2/route-table.provider.js.map +1 -0
  38. package/lib/aws/ec2/route.d.ts +155 -0
  39. package/lib/aws/ec2/route.d.ts.map +1 -0
  40. package/lib/aws/ec2/route.js +3 -0
  41. package/lib/aws/ec2/route.js.map +1 -0
  42. package/lib/aws/ec2/route.provider.d.ts +4 -0
  43. package/lib/aws/ec2/route.provider.d.ts.map +1 -0
  44. package/lib/aws/ec2/route.provider.js +166 -0
  45. package/lib/aws/ec2/route.provider.js.map +1 -0
  46. package/lib/aws/ec2/subnet.provider.d.ts.map +1 -1
  47. package/lib/aws/ec2/subnet.provider.js +1 -1
  48. package/lib/aws/ec2/subnet.provider.js.map +1 -1
  49. package/lib/aws/ec2/vpc.d.ts +1 -0
  50. package/lib/aws/ec2/vpc.d.ts.map +1 -1
  51. package/lib/aws/ec2/vpc.provider.d.ts +2 -2
  52. package/lib/aws/ec2/vpc.provider.d.ts.map +1 -1
  53. package/lib/aws/ec2/vpc.provider.js +38 -15
  54. package/lib/aws/ec2/vpc.provider.js.map +1 -1
  55. package/lib/aws/index.d.ts +2 -3
  56. package/lib/aws/index.d.ts.map +1 -1
  57. package/lib/aws/index.js +2 -1
  58. package/lib/aws/index.js.map +1 -1
  59. package/lib/aws/lambda/function.provider.d.ts +2 -2
  60. package/lib/aws/lambda/function.provider.d.ts.map +1 -1
  61. package/lib/aws/lambda/function.provider.js +21 -20
  62. package/lib/aws/lambda/function.provider.js.map +1 -1
  63. package/lib/aws/sqs/queue.provider.d.ts +2 -2
  64. package/lib/aws/sqs/queue.provider.d.ts.map +1 -1
  65. package/lib/aws/sqs/queue.provider.js +3 -2
  66. package/lib/aws/sqs/queue.provider.js.map +1 -1
  67. package/lib/cli/index.d.ts +178 -99
  68. package/lib/cli/index.d.ts.map +1 -1
  69. package/lib/cloudflare/kv/namespace.client.d.ts +1 -1
  70. package/lib/cloudflare/kv/namespace.provider.d.ts.map +1 -1
  71. package/lib/cloudflare/kv/namespace.provider.js +1 -0
  72. package/lib/cloudflare/kv/namespace.provider.js.map +1 -1
  73. package/lib/cloudflare/r2/bucket.provider.d.ts.map +1 -1
  74. package/lib/cloudflare/r2/bucket.provider.js +6 -1
  75. package/lib/cloudflare/r2/bucket.provider.js.map +1 -1
  76. package/lib/cloudflare/worker/worker.provider.d.ts +1 -1
  77. package/lib/cloudflare/worker/worker.provider.d.ts.map +1 -1
  78. package/lib/cloudflare/worker/worker.provider.js +6 -2
  79. package/lib/cloudflare/worker/worker.provider.js.map +1 -1
  80. package/lib/diff.d.ts +8 -6
  81. package/lib/diff.d.ts.map +1 -1
  82. package/lib/diff.js +13 -0
  83. package/lib/diff.js.map +1 -1
  84. package/lib/event.d.ts +1 -1
  85. package/lib/event.d.ts.map +1 -1
  86. package/lib/instance-id.d.ts +8 -0
  87. package/lib/instance-id.d.ts.map +1 -0
  88. package/lib/instance-id.js +12 -0
  89. package/lib/instance-id.js.map +1 -0
  90. package/lib/output.d.ts +4 -2
  91. package/lib/output.d.ts.map +1 -1
  92. package/lib/output.js +18 -4
  93. package/lib/output.js.map +1 -1
  94. package/lib/physical-name.d.ts +14 -1
  95. package/lib/physical-name.d.ts.map +1 -1
  96. package/lib/physical-name.js +41 -2
  97. package/lib/physical-name.js.map +1 -1
  98. package/lib/plan.d.ts +49 -42
  99. package/lib/plan.d.ts.map +1 -1
  100. package/lib/plan.js +359 -127
  101. package/lib/plan.js.map +1 -1
  102. package/lib/provider.d.ts +26 -9
  103. package/lib/provider.d.ts.map +1 -1
  104. package/lib/provider.js +9 -0
  105. package/lib/provider.js.map +1 -1
  106. package/lib/resource.d.ts +2 -2
  107. package/lib/resource.d.ts.map +1 -1
  108. package/lib/resource.js.map +1 -1
  109. package/lib/state.d.ts +86 -9
  110. package/lib/state.d.ts.map +1 -1
  111. package/lib/state.js +21 -18
  112. package/lib/state.js.map +1 -1
  113. package/lib/tags.d.ts +15 -0
  114. package/lib/tags.d.ts.map +1 -1
  115. package/lib/tags.js +27 -0
  116. package/lib/tags.js.map +1 -1
  117. package/lib/test.d.ts +2 -2
  118. package/lib/test.d.ts.map +1 -1
  119. package/lib/test.js +4 -4
  120. package/lib/test.js.map +1 -1
  121. package/lib/todo.d.ts +3 -0
  122. package/lib/todo.d.ts.map +1 -0
  123. package/lib/todo.js +3 -0
  124. package/lib/todo.js.map +1 -0
  125. package/lib/tsconfig.test.tsbuildinfo +1 -1
  126. package/package.json +2 -2
  127. package/src/apply.ts +758 -374
  128. package/src/aws/dynamodb/table.provider.ts +1 -0
  129. package/src/aws/ec2/index.ts +8 -0
  130. package/src/aws/ec2/internet-gateway.provider.ts +316 -0
  131. package/src/aws/ec2/internet-gateway.ts +79 -0
  132. package/src/aws/ec2/route-table-association.provider.ts +214 -0
  133. package/src/aws/ec2/route-table-association.ts +82 -0
  134. package/src/aws/ec2/route-table.provider.ts +306 -0
  135. package/src/aws/ec2/route-table.ts +175 -0
  136. package/src/aws/ec2/route.provider.ts +213 -0
  137. package/src/aws/ec2/route.ts +192 -0
  138. package/src/aws/ec2/subnet.provider.ts +2 -2
  139. package/src/aws/ec2/vpc.provider.ts +43 -19
  140. package/src/aws/ec2/vpc.ts +2 -0
  141. package/src/aws/index.ts +4 -1
  142. package/src/aws/lambda/function.provider.ts +25 -23
  143. package/src/aws/sqs/queue.provider.ts +3 -2
  144. package/src/cloudflare/kv/namespace.provider.ts +1 -0
  145. package/src/cloudflare/r2/bucket.provider.ts +7 -1
  146. package/src/cloudflare/worker/worker.provider.ts +6 -2
  147. package/src/diff.ts +35 -17
  148. package/src/event.ts +6 -0
  149. package/src/instance-id.ts +16 -0
  150. package/src/output.ts +29 -5
  151. package/src/physical-name.ts +57 -2
  152. package/src/plan.ts +488 -197
  153. package/src/provider.ts +46 -9
  154. package/src/resource.ts +50 -4
  155. package/src/state.ts +150 -35
  156. package/src/tags.ts +31 -0
  157. package/src/test.ts +5 -5
  158. package/src/todo.ts +4 -0
package/src/provider.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import * as Context from "effect/Context";
2
- import type * as Effect from "effect/Effect";
2
+ import * as Effect from "effect/Effect";
3
+ import type { ScopedPlanStatusSession } from "./cli/service.ts";
3
4
  import type { Diff } from "./diff.ts";
4
5
  import type { Input } from "./input.ts";
5
6
  import type { Resource } from "./resource.ts";
6
7
  import type { Runtime } from "./runtime.ts";
7
8
  import type { Service } from "./service.ts";
8
- import type { ScopedPlanStatusSession } from "./cli/service.ts";
9
9
 
10
10
  export interface Provider<R extends Resource | Service>
11
11
  extends Context.TagClass<
@@ -25,7 +25,21 @@ type BindingData<Res extends Resource> = [Res] extends [Runtime]
25
25
 
26
26
  type Props<Res extends Resource> = Input.ResolveOpaque<Res["props"]>;
27
27
 
28
- export interface ProviderService<Res extends Resource = Resource> {
28
+ export interface ProviderService<
29
+ Res extends Resource = Resource,
30
+ ReadReq = never,
31
+ DiffReq = never,
32
+ PrecreateReq = never,
33
+ CreateReq = never,
34
+ UpdateReq = never,
35
+ DeleteReq = never,
36
+ > {
37
+ /**
38
+ * The version of the provider.
39
+ *
40
+ * @default 0
41
+ */
42
+ version?: number;
29
43
  // tail();
30
44
  // watch();
31
45
  // replace(): Effect.Effect<void, never, never>;
@@ -33,44 +47,67 @@ export interface ProviderService<Res extends Resource = Resource> {
33
47
  // run?() {}
34
48
  read?(input: {
35
49
  id: string;
50
+ instanceId: string;
36
51
  olds: Props<Res> | undefined;
37
52
  // what is the ARN?
38
53
  output: Res["attr"] | undefined; // current state -> synced state
39
54
  session: ScopedPlanStatusSession;
40
55
  bindings: BindingData<Res>;
41
- }): Effect.Effect<Res["attr"] | undefined, any, never>;
56
+ }): Effect.Effect<Res["attr"] | undefined, any, ReadReq>;
57
+ /**
58
+ * Properties that are always stable across any update.
59
+ */
60
+ stables?: Extract<keyof Res["attr"], string>[];
42
61
  diff?(input: {
43
62
  id: string;
44
63
  olds: Props<Res>;
64
+ instanceId: string;
45
65
  // Note: we do not resolve (Props<Res>) here because diff runs during plan
46
66
  // -> we need a way for the diff handlers to work with Outputs
47
67
  news: Res["props"];
48
68
  output: Res["attr"];
49
- }): Effect.Effect<Diff | void, never, never>;
69
+ }): Effect.Effect<Diff | void, never, DiffReq>;
50
70
  precreate?(input: {
51
71
  id: string;
52
72
  news: Props<Res>;
73
+ instanceId: string;
53
74
  session: ScopedPlanStatusSession;
54
- }): Effect.Effect<Res["attr"], any, never>;
75
+ }): Effect.Effect<Res["attr"], any, PrecreateReq>;
55
76
  create(input: {
56
77
  id: string;
78
+ instanceId: string;
57
79
  news: Props<Res>;
58
80
  session: ScopedPlanStatusSession;
59
81
  bindings: BindingData<Res>;
60
- }): Effect.Effect<Res["attr"], any, never>;
82
+ }): Effect.Effect<Res["attr"], any, CreateReq>;
61
83
  update(input: {
62
84
  id: string;
85
+ instanceId: string;
63
86
  news: Props<Res>;
64
87
  olds: Props<Res>;
65
88
  output: Res["attr"];
66
89
  session: ScopedPlanStatusSession;
67
90
  bindings: BindingData<Res>;
68
- }): Effect.Effect<Res["attr"], any, never>;
91
+ }): Effect.Effect<Res["attr"], any, UpdateReq>;
69
92
  delete(input: {
70
93
  id: string;
94
+ instanceId: string;
71
95
  olds: Props<Res>;
72
96
  output: Res["attr"];
73
97
  session: ScopedPlanStatusSession;
74
98
  bindings: BindingData<Res>;
75
- }): Effect.Effect<void, any, never>;
99
+ }): Effect.Effect<void, any, DeleteReq>;
76
100
  }
101
+
102
+ export const getProviderByType = Effect.fnUntraced(function* (
103
+ resourceType: string,
104
+ ) {
105
+ const context = yield* Effect.context<never>();
106
+ const provider: ProviderService = context.unsafeMap.get(resourceType);
107
+ if (!provider) {
108
+ return yield* Effect.die(
109
+ new Error(`Provider not found for ${resourceType}`),
110
+ );
111
+ }
112
+ return provider;
113
+ });
package/src/resource.ts CHANGED
@@ -40,10 +40,56 @@ export interface Resource<
40
40
  export interface ResourceTags<R extends Resource<string, string, any, any>> {
41
41
  of<S extends ProviderService<R>>(service: S): S;
42
42
  tag: Provider<R>;
43
- effect<Err, Req>(
44
- eff: Effect<ProviderService<R>, Err, Req>,
45
- ): Layer.Layer<Provider<R>, Err, Req>;
46
- succeed(service: ProviderService<R>): Layer.Layer<Provider<R>>;
43
+ effect<
44
+ Err,
45
+ Req,
46
+ ReadReq = never,
47
+ DiffReq = never,
48
+ PrecreateReq = never,
49
+ CreateReq = never,
50
+ UpdateReq = never,
51
+ DeleteReq = never,
52
+ >(
53
+ eff: Effect<
54
+ ProviderService<
55
+ R,
56
+ ReadReq,
57
+ DiffReq,
58
+ PrecreateReq,
59
+ CreateReq,
60
+ UpdateReq,
61
+ DeleteReq
62
+ >,
63
+ Err,
64
+ Req
65
+ >,
66
+ ): Layer.Layer<
67
+ Provider<R>,
68
+ Err,
69
+ Req | ReadReq | DiffReq | PrecreateReq | CreateReq | UpdateReq | DeleteReq
70
+ >;
71
+ succeed<
72
+ ReadReq = never,
73
+ DiffReq = never,
74
+ PrecreateReq = never,
75
+ CreateReq = never,
76
+ UpdateReq = never,
77
+ DeleteReq = never,
78
+ >(
79
+ service: ProviderService<
80
+ R,
81
+ ReadReq,
82
+ DiffReq,
83
+ PrecreateReq,
84
+ CreateReq,
85
+ UpdateReq,
86
+ DeleteReq
87
+ >,
88
+ ): Layer.Layer<
89
+ Provider<R>,
90
+ never,
91
+ ReadReq | DiffReq | PrecreateReq | CreateReq | UpdateReq | DeleteReq
92
+ >;
47
93
  }
48
94
 
49
95
  export const Resource = <Ctor extends (id: string, props: any) => Resource>(
package/src/state.ts CHANGED
@@ -52,22 +52,114 @@ import { isResource } from "./resource.ts";
52
52
 
53
53
  // Scrap the "key-value" store on State/Scope
54
54
 
55
- export type ResourceStatus =
56
- | "creating"
57
- | "created"
58
- | "updating"
59
- | "updated"
60
- | "deleting"
61
- | "deleted";
62
-
63
- export type ResourceState = {
64
- type: string;
65
- id: string;
55
+ export type Props = Record<string, any>;
56
+ export type Attr = Record<string, any>;
57
+
58
+ export type ResourceStatus = ResourceState["status"];
59
+
60
+ interface BaseResourceState {
61
+ resourceType: string;
62
+ /** Logical ID of the Resource (stable across creates, updates, deletes and replaces) */
63
+ logicalId: string;
64
+ /** A unique randomly generated token used to seed ID generation (only changes when replaced) */
65
+ instanceId: string;
66
+ /** The version of the provider that was used to create/update the resource. */
67
+ providerVersion: number;
68
+ /** Current status of the logical Resource */
66
69
  status: ResourceStatus;
67
- props: any;
68
- output: any;
70
+ /** List of logical IDs of resources that depend on this resource */
71
+ downstream: string[];
72
+ /** List of Bindings attached to this Resource */
69
73
  bindings?: BindNode[];
70
- };
74
+ /** Desired state (input props) of this Resource */
75
+ props?: Props;
76
+ /** The output attributes of this Resource (if it has been created) */
77
+ attr?: Attr;
78
+ }
79
+
80
+ export interface CreatingResourceState extends BaseResourceState {
81
+ status: "creating";
82
+ /** The new resource properties that are being (or have been) applied. */
83
+ props: Props;
84
+ }
85
+
86
+ export interface CreatedResourceState extends BaseResourceState {
87
+ status: "created";
88
+ /** The new resource properties that have been applied. */
89
+ props: Props;
90
+ /** The output attributes of the created resource */
91
+ attr: Attr;
92
+ }
93
+
94
+ export interface UpdatingReourceState extends BaseResourceState {
95
+ status: "updating";
96
+ /** The new resource properties that are being (or have been) applied. */
97
+ props: Props;
98
+ old: {
99
+ /** The old resource properties that have been successfully applied. */
100
+ props: Props;
101
+ /** The old output properties that have been successfully applied. */
102
+ attr: Attr;
103
+ // TODO(sam): do I need to track the old downstream edges?
104
+ // downstream: string[];
105
+ };
106
+ }
107
+
108
+ export interface UpdatedResourceState extends BaseResourceState {
109
+ status: "updated";
110
+ /** The new resource properties that are being (or have been) applied. */
111
+ props: Props;
112
+ /** The output attributes of the created resource */
113
+ attr: Attr;
114
+ }
115
+
116
+ export interface DeletingResourceState extends BaseResourceState {
117
+ status: "deleting";
118
+ /** Attributes of the resource being deleted */
119
+ attr: Attr | undefined;
120
+ }
121
+
122
+ export interface ReplacingResourceState extends BaseResourceState {
123
+ status: "replacing";
124
+ /** Desired properties of the new resource (the replacement) */
125
+ props: Props;
126
+ /** Reference to the state of the old resource (the one being replaced) */
127
+ old:
128
+ | CreatedResourceState
129
+ | UpdatedResourceState
130
+ | CreatingResourceState
131
+ | UpdatingReourceState
132
+ | DeletingResourceState;
133
+ /** Whether the resource should be deleted before or after replacements */
134
+ deleteFirst: boolean;
135
+ }
136
+
137
+ export interface ReplacedResourceState extends BaseResourceState {
138
+ status: "replaced";
139
+ /** Desired properties of the new resource (the replacement) */
140
+ props: Props;
141
+ /** Output attributes of the new resource (the replacement) */
142
+ attr: Attr;
143
+ /** Reference to the state of the old resource (the one being replaced) */
144
+ old:
145
+ | CreatingResourceState
146
+ | CreatedResourceState
147
+ | UpdatingReourceState
148
+ | UpdatedResourceState
149
+ | DeletingResourceState;
150
+ /** Whether the resource should be deleted before or after replacements */
151
+ deleteFirst: boolean;
152
+ // .. will (finally) transition to `CreatedResourceState` after finalizing
153
+ }
154
+
155
+ export type ResourceState =
156
+ | CreatingResourceState
157
+ | CreatedResourceState
158
+ | UpdatingReourceState
159
+ | UpdatedResourceState
160
+ | DeletingResourceState
161
+ | ReplacingResourceState
162
+ | ReplacedResourceState;
71
163
 
72
164
  export class StateStoreError extends Data.TaggedError("StateStoreError")<{
73
165
  message: string;
@@ -82,6 +174,10 @@ export interface StateService {
82
174
  stage: string;
83
175
  resourceId: string;
84
176
  }): Effect.Effect<ResourceState | undefined, StateStoreError, never>;
177
+ getReplacedResources(request: {
178
+ stack: string;
179
+ stage: string;
180
+ }): Effect.Effect<ReplacedResourceState[], StateStoreError, never>;
85
181
  set<V extends ResourceState>(request: {
86
182
  stack: string;
87
183
  stage: string;
@@ -107,7 +203,6 @@ export class State extends Context.Tag("AWS::Lambda::State")<
107
203
  // TODO(sam): implement with SQLite3
108
204
  export const localFs = Layer.effect(
109
205
  State,
110
- // @ts-expect-error -
111
206
  Effect.gen(function* () {
112
207
  const fs = yield* FileSystem.FileSystem;
113
208
  const path = yield* Path.Path;
@@ -146,8 +241,8 @@ export const localFs = Layer.effect(
146
241
  fs.makeDirectory(dir, { recursive: true }),
147
242
  );
148
243
 
149
- return {
150
- listApps: () =>
244
+ const state: StateService = {
245
+ listStacks: () =>
151
246
  fs.readDirectory(stateDir).pipe(
152
247
  recover,
153
248
  Effect.map((files) => files ?? []),
@@ -162,6 +257,17 @@ export const localFs = Layer.effect(
162
257
  Effect.map((file) => JSON.parse(file.toString())),
163
258
  recover,
164
259
  ),
260
+ getReplacedResources: Effect.fnUntraced(function* (request) {
261
+ return (yield* Effect.all(
262
+ (yield* state.list(request)).map((resourceId) =>
263
+ state.get({
264
+ stack: request.stack,
265
+ stage: request.stage,
266
+ resourceId,
267
+ }),
268
+ ),
269
+ )).filter((r) => r?.status === "replaced");
270
+ }),
165
271
  set: (request) =>
166
272
  ensure(stage(request)).pipe(
167
273
  Effect.flatMap(() =>
@@ -196,6 +302,7 @@ export const localFs = Layer.effect(
196
302
  ),
197
303
  ),
198
304
  };
305
+ return state;
199
306
  }),
200
307
  );
201
308
 
@@ -221,22 +328,14 @@ export const inMemoryService = (
221
328
  Record<StageId, Record<ResourceId, ResourceState>>
222
329
  > = {},
223
330
  ) => {
224
- const state = new Map<StackId, Map<StageId, Map<ResourceId, ResourceState>>>(
225
- Object.entries(initialState).map(([stack, stages]) => [
226
- stack,
227
- new Map(
228
- Object.entries(stages).map(([stage, resources]) => [
229
- stage,
230
- new Map(Object.entries(resources)),
231
- ]),
232
- ),
233
- ]),
234
- );
331
+ const state = initialState;
235
332
  return {
236
- listStacks: () => Effect.succeed(Array.from(state.keys())),
333
+ listStacks: () => Effect.succeed(Array.from(Object.keys(state))),
237
334
  // oxlint-disable-next-line require-yield
238
335
  listStages: (stack: string) =>
239
- Effect.succeed(Array.from(state.get(stack)?.keys() ?? [])),
336
+ Effect.succeed(
337
+ Array.from(stack in state ? Object.keys(state[stack]) : []),
338
+ ),
240
339
  get: ({
241
340
  stack,
242
341
  stage,
@@ -245,7 +344,19 @@ export const inMemoryService = (
245
344
  stack: string;
246
345
  stage: string;
247
346
  resourceId: string;
248
- }) => Effect.succeed(state.get(stack)?.get(stage)?.get(resourceId)),
347
+ }) => Effect.succeed(state[stack]?.[stage]?.[resourceId]),
348
+ getReplacedResources: ({
349
+ stack,
350
+ stage,
351
+ }: {
352
+ stack: string;
353
+ stage: string;
354
+ }) =>
355
+ Effect.succeed(
356
+ Array.from(Object.values(state[stack]?.[stage] ?? {}) ?? []).filter(
357
+ (s) => s.status === "replaced",
358
+ ),
359
+ ),
249
360
  set: <V extends ResourceState>({
250
361
  stack,
251
362
  stage,
@@ -257,7 +368,9 @@ export const inMemoryService = (
257
368
  resourceId: string;
258
369
  value: V;
259
370
  }) => {
260
- state.get(stack)?.get(stage)?.set(resourceId, value);
371
+ const stackState = (state[stack] ??= {});
372
+ const stageState = (stackState[stage] ??= {});
373
+ stageState[resourceId] = value;
261
374
  return Effect.succeed(value);
262
375
  },
263
376
  delete: ({
@@ -268,8 +381,10 @@ export const inMemoryService = (
268
381
  stack: string;
269
382
  stage: string;
270
383
  resourceId: string;
271
- }) => Effect.succeed(state.get(stack)?.get(stage)?.delete(resourceId)),
384
+ }) => Effect.succeed(delete state[stack]?.[stage]?.[resourceId]),
272
385
  list: ({ stack, stage }: { stack: string; stage: string }) =>
273
- Effect.succeed(Array.from(state.get(stack)?.get(stage)?.keys() ?? [])),
274
- };
386
+ Effect.succeed(
387
+ Array.from(Object.keys(state[stack]?.[stage] ?? {}) ?? []),
388
+ ),
389
+ } satisfies StateService;
275
390
  };
package/src/tags.ts CHANGED
@@ -36,3 +36,34 @@ export const createTagger = Effect.fn(function* () {
36
36
  "alchemy::id": id,
37
37
  });
38
38
  });
39
+
40
+ export const diffTags = (
41
+ oldTags: Record<string, string>,
42
+ newTags: Record<string, string>,
43
+ ) => {
44
+ const removed: string[] = [];
45
+ const updated: { Key: string; Value: string }[] = [];
46
+ const added: { Key: string; Value: string }[] = [];
47
+ for (const key in oldTags) {
48
+ if (!(key in newTags)) {
49
+ removed.push(key);
50
+ } else if (oldTags[key] !== newTags[key]) {
51
+ updated.push({ Key: key, Value: newTags[key] });
52
+ }
53
+ }
54
+ for (const key in newTags) {
55
+ if (!(key in oldTags)) {
56
+ added.push({ Key: key, Value: newTags[key] });
57
+ } else if (oldTags[key] !== newTags[key]) {
58
+ updated.push({ Key: key, Value: newTags[key] });
59
+ }
60
+ }
61
+ return {
62
+ added,
63
+ removed,
64
+ updated,
65
+ upsert: [...added, ...updated].filter(
66
+ (tag, index, self) => self.findIndex((t) => t.Key === tag.Key) === index,
67
+ ),
68
+ };
69
+ };
package/src/test.ts CHANGED
@@ -1,18 +1,18 @@
1
1
  import { FetchHttpClient, FileSystem, HttpClient } from "@effect/platform";
2
- import * as PlatformConfigProvider from "@effect/platform/PlatformConfigProvider";
3
2
  import { NodeContext } from "@effect/platform-node";
4
3
  import * as Path from "@effect/platform/Path";
5
- import { it, expect } from "@effect/vitest";
4
+ import * as PlatformConfigProvider from "@effect/platform/PlatformConfigProvider";
5
+ import { expect, it } from "@effect/vitest";
6
6
  import { ConfigProvider, LogLevel } from "effect";
7
7
  import * as Effect from "effect/Effect";
8
8
  import * as Layer from "effect/Layer";
9
9
  import * as Logger from "effect/Logger";
10
10
  import * as Scope from "effect/Scope";
11
11
  import * as App from "./app.ts";
12
+ import { CLI } from "./cli/service.ts";
12
13
  import { DotAlchemy, dotAlchemy } from "./dot-alchemy.ts";
13
- import * as State from "./state.ts";
14
14
  import type { Resource } from "./resource.ts";
15
- import { CLI } from "./cli/service.ts";
15
+ import * as State from "./state.ts";
16
16
 
17
17
  declare module "@effect/vitest" {
18
18
  interface ExpectStatic {
@@ -165,7 +165,7 @@ export const testCLI = Layer.succeed(
165
165
  Effect.log(
166
166
  event.kind === "status-change"
167
167
  ? `${event.status} ${event.id}(${event.type})`
168
- : event.message,
168
+ : `${event.id}: ${event.message}`,
169
169
  ),
170
170
  }),
171
171
  }),
package/src/todo.ts ADDED
@@ -0,0 +1,4 @@
1
+ import * as Effect from "effect/Effect";
2
+
3
+ export const todo = (message?: string) =>
4
+ Effect.dieMessage(message ?? `Not implemented`);