alchemy 2.0.0-beta.46 → 2.0.0-beta.47

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 (91) hide show
  1. package/bin/exec.js +18 -10
  2. package/bin/exec.js.map +1 -1
  3. package/lib/Apply.js +2 -2
  4. package/lib/Apply.js.map +1 -1
  5. package/lib/Cli/commands/_shared.d.ts +7 -1
  6. package/lib/Cli/commands/_shared.d.ts.map +1 -1
  7. package/lib/Cli/commands/_shared.js +1 -0
  8. package/lib/Cli/commands/_shared.js.map +1 -1
  9. package/lib/Cli/commands/login.d.ts +1 -1
  10. package/lib/Cli/commands/login.d.ts.map +1 -1
  11. package/lib/Cli/commands/login.js +45 -42
  12. package/lib/Cli/commands/login.js.map +1 -1
  13. package/lib/Cli/commands/logs.js +1 -1
  14. package/lib/Cli/commands/logs.js.map +1 -1
  15. package/lib/Cli/commands/state.js +1 -1
  16. package/lib/Cli/commands/state.js.map +1 -1
  17. package/lib/Cli/commands/tail.js +1 -1
  18. package/lib/Cli/commands/tail.js.map +1 -1
  19. package/lib/Cloudflare/AiGateway/AiGateway.d.ts +112 -1
  20. package/lib/Cloudflare/AiGateway/AiGateway.d.ts.map +1 -1
  21. package/lib/Cloudflare/AiGateway/AiGateway.js +112 -1
  22. package/lib/Cloudflare/AiGateway/AiGateway.js.map +1 -1
  23. package/lib/Cloudflare/AiGateway/AiGatewayBinding.d.ts +43 -10
  24. package/lib/Cloudflare/AiGateway/AiGatewayBinding.d.ts.map +1 -1
  25. package/lib/Cloudflare/AiGateway/AiGatewayBinding.js +38 -10
  26. package/lib/Cloudflare/AiGateway/AiGatewayBinding.js.map +1 -1
  27. package/lib/Cloudflare/AiGateway/LanguageModel.d.ts +35 -0
  28. package/lib/Cloudflare/AiGateway/LanguageModel.d.ts.map +1 -0
  29. package/lib/Cloudflare/AiGateway/LanguageModel.js +576 -0
  30. package/lib/Cloudflare/AiGateway/LanguageModel.js.map +1 -0
  31. package/lib/Cloudflare/AiGateway/index.d.ts +1 -0
  32. package/lib/Cloudflare/AiGateway/index.d.ts.map +1 -1
  33. package/lib/Cloudflare/AiGateway/index.js +1 -0
  34. package/lib/Cloudflare/AiGateway/index.js.map +1 -1
  35. package/lib/Cloudflare/StateStore/State.d.ts.map +1 -1
  36. package/lib/Cloudflare/StateStore/State.js +82 -78
  37. package/lib/Cloudflare/StateStore/State.js.map +1 -1
  38. package/lib/Cloudflare/Workers/DurableObjectBridge.d.ts.map +1 -1
  39. package/lib/Cloudflare/Workers/DurableObjectBridge.js +10 -1
  40. package/lib/Cloudflare/Workers/DurableObjectBridge.js.map +1 -1
  41. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  42. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +13 -1
  43. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  44. package/lib/Cloudflare/Workers/Rpc.d.ts.map +1 -1
  45. package/lib/Cloudflare/Workers/Rpc.js +20 -2
  46. package/lib/Cloudflare/Workers/Rpc.js.map +1 -1
  47. package/lib/Cloudflare/Workers/WorkerBridge.d.ts.map +1 -1
  48. package/lib/Cloudflare/Workers/WorkerBridge.js +20 -2
  49. package/lib/Cloudflare/Workers/WorkerBridge.js.map +1 -1
  50. package/lib/Output.js +2 -2
  51. package/lib/Output.js.map +1 -1
  52. package/lib/Plan.d.ts +2 -2
  53. package/lib/Plan.d.ts.map +1 -1
  54. package/lib/Plan.js +3 -3
  55. package/lib/Plan.js.map +1 -1
  56. package/lib/Stack.d.ts.map +1 -1
  57. package/lib/Stack.js +6 -0
  58. package/lib/Stack.js.map +1 -1
  59. package/lib/State/InMemoryState.d.ts +2 -1
  60. package/lib/State/InMemoryState.d.ts.map +1 -1
  61. package/lib/State/InMemoryState.js +3 -3
  62. package/lib/State/InMemoryState.js.map +1 -1
  63. package/lib/State/LocalState.d.ts.map +1 -1
  64. package/lib/State/LocalState.js +5 -1
  65. package/lib/State/LocalState.js.map +1 -1
  66. package/lib/State/State.d.ts +2 -2
  67. package/lib/State/State.d.ts.map +1 -1
  68. package/lib/State/State.js.map +1 -1
  69. package/lib/tsconfig.test.tsbuildinfo +1 -1
  70. package/package.json +1 -1
  71. package/src/Apply.ts +2 -2
  72. package/src/Cli/commands/_shared.ts +7 -1
  73. package/src/Cli/commands/login.ts +69 -60
  74. package/src/Cli/commands/logs.ts +1 -1
  75. package/src/Cli/commands/state.ts +1 -1
  76. package/src/Cli/commands/tail.ts +1 -1
  77. package/src/Cloudflare/AiGateway/AiGateway.ts +112 -1
  78. package/src/Cloudflare/AiGateway/AiGatewayBinding.ts +52 -10
  79. package/src/Cloudflare/AiGateway/LanguageModel.ts +907 -0
  80. package/src/Cloudflare/AiGateway/index.ts +1 -0
  81. package/src/Cloudflare/StateStore/State.ts +113 -107
  82. package/src/Cloudflare/Workers/DurableObjectBridge.ts +10 -1
  83. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +13 -1
  84. package/src/Cloudflare/Workers/Rpc.ts +38 -9
  85. package/src/Cloudflare/Workers/WorkerBridge.ts +20 -2
  86. package/src/Output.ts +2 -2
  87. package/src/Plan.ts +4 -4
  88. package/src/Stack.ts +6 -0
  89. package/src/State/InMemoryState.ts +87 -85
  90. package/src/State/LocalState.ts +15 -1
  91. package/src/State/State.ts +5 -4
@@ -1,2 +1,3 @@
1
1
  export * from "./AiGateway.ts";
2
2
  export * from "./AiGatewayBinding.ts";
3
+ export * from "./LanguageModel.ts";
@@ -57,124 +57,130 @@ export const state = () =>
57
57
  const profileName = yield* ALCHEMY_PROFILE;
58
58
  const localStage = `${profileName}_${scriptName}`;
59
59
  const credStore = yield* CredentialsStore;
60
- if (yield* hasLocalStack(localStage)) {
61
- // if there's still a local stack, then we need to finish the bootstrap
62
- // TODO(sam): what if the local stack was
63
- return yield* deployWithLocalState({
64
- scriptName,
65
- profileName,
66
- isCI,
67
- force: false,
68
- });
69
- }
70
-
71
- const ensureLatest = ({
72
- url,
73
- authToken,
74
- }: {
75
- url: string;
76
- authToken: string;
77
- }) =>
78
- Effect.gen(function* () {
79
- const { matches, expected, observed } =
80
- yield* checkStateStoreVersion(url);
81
-
82
- if (observed === undefined) {
83
- const shouldDeploy = yield* Clank.confirm({
84
- message: `Cloudflare State Store '${scriptName}' is not available. Do you want to deploy it?`,
85
- });
86
- if (shouldDeploy) {
87
- return yield* bootstrap({
88
- workerName: scriptName,
89
- profile: profileName,
90
- });
91
- } else {
92
- return yield* Effect.die(new Clank.PromptCancelled());
93
- }
94
- }
60
+ const context = yield* Effect.context<Effect.Services<typeof init>>();
61
+
62
+ const init = Effect.gen(function* () {
63
+ if (yield* hasLocalStack(localStage)) {
64
+ // if there's still a local stack, then we need to finish the bootstrap
65
+ // TODO(sam): what if the local stack was
66
+ return yield* deployWithLocalState({
67
+ scriptName,
68
+ profileName,
69
+ isCI,
70
+ force: false,
71
+ });
72
+ }
95
73
 
96
- const httpState = yield* ensureAccess({ url, authToken });
97
- if (matches) {
98
- return httpState;
99
- } else if (isCI) {
100
- return yield* Effect.die(
101
- new AuthError({
102
- message: `Cloudflare State store not found. Run 'alchemy bootstrap cloudflare --profile <your-ci-profile>' to deploy it first.`,
103
- }),
104
- );
105
- } else {
106
- const shouldDeploy = yield* Clank.confirm({
107
- message:
108
- `Cloudflare State Store '${scriptName}' is out of date ` +
109
- `(expected v${expected}, observed v${observed ?? "unknown"})`,
110
- });
111
- if (shouldDeploy) {
112
- const stateStoreOptions = yield* deployStateStore({
113
- stage: scriptName,
114
- state: httpState,
115
- force: false,
74
+ const ensureLatest = ({
75
+ url,
76
+ authToken,
77
+ }: {
78
+ url: string;
79
+ authToken: string;
80
+ }) =>
81
+ Effect.gen(function* () {
82
+ const { matches, expected, observed } =
83
+ yield* checkStateStoreVersion(url);
84
+
85
+ if (observed === undefined) {
86
+ const shouldDeploy = yield* Clank.confirm({
87
+ message: `Cloudflare State Store '${scriptName}' is not available. Do you want to deploy it?`,
116
88
  });
117
- return yield* makeCloudflareStateStore(stateStoreOptions);
118
- } else {
119
- return yield* Effect.die(new Clank.PromptCancelled());
89
+ if (shouldDeploy) {
90
+ return yield* bootstrap({
91
+ workerName: scriptName,
92
+ profile: profileName,
93
+ });
94
+ } else {
95
+ return yield* Effect.die(new Clank.PromptCancelled());
96
+ }
120
97
  }
121
- }
122
- });
123
98
 
124
- const ensureAccess = (credentials: HttpStateStoreCredentials) =>
125
- Effect.gen(function* () {
126
- const isAuth = yield* checkHttpStateStoreAuth(credentials);
127
- if (!isAuth) {
128
- // our token is wrong, force a refresh
129
- yield* Clank.info(
130
- `Cloudflare State store authentication failed, refreshing credentials...`,
131
- );
132
- const credentials = yield* loginWithCloudflare(profileName, true);
133
- if (!(yield* checkHttpStateStoreAuth(credentials))) {
99
+ const httpState = yield* ensureAccess({ url, authToken });
100
+ if (matches) {
101
+ return httpState;
102
+ } else if (isCI) {
134
103
  return yield* Effect.die(
135
104
  new AuthError({
136
- message: `Cloudflare State store authentication failed, after refreshing credentials.`,
105
+ message: `Cloudflare State store not found. Run 'alchemy bootstrap cloudflare --profile <your-ci-profile>' to deploy it first.`,
137
106
  }),
138
107
  );
108
+ } else {
109
+ const shouldDeploy = yield* Clank.confirm({
110
+ message:
111
+ `Cloudflare State Store '${scriptName}' is out of date ` +
112
+ `(expected v${expected}, observed v${observed ?? "unknown"})`,
113
+ });
114
+ if (shouldDeploy) {
115
+ const stateStoreOptions = yield* deployStateStore({
116
+ stage: scriptName,
117
+ state: httpState,
118
+ force: false,
119
+ });
120
+ return yield* makeCloudflareStateStore(stateStoreOptions);
121
+ } else {
122
+ return yield* Effect.die(new Clank.PromptCancelled());
123
+ }
124
+ }
125
+ });
126
+
127
+ const ensureAccess = (credentials: HttpStateStoreCredentials) =>
128
+ Effect.gen(function* () {
129
+ const isAuth = yield* checkHttpStateStoreAuth(credentials);
130
+ if (!isAuth) {
131
+ // our token is wrong, force a refresh
132
+ yield* Clank.info(
133
+ `Cloudflare State store authentication failed, refreshing credentials...`,
134
+ );
135
+ const credentials = yield* loginWithCloudflare(profileName, true);
136
+ if (!(yield* checkHttpStateStoreAuth(credentials))) {
137
+ return yield* Effect.die(
138
+ new AuthError({
139
+ message: `Cloudflare State store authentication failed, after refreshing credentials.`,
140
+ }),
141
+ );
142
+ }
143
+ return yield* makeCloudflareStateStore(credentials);
139
144
  }
140
145
  return yield* makeCloudflareStateStore(credentials);
141
- }
142
- return yield* makeCloudflareStateStore(credentials);
143
- });
146
+ });
144
147
 
145
- const credentials = yield* credStore.read<HttpStateStoreCredentials>(
146
- profileName,
147
- CREDENTIALS_FILE,
148
- );
149
- if (credentials) {
150
- return yield* ensureLatest(credentials);
151
- }
152
- const workerExists = yield* isStateStoreAvailable(scriptName);
153
- if (workerExists) {
154
- return yield* ensureLatest(
155
- yield* loginWithCloudflare(profileName, false),
156
- );
157
- } else if (isCI) {
158
- // TODO(sam): do we want to support bootstrapping the state store from CI?
159
- // for now - just die here
160
- return yield* Effect.die(
161
- new AuthError({
162
- message: `Cloudflare State store not found. Run 'alchemy bootstrap cloudflare --profile <your-ci-profile>' to deploy it first.`,
163
- }),
164
- );
165
- } else {
166
- return yield* Clank.confirm({
167
- message:
168
- "Cloudflare State Store not found. Do you want to deploy it?",
169
- }).pipe(
170
- Effect.flatMap((shouldDeploy) =>
171
- shouldDeploy
172
- ? bootstrap()
173
- : Effect.die(new Clank.PromptCancelled()),
174
- ),
148
+ const credentials = yield* credStore.read<HttpStateStoreCredentials>(
149
+ profileName,
150
+ CREDENTIALS_FILE,
175
151
  );
176
- }
177
- }).pipe(recordStateStoreInit, Effect.orDie),
152
+ if (credentials) {
153
+ return yield* ensureLatest(credentials);
154
+ }
155
+ const workerExists = yield* isStateStoreAvailable(scriptName);
156
+ if (workerExists) {
157
+ return yield* ensureLatest(
158
+ yield* loginWithCloudflare(profileName, false),
159
+ );
160
+ } else if (isCI) {
161
+ // TODO(sam): do we want to support bootstrapping the state store from CI?
162
+ // for now - just die here
163
+ return yield* Effect.die(
164
+ new AuthError({
165
+ message: `Cloudflare State store not found. Run 'alchemy bootstrap cloudflare --profile <your-ci-profile>' to deploy it first.`,
166
+ }),
167
+ );
168
+ } else {
169
+ return yield* Clank.confirm({
170
+ message:
171
+ "Cloudflare State Store not found. Do you want to deploy it?",
172
+ }).pipe(
173
+ Effect.flatMap((shouldDeploy) =>
174
+ shouldDeploy
175
+ ? bootstrap()
176
+ : Effect.die(new Clank.PromptCancelled()),
177
+ ),
178
+ );
179
+ }
180
+ }).pipe(recordStateStoreInit, Effect.orDie);
181
+
182
+ return yield* Effect.cached(init.pipe(Effect.provideContext(context)));
183
+ }),
178
184
  ).pipe(
179
185
  Layer.provideMerge(Credentials.fromAuthProvider()),
180
186
  Layer.provideMerge(CloudflareEnvironment.fromProfile()),
@@ -311,7 +317,7 @@ const deployStateStore = ({
311
317
  Effect.gen(function* () {
312
318
  yield* annotateAccountHash();
313
319
  // deploy it with local state (which we will then hoist into the Cloudflare state store)
314
- const stateLayer = Layer.succeed(State, state);
320
+ const stateLayer = Layer.succeed(State, Effect.succeed(state));
315
321
  const { url, authToken } = yield* deploy({
316
322
  // use the script name as the stage name (so the user can have multiple state stores)
317
323
  stage,
@@ -6,6 +6,7 @@ import * as Effect from "effect/Effect";
6
6
  import * as Exit from "effect/Exit";
7
7
  import * as Layer from "effect/Layer";
8
8
  import * as Scope from "effect/Scope";
9
+ import * as Stream from "effect/Stream";
9
10
 
10
11
  import { HttpServerResponse } from "effect/unstable/http";
11
12
  import type {
@@ -89,7 +90,15 @@ export const makeDurableObjectBridge =
89
90
  this.#execute((instance) => {
90
91
  const method = instance[prop as keyof DurableObjectShape];
91
92
  if (typeof method === "function") {
92
- return (method as any)(...args);
93
+ const result = (method as any)(...args);
94
+ // A streaming RPC method returns a `Stream` directly rather
95
+ // than an `Effect`. Lift it into the success channel so the
96
+ // resulting `Exit.value` is the `Stream` and `handleRpcExit`
97
+ // can encode it as a stream envelope instead of trying to run
98
+ // it as an effect.
99
+ return Stream.isStream(result)
100
+ ? Effect.succeed(result)
101
+ : result;
93
102
  } else if (Effect.isEffect(method)) {
94
103
  return method;
95
104
  } else {
@@ -232,7 +232,19 @@ export const LocalWorkerProvider = () =>
232
232
  }) {
233
233
  const name = yield* createWorkerName(id, props.name);
234
234
  const compatibility = getCompatibility(props);
235
- const workerBindings: BindingHook<BindingServices>[] = [];
235
+ const workerBindings: BindingHook<BindingServices>[] = [
236
+ Text.local("ALCHEMY_PHASE", "runtime"),
237
+ Text.local("ALCHEMY_STACK_NAME", stack.name),
238
+ Text.local("ALCHEMY_STAGE", stack.stage),
239
+ ...Object.entries(props.env ?? {}).map(([key, value]) => {
240
+ const unredacted = Redacted.isRedacted(value)
241
+ ? Redacted.value(value)
242
+ : value;
243
+ return typeof unredacted === "string"
244
+ ? Text.local(key, unredacted)
245
+ : Json.local(key, unredacted);
246
+ }),
247
+ ];
236
248
  const durableObjectNamespaces: Record<string, string> = {};
237
249
  const hyperdrives: Record<string, Required<HyperdriveOrigin>> = {};
238
250
  for (const { data } of bindings) {
@@ -223,19 +223,48 @@ export const makeRpcStub = <Shape>(
223
223
  return target[prop];
224
224
  }
225
225
  return (...args: any[]) =>
226
- Effect.gen(function* () {
227
- const stub = isLazy
228
- ? yield* stubSource as Effect.Effect<any>
229
- : stubSource;
230
- return yield* Effect.tryPromise({
231
- try: () => (stub as any)[prop](...args),
232
- catch: (cause) => new RpcCallError({ method: String(prop), cause }),
233
- }).pipe(Effect.flatMap(decodeRpcResult));
234
- });
226
+ asEffectOrStream(
227
+ Effect.gen(function* () {
228
+ const stub = isLazy
229
+ ? yield* stubSource as Effect.Effect<any>
230
+ : stubSource;
231
+ return yield* Effect.tryPromise({
232
+ try: () => (stub as any)[prop](...args),
233
+ catch: (cause) =>
234
+ new RpcCallError({ method: String(prop), cause }),
235
+ }).pipe(Effect.flatMap(decodeRpcResult));
236
+ }),
237
+ );
235
238
  },
236
239
  }) as Shape;
237
240
  };
238
241
 
242
+ // Effect's internal Stream brand. `Stream.isStream` recognises a value by
243
+ // this property and reads its `channel`. A `makeRpcStub` method can't know
244
+ // synchronously whether the remote method returns a value or a `Stream`
245
+ // (the call is async), yet its declared type mirrors the DO `Shape`
246
+ // verbatim — value methods are typed `Effect<A>`, streaming methods `Stream<A>`.
247
+ // We satisfy BOTH by handing back the call `Effect` augmented with the Stream
248
+ // brand + channel, so the single return value can be `yield*`-ed / `.pipe`d as
249
+ // an Effect (value methods, e.g. `stub.put(k, v).pipe(Effect.orDie)`) AND piped
250
+ // through `Stream.*` combinators (streaming methods, e.g.
251
+ // `stub.tick(n).pipe(Stream.map(...))`).
252
+ const StreamTypeId = "~effect/Stream";
253
+
254
+ const asEffectOrStream = (
255
+ call: Effect.Effect<unknown, unknown>,
256
+ ): Effect.Effect<unknown, unknown> => {
257
+ const streamForm = Stream.unwrap(
258
+ Effect.map(call, (value) =>
259
+ Stream.isStream(value) ? value : Stream.succeed(value),
260
+ ),
261
+ );
262
+ return Object.assign(call, {
263
+ [StreamTypeId]: streamForm[StreamTypeId],
264
+ channel: streamForm.channel,
265
+ });
266
+ };
267
+
239
268
  export const toRpcStream = (stream: Stream.Stream<any, any, any>) =>
240
269
  Effect.scoped(
241
270
  Effect.gen(function* () {
@@ -141,8 +141,17 @@ export const makeWorkerBridge = (
141
141
  ),
142
142
  );
143
143
  }
144
+ const result = dispatcher(...args);
145
+ // A streaming RPC method returns a `Stream` directly rather
146
+ // than an `Effect`. Lift it into the success channel so the
147
+ // inner effect resolves to the `Stream` and `handleRpcExit`
148
+ // can encode it as a stream envelope. Anything else is the
149
+ // `Effect` it claims to be (its resolved value may itself be
150
+ // a `Stream`, which `handleRpcExit` also handles).
144
151
  return Effect.succeed([
145
- dispatcher(...args) as Effect.Effect<any>,
152
+ Stream.isStream(result)
153
+ ? Effect.succeed(result)
154
+ : (result as Effect.Effect<any>),
146
155
  Context.empty(),
147
156
  ] as const);
148
157
  }),
@@ -274,7 +283,16 @@ export const makeRpcProxy = (
274
283
  ),
275
284
  );
276
285
  }
277
- return dispatcher(...args) as Effect.Effect<any>;
286
+ const result = dispatcher(...args);
287
+ // A streaming RPC method returns a `Stream` directly rather than
288
+ // an `Effect`. Lift it into the success channel so the resulting
289
+ // `Exit.value` is the `Stream` and `handleRpcExit` can encode it
290
+ // as a stream envelope. Anything else is the `Effect` it claims
291
+ // to be and is run normally (its resolved value may itself be a
292
+ // `Stream`, which `handleRpcExit` also handles).
293
+ return Stream.isStream(result)
294
+ ? Effect.succeed(result)
295
+ : (result as Effect.Effect<any>);
278
296
  }),
279
297
  processEvent,
280
298
  )
package/src/Output.ts CHANGED
@@ -549,7 +549,7 @@ export const evaluate: <A, Req = never>(
549
549
  } else if (isNamedExpr(expr)) {
550
550
  return yield* evaluate(expr.expr, upstream);
551
551
  } else if (isRefExpr(expr)) {
552
- const state = yield* State.State;
552
+ const state = yield* yield* State.State;
553
553
  const stack = expr.stack ?? (yield* Stack).name;
554
554
  const stage = expr.stage ?? (yield* Stage);
555
555
  const resource = yield* state.get({
@@ -572,7 +572,7 @@ export const evaluate: <A, Req = never>(
572
572
  // task's output value, otherwise undefined.
573
573
  return (resource as any).attr ?? (resource as any).output;
574
574
  } else if (isStackRefExpr(expr)) {
575
- const state = yield* State.State;
575
+ const state = yield* yield* State.State;
576
576
  const stack = expr.stack;
577
577
  const stage = expr.stage ?? (yield* Stage);
578
578
  const output = yield* state.getOutput({ stack, stage });
package/src/Plan.ts CHANGED
@@ -4,6 +4,7 @@ import * as Effect from "effect/Effect";
4
4
  import * as Option from "effect/Option";
5
5
  import * as Redacted from "effect/Redacted";
6
6
  import { asEffect } from ".//Util/types.ts";
7
+ import { isAction, type ActionLike } from "./Action.ts";
7
8
  import {
8
9
  AdoptPolicy,
9
10
  OwnedBySomeoneElse,
@@ -41,19 +42,18 @@ import {
41
42
  } from "./Resource.ts";
42
43
  import { type StackSpec } from "./Stack.ts";
43
44
  import {
45
+ isActionState,
44
46
  State,
47
+ type ActionState,
45
48
  type CreatedResourceState,
46
49
  type CreatingResourceState,
47
- isActionState,
48
50
  type RanActionState,
49
51
  type ReplacedResourceState,
50
52
  type ReplacingResourceState,
51
53
  type ResourceState,
52
- type ActionState,
53
54
  type UpdatedResourceState,
54
55
  type UpdatingReourceState,
55
56
  } from "./State/index.ts";
56
- import { isAction, type ActionLike } from "./Action.ts";
57
57
  import { hashInput } from "./Util/hash.ts";
58
58
  import { findCycleMembers } from "./Util/scc.ts";
59
59
 
@@ -236,7 +236,7 @@ export const make = <A>(
236
236
  ): Effect.Effect<Plan<A>, never, State> =>
237
237
  // @ts-expect-error
238
238
  Effect.gen(function* () {
239
- const state = yield* State;
239
+ const state = yield* yield* State;
240
240
 
241
241
  const resources = Object.values(stack.resources);
242
242
  const actions = Object.values(stack.actions ?? {});
package/src/Stack.ts CHANGED
@@ -125,7 +125,10 @@ export const Stack: Context.ServiceClass<
125
125
  // by default, reference the stack at the "current" stage of the importer
126
126
  Output.stackRef<A>(stackName).pipe(effectClass),
127
127
  {
128
+ stackName,
128
129
  stage: createStageProxy(stackName),
130
+ state: options?.state,
131
+ providers: options?.providers,
129
132
  make: <Req = never>(
130
133
  options: StackProps<NoInfer<Req>>,
131
134
  eff: Effect.Effect<A, never, Req>,
@@ -140,7 +143,10 @@ export const Stack: Context.ServiceClass<
140
143
  }),
141
144
  (eff) =>
142
145
  Object.assign(eff, {
146
+ stackName,
143
147
  stage: createStageProxy(stackName),
148
+ state: options?.state,
149
+ providers: options?.providers,
144
150
  }),
145
151
  );
146
152
  },
@@ -18,8 +18,8 @@ export const inMemoryState = (
18
18
  ) =>
19
19
  Layer.effect(
20
20
  State,
21
- Effect.succeed(InMemoryService(initialState, initialOutputs)).pipe(
22
- recordStateStoreInit,
21
+ Effect.cached(
22
+ InMemoryService(initialState, initialOutputs).pipe(recordStateStoreInit),
23
23
  ),
24
24
  );
25
25
 
@@ -27,87 +27,89 @@ export const InMemoryService = (
27
27
  state: Record<StackId, Record<StageId, Record<Fqn, ResourceState>>> = {},
28
28
  outputs: Record<StackId, Record<StageId, unknown>> = {},
29
29
  ) =>
30
- State.of({
31
- id: "inmemory",
32
- getVersion: () => Effect.succeed(STATE_STORE_VERSION),
33
- listStacks: () => Effect.succeed(Array.from(Object.keys(state))),
34
- listStages: (stack: string) =>
35
- Effect.succeed(
36
- Array.from(stack in state ? Object.keys(state[stack]) : []),
37
- ),
38
- get: ({
39
- stack,
40
- stage,
41
- fqn,
42
- }: {
43
- stack: string;
44
- stage: string;
45
- fqn: string;
46
- }) => Effect.succeed(state[stack]?.[stage]?.[fqn]),
47
- getReplacedResources: ({
48
- stack,
49
- stage,
50
- }: {
51
- stack: string;
52
- stage: string;
53
- }) =>
54
- Effect.succeed(
55
- Array.from(Object.values(state[stack]?.[stage] ?? {}) ?? []).filter(
56
- (s) => s.status === "replaced",
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]) : []),
57
38
  ),
58
- ),
59
- set: <V extends PersistedState>({
60
- stack,
61
- stage,
62
- fqn,
63
- value,
64
- }: {
65
- stack: string;
66
- stage: string;
67
- fqn: string;
68
- value: V;
69
- }) =>
70
- Effect.sync(() => {
71
- const stackState = (state[stack] ??= {});
72
- const stageState = (stackState[stage] ??= {});
73
- stageState[fqn] = value as ResourceState;
74
- return value;
75
- }),
76
- delete: ({
77
- stack,
78
- stage,
79
- fqn,
80
- }: {
81
- stack: string;
82
- stage: string;
83
- fqn: string;
84
- }) => Effect.sync(() => delete state[stack]?.[stage]?.[fqn]),
85
- deleteStack: ({ stack, stage }: { stack: string; stage?: string }) =>
86
- Effect.sync(() => {
87
- if (stage === undefined) {
88
- delete state[stack];
89
- } else {
90
- delete state[stack]?.[stage];
91
- }
92
- }),
93
- list: ({ stack, stage }: { stack: string; stage: string }) =>
94
- Effect.succeed(
95
- Array.from(Object.keys(state[stack]?.[stage] ?? {}) ?? []),
96
- ),
97
- getOutput: ({ stack, stage }: { stack: string; stage: string }) =>
98
- Effect.succeed(outputs[stack]?.[stage]),
99
- setOutput: ({
100
- stack,
101
- stage,
102
- value,
103
- }: {
104
- stack: string;
105
- stage: string;
106
- value: unknown;
107
- }) =>
108
- Effect.sync(() => {
109
- const stackOutputs = (outputs[stack] ??= {});
110
- stackOutputs[stage] = value;
111
- return value;
112
- }),
113
- });
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
+ } else {
91
+ delete state[stack]?.[stage];
92
+ }
93
+ }),
94
+ list: ({ stack, stage }: { stack: string; stage: string }) =>
95
+ Effect.succeed(
96
+ Array.from(Object.keys(state[stack]?.[stage] ?? {}) ?? []),
97
+ ),
98
+ getOutput: ({ stack, stage }: { stack: string; stage: string }) =>
99
+ Effect.succeed(outputs[stack]?.[stage]),
100
+ setOutput: ({
101
+ stack,
102
+ stage,
103
+ value,
104
+ }: {
105
+ stack: string;
106
+ stage: string;
107
+ value: unknown;
108
+ }) =>
109
+ Effect.sync(() => {
110
+ const stackOutputs = (outputs[stack] ??= {});
111
+ stackOutputs[stage] = value;
112
+ return value;
113
+ }),
114
+ }),
115
+ );