alchemy 2.0.0-beta.35 → 2.0.0-beta.37

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 (222) hide show
  1. package/bin/exec.js +176 -26
  2. package/bin/exec.js.map +1 -1
  3. package/lib/AWS/Lambda/Function.d.ts.map +1 -1
  4. package/lib/AWS/Lambda/Function.js +22 -2
  5. package/lib/AWS/Lambda/Function.js.map +1 -1
  6. package/lib/Apply.d.ts.map +1 -1
  7. package/lib/Apply.js +6 -1
  8. package/lib/Apply.js.map +1 -1
  9. package/lib/Cli/main.d.ts.map +1 -1
  10. package/lib/Cli/main.js +2 -0
  11. package/lib/Cli/main.js.map +1 -1
  12. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.d.ts +54 -0
  13. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.d.ts.map +1 -0
  14. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.js +17 -0
  15. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.js.map +1 -0
  16. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.d.ts +37 -0
  17. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.d.ts.map +1 -0
  18. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.js +45 -0
  19. package/lib/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.js.map +1 -0
  20. package/lib/Cloudflare/AnalyticsEngine/index.d.ts +3 -0
  21. package/lib/Cloudflare/AnalyticsEngine/index.d.ts.map +1 -0
  22. package/lib/Cloudflare/AnalyticsEngine/index.js +3 -0
  23. package/lib/Cloudflare/AnalyticsEngine/index.js.map +1 -0
  24. package/lib/Cloudflare/D1/D1Connection.d.ts +47 -3
  25. package/lib/Cloudflare/D1/D1Connection.d.ts.map +1 -1
  26. package/lib/Cloudflare/D1/D1Connection.js +62 -5
  27. package/lib/Cloudflare/D1/D1Connection.js.map +1 -1
  28. package/lib/Cloudflare/Images/Images.d.ts +103 -0
  29. package/lib/Cloudflare/Images/Images.d.ts.map +1 -0
  30. package/lib/Cloudflare/Images/Images.js +82 -0
  31. package/lib/Cloudflare/Images/Images.js.map +1 -0
  32. package/lib/Cloudflare/Images/ImagesBinding.d.ts +73 -0
  33. package/lib/Cloudflare/Images/ImagesBinding.d.ts.map +1 -0
  34. package/lib/Cloudflare/Images/ImagesBinding.js +83 -0
  35. package/lib/Cloudflare/Images/ImagesBinding.js.map +1 -0
  36. package/lib/Cloudflare/Images/index.d.ts +3 -0
  37. package/lib/Cloudflare/Images/index.d.ts.map +1 -0
  38. package/lib/Cloudflare/Images/index.js +3 -0
  39. package/lib/Cloudflare/Images/index.js.map +1 -0
  40. package/lib/Cloudflare/Local/SidecarHandlers.js +9 -1
  41. package/lib/Cloudflare/Local/SidecarHandlers.js.map +1 -1
  42. package/lib/Cloudflare/Providers.d.ts.map +1 -1
  43. package/lib/Cloudflare/Providers.js +6 -1
  44. package/lib/Cloudflare/Providers.js.map +1 -1
  45. package/lib/Cloudflare/R2/R2Bucket.d.ts +135 -0
  46. package/lib/Cloudflare/R2/R2Bucket.d.ts.map +1 -1
  47. package/lib/Cloudflare/R2/R2Bucket.js +146 -7
  48. package/lib/Cloudflare/R2/R2Bucket.js.map +1 -1
  49. package/lib/Cloudflare/StateStore/Api.d.ts +6 -1
  50. package/lib/Cloudflare/StateStore/Api.d.ts.map +1 -1
  51. package/lib/Cloudflare/StateStore/Api.js +23 -1
  52. package/lib/Cloudflare/StateStore/Api.js.map +1 -1
  53. package/lib/Cloudflare/StateStore/Store.d.ts +16 -1
  54. package/lib/Cloudflare/StateStore/Store.d.ts.map +1 -1
  55. package/lib/Cloudflare/StateStore/Store.js +22 -5
  56. package/lib/Cloudflare/StateStore/Store.js.map +1 -1
  57. package/lib/Cloudflare/Workers/CronEventSource.d.ts +35 -0
  58. package/lib/Cloudflare/Workers/CronEventSource.d.ts.map +1 -0
  59. package/lib/Cloudflare/Workers/CronEventSource.js +55 -0
  60. package/lib/Cloudflare/Workers/CronEventSource.js.map +1 -0
  61. package/lib/Cloudflare/Workers/InferEnv.d.ts +37 -2
  62. package/lib/Cloudflare/Workers/InferEnv.d.ts.map +1 -1
  63. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  64. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +2 -1
  65. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  66. package/lib/Cloudflare/Workers/Rpc.d.ts +76 -3
  67. package/lib/Cloudflare/Workers/Rpc.d.ts.map +1 -1
  68. package/lib/Cloudflare/Workers/Rpc.js +197 -13
  69. package/lib/Cloudflare/Workers/Rpc.js.map +1 -1
  70. package/lib/Cloudflare/Workers/Worker.d.ts +26 -6
  71. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  72. package/lib/Cloudflare/Workers/Worker.js +183 -41
  73. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  74. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
  75. package/lib/Cloudflare/Workers/WorkerBundle.js +15 -5
  76. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
  77. package/lib/Cloudflare/Workers/Workflow.d.ts +50 -47
  78. package/lib/Cloudflare/Workers/Workflow.d.ts.map +1 -1
  79. package/lib/Cloudflare/Workers/Workflow.js +41 -35
  80. package/lib/Cloudflare/Workers/Workflow.js.map +1 -1
  81. package/lib/Cloudflare/Workers/index.d.ts +1 -0
  82. package/lib/Cloudflare/Workers/index.d.ts.map +1 -1
  83. package/lib/Cloudflare/Workers/index.js +1 -0
  84. package/lib/Cloudflare/Workers/index.js.map +1 -1
  85. package/lib/Cloudflare/index.d.ts +2 -0
  86. package/lib/Cloudflare/index.d.ts.map +1 -1
  87. package/lib/Cloudflare/index.js +2 -0
  88. package/lib/Cloudflare/index.js.map +1 -1
  89. package/lib/Deploy.d.ts +4 -1
  90. package/lib/Deploy.d.ts.map +1 -1
  91. package/lib/Deploy.js +2 -2
  92. package/lib/Deploy.js.map +1 -1
  93. package/lib/Destroy.d.ts +4 -1
  94. package/lib/Destroy.d.ts.map +1 -1
  95. package/lib/Destroy.js +2 -2
  96. package/lib/Destroy.js.map +1 -1
  97. package/lib/Output.d.ts +45 -5
  98. package/lib/Output.d.ts.map +1 -1
  99. package/lib/Output.js +100 -4
  100. package/lib/Output.js.map +1 -1
  101. package/lib/Plan.d.ts.map +1 -1
  102. package/lib/Plan.js +93 -12
  103. package/lib/Plan.js.map +1 -1
  104. package/lib/Platform.d.ts.map +1 -1
  105. package/lib/Platform.js +16 -3
  106. package/lib/Platform.js.map +1 -1
  107. package/lib/Ref.d.ts +4 -1
  108. package/lib/Ref.d.ts.map +1 -1
  109. package/lib/Ref.js +1 -1
  110. package/lib/Ref.js.map +1 -1
  111. package/lib/Resource.d.ts +8 -0
  112. package/lib/Resource.d.ts.map +1 -1
  113. package/lib/Resource.js +10 -0
  114. package/lib/Resource.js.map +1 -1
  115. package/lib/RuntimeContext.d.ts +3 -1
  116. package/lib/RuntimeContext.d.ts.map +1 -1
  117. package/lib/RuntimeContext.js.map +1 -1
  118. package/lib/Secret.d.ts +50 -0
  119. package/lib/Secret.d.ts.map +1 -0
  120. package/lib/Secret.js +23 -0
  121. package/lib/Secret.js.map +1 -0
  122. package/lib/Serverless/Function.d.ts +9 -1
  123. package/lib/Serverless/Function.d.ts.map +1 -1
  124. package/lib/Stack.d.ts +14 -5
  125. package/lib/Stack.d.ts.map +1 -1
  126. package/lib/Stack.js +31 -19
  127. package/lib/Stack.js.map +1 -1
  128. package/lib/State/HttpStateApi.d.ts +15 -1
  129. package/lib/State/HttpStateApi.d.ts.map +1 -1
  130. package/lib/State/HttpStateApi.js +11 -0
  131. package/lib/State/HttpStateApi.js.map +1 -1
  132. package/lib/State/HttpStateStore.d.ts.map +1 -1
  133. package/lib/State/HttpStateStore.js +11 -0
  134. package/lib/State/HttpStateStore.js.map +1 -1
  135. package/lib/State/InMemoryState.d.ts +2 -2
  136. package/lib/State/InMemoryState.d.ts.map +1 -1
  137. package/lib/State/InMemoryState.js +12 -6
  138. package/lib/State/InMemoryState.js.map +1 -1
  139. package/lib/State/LocalState.d.ts.map +1 -1
  140. package/lib/State/LocalState.js +11 -2
  141. package/lib/State/LocalState.js.map +1 -1
  142. package/lib/State/State.d.ts +21 -0
  143. package/lib/State/State.d.ts.map +1 -1
  144. package/lib/Test/Bun.d.ts.map +1 -1
  145. package/lib/Test/Bun.js +30 -7
  146. package/lib/Test/Bun.js.map +1 -1
  147. package/lib/Test/Core.d.ts +17 -2
  148. package/lib/Test/Core.d.ts.map +1 -1
  149. package/lib/Test/Core.js +29 -11
  150. package/lib/Test/Core.js.map +1 -1
  151. package/lib/Test/Vitest.d.ts.map +1 -1
  152. package/lib/Test/Vitest.js +26 -5
  153. package/lib/Test/Vitest.js.map +1 -1
  154. package/lib/Util/LocalhostDns.d.ts +23 -0
  155. package/lib/Util/LocalhostDns.d.ts.map +1 -0
  156. package/lib/Util/LocalhostDns.js +46 -0
  157. package/lib/Util/LocalhostDns.js.map +1 -0
  158. package/lib/Util/PlatformServices.d.ts.map +1 -1
  159. package/lib/Util/PlatformServices.js +2 -0
  160. package/lib/Util/PlatformServices.js.map +1 -1
  161. package/lib/Util/effect.d.ts +7 -0
  162. package/lib/Util/effect.d.ts.map +1 -1
  163. package/lib/Util/effect.js +6 -0
  164. package/lib/Util/effect.js.map +1 -1
  165. package/lib/Variable.d.ts +29 -0
  166. package/lib/Variable.d.ts.map +1 -0
  167. package/lib/Variable.js +9 -0
  168. package/lib/Variable.js.map +1 -0
  169. package/lib/index.d.ts +3 -0
  170. package/lib/index.d.ts.map +1 -1
  171. package/lib/index.js +3 -0
  172. package/lib/index.js.map +1 -1
  173. package/lib/tsconfig.test.tsbuildinfo +1 -1
  174. package/package.json +7 -7
  175. package/src/AWS/Lambda/Function.ts +30 -2
  176. package/src/Apply.ts +8 -1
  177. package/src/Cli/main.ts +3 -0
  178. package/src/Cloudflare/AnalyticsEngine/AnalyticsEngineDataset.ts +82 -0
  179. package/src/Cloudflare/AnalyticsEngine/AnalyticsEngineDatasetBinding.ts +106 -0
  180. package/src/Cloudflare/AnalyticsEngine/index.ts +2 -0
  181. package/src/Cloudflare/D1/D1Connection.ts +95 -12
  182. package/src/Cloudflare/Images/Images.ts +121 -0
  183. package/src/Cloudflare/Images/ImagesBinding.ts +192 -0
  184. package/src/Cloudflare/Images/index.ts +2 -0
  185. package/src/Cloudflare/Local/SidecarHandlers.ts +9 -1
  186. package/src/Cloudflare/Providers.ts +8 -0
  187. package/src/Cloudflare/R2/R2Bucket.ts +271 -7
  188. package/src/Cloudflare/StateStore/Api.ts +34 -1
  189. package/src/Cloudflare/StateStore/Store.ts +40 -10
  190. package/src/Cloudflare/Workers/CronEventSource.ts +89 -0
  191. package/src/Cloudflare/Workers/InferEnv.ts +94 -16
  192. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +9 -1
  193. package/src/Cloudflare/Workers/Rpc.ts +227 -18
  194. package/src/Cloudflare/Workers/Worker.ts +295 -62
  195. package/src/Cloudflare/Workers/WorkerBundle.ts +15 -5
  196. package/src/Cloudflare/Workers/Workflow.ts +74 -70
  197. package/src/Cloudflare/Workers/index.ts +1 -0
  198. package/src/Cloudflare/index.ts +2 -0
  199. package/src/Deploy.ts +5 -1
  200. package/src/Destroy.ts +5 -1
  201. package/src/Output.ts +121 -12
  202. package/src/Plan.ts +101 -12
  203. package/src/Platform.ts +19 -4
  204. package/src/Ref.ts +10 -5
  205. package/src/Resource.ts +22 -0
  206. package/src/RuntimeContext.ts +1 -0
  207. package/src/Secret.ts +87 -0
  208. package/src/Serverless/Function.ts +10 -1
  209. package/src/Stack.ts +52 -29
  210. package/src/State/HttpStateApi.ts +21 -0
  211. package/src/State/HttpStateStore.ts +21 -0
  212. package/src/State/InMemoryState.ts +29 -8
  213. package/src/State/LocalState.ts +28 -4
  214. package/src/State/State.ts +21 -0
  215. package/src/Test/Bun.ts +43 -7
  216. package/src/Test/Core.ts +29 -8
  217. package/src/Test/Vitest.ts +36 -4
  218. package/src/Util/LocalhostDns.ts +52 -0
  219. package/src/Util/PlatformServices.ts +2 -0
  220. package/src/Util/effect.ts +25 -0
  221. package/src/Variable.ts +44 -0
  222. package/src/index.ts +3 -0
package/src/Plan.ts CHANGED
@@ -349,8 +349,53 @@ export const make = <A>(
349
349
  });
350
350
  } else if (Output.isLiteralExpr(expr)) {
351
351
  return expr.value;
352
+ } else if (Output.isRefExpr(expr)) {
353
+ const refStack = expr.stack ?? stackName;
354
+ const refStage = expr.stage ?? stage;
355
+ const refState = yield* state
356
+ .get({
357
+ stack: refStack,
358
+ stage: refStage,
359
+ fqn: expr.resourceId,
360
+ })
361
+ .pipe(Effect.orDie);
362
+ if (!refState) {
363
+ return yield* Effect.die(
364
+ new Output.InvalidReferenceError({
365
+ message: `Reference to '${expr.resourceId}' in stack '${refStack}' and stage '${refStage}' not found. Have you deployed '${refStage}' of '${refStack}'?`,
366
+ stack: refStack,
367
+ stage: refStage,
368
+ resourceId: expr.resourceId,
369
+ }),
370
+ );
371
+ }
372
+ return refState.attr;
373
+ } else if (Output.isStackRefExpr(expr)) {
374
+ const refStack = expr.stack;
375
+ const refStage = expr.stage ?? stage;
376
+ const output = yield* state
377
+ .getOutput({
378
+ stack: refStack,
379
+ stage: refStage,
380
+ })
381
+ .pipe(Effect.orDie);
382
+ if (output == null) {
383
+ return yield* Effect.die(
384
+ new Output.InvalidReferenceError({
385
+ message: `Reference to stack '${refStack}' at stage '${refStage}' not found. Have you deployed stage '${refStage}' of '${refStack}'?`,
386
+ stack: refStack,
387
+ stage: refStage,
388
+ resourceId: refStack,
389
+ }),
390
+ );
391
+ }
392
+ return output;
393
+ } else if (Output.isNamedExpr(expr)) {
394
+ return yield* resolveOutput(expr.expr);
352
395
  }
353
- return yield* Effect.die(new Error("Not implemented yet"));
396
+ return yield* Effect.die(
397
+ new Error("Not implemented yet" + (expr as any).kind),
398
+ );
354
399
  });
355
400
 
356
401
  // map of resource FQN -> its downstream dependencies (resources that depend on it)
@@ -413,16 +458,59 @@ export const make = <A>(
413
458
  }),
414
459
  );
415
460
 
416
- // Map FQN -> list of downstream FQNs (resources that depend on this one)
461
+ // Resources that participate in a cycle when both prop and binding
462
+ // edges are considered. Used below to decide whether an acyclic
463
+ // binding edge should also become a downstream edge.
464
+ const combinedCycleMembers = findCycleMembers(allUpstreamDependencies);
465
+
466
+ // Map FQN -> list of downstream FQNs (resources that depend on this one).
467
+ //
468
+ // Prop edges always become downstream edges — they can't form cycles
469
+ // (the resource graph is a DAG by construction once props are fully
470
+ // resolved). Binding edges become downstream edges too, except when
471
+ // they participate in a cycle in the combined graph: mutual bindings
472
+ // (A binds to B's data, B binds to A's data) intentionally do not
473
+ // create downstream edges, so deletion does not deadlock waiting on
474
+ // each other (see the "binding-only cycles inside a construct" test
475
+ // in `plan.test.ts`).
476
+ //
477
+ // Including acyclic binding edges is required for cloud APIs that
478
+ // enforce the binding at the provider level — e.g. a Cloudflare
479
+ // Worker with a `service` binding to another Worker cannot delete
480
+ // the upstream worker until the downstream worker's binding has been
481
+ // removed. Without the binding edge in `downstream`, the two delete
482
+ // concurrently and the upstream delete fails with
483
+ // `ServiceBindingConflict`.
417
484
  const newDownstreamDependencies: {
418
485
  [fqn: string]: string[];
419
486
  } = Object.fromEntries(
420
- resources.map((resource) => [
421
- resource.FQN,
422
- Object.entries(newUpstreamDependencies)
423
- .filter(([_, upstream]) => upstream.includes(resource.FQN))
424
- .map(([depFqn]) => depFqn),
425
- ]),
487
+ resources.map((resource) => {
488
+ const upFqn = resource.FQN;
489
+ const downstream: string[] = [];
490
+ for (const [downFqn, upstreams] of Object.entries(
491
+ rawUpstreamDependencies,
492
+ )) {
493
+ if (downFqn === upFqn) continue;
494
+ if (!upstreams.includes(upFqn)) continue;
495
+ const isPropEdge = (newUpstreamDependencies[downFqn] ?? []).includes(
496
+ upFqn,
497
+ );
498
+ if (isPropEdge) {
499
+ downstream.push(downFqn);
500
+ continue;
501
+ }
502
+ // Binding-only edge — exclude when both endpoints sit inside
503
+ // the same SCC of the combined graph.
504
+ if (
505
+ combinedCycleMembers.has(upFqn) &&
506
+ combinedCycleMembers.has(downFqn)
507
+ ) {
508
+ continue;
509
+ }
510
+ downstream.push(downFqn);
511
+ }
512
+ return [upFqn, downstream];
513
+ }),
426
514
  );
427
515
 
428
516
  const resourceGraph = Object.fromEntries(
@@ -789,10 +877,11 @@ export const make = <A>(
789
877
  )).map((update) => [update.resource.FQN, update]),
790
878
  ) as Plan["resources"];
791
879
 
792
- // Compute SCC membership once. Apply uses it to decide whether an
793
- // update node must publish its prior attr early to break a cycle, or
794
- // can simply wait for upstreams like a DAG node (the common case).
795
- const cycleMembers = findCycleMembers(allUpstreamDependencies);
880
+ // SCC membership of the combined upstream graph. Apply uses it to
881
+ // decide whether an update node must publish its prior attr early to
882
+ // break a cycle, or can simply wait for upstreams like a DAG node
883
+ // (the common case). Computed once, above, for the downstream graph.
884
+ const cycleMembers = combinedCycleMembers;
796
885
 
797
886
  // Detect unsatisfiable dependency cycles among create/replace nodes.
798
887
  // Update/noop nodes signal their Deferred before waitForDeps when in a
package/src/Platform.ts CHANGED
@@ -242,8 +242,18 @@ export const Platform = <
242
242
  }),
243
243
  )
244
244
  ).pipe(
245
- Effect.tap(
246
- (resource) => hooks.onCreate?.(resource as R, props) ?? Effect.void,
245
+ Effect.tap((resource) =>
246
+ hooks.onCreate
247
+ ? Effect.flatMap(
248
+ // `props` may itself be an Effect (e.g. when wrapped by
249
+ // `Cloudflare.Vite` via `Effect.map`); resolve it before
250
+ // handing it to the hook so `onCreate` always sees the
251
+ // plain props object — the second call site (in
252
+ // `cls.make`) already does this.
253
+ Effect.isEffect(props) ? props : Effect.succeed(props ?? {}),
254
+ (resolved) => hooks.onCreate!(resource as R, resolved),
255
+ )
256
+ : Effect.void,
247
257
  ),
248
258
  );
249
259
  return Object.assign(
@@ -324,8 +334,13 @@ export const Platform = <
324
334
  yield* impl.pipe(
325
335
  Effect.flatMap((impl) =>
326
336
  impl?.fetch
327
- ? (runtimeContext.serve?.(impl.fetch) ??
328
- Effect.die("No serve handler"))
337
+ ? // Hand the full impl to `serve` so the runtime can
338
+ // expose any non-handler methods on the impl shape
339
+ // (e.g. RPC methods on a Cloudflare Worker) alongside
340
+ // the standard `fetch` handler.
341
+ (runtimeContext.serve?.(impl.fetch, {
342
+ shape: impl as Record<string, unknown>,
343
+ }) ?? Effect.die("No serve handler"))
329
344
  : Effect.void,
330
345
  ),
331
346
  Effect.provide(
package/src/Ref.ts CHANGED
@@ -22,11 +22,16 @@ export interface RefMetadata<R extends ResourceLike> {
22
22
  stage?: string;
23
23
  }
24
24
 
25
- export const ref = <R extends ResourceLike>({
26
- stack,
27
- id,
28
- stage,
29
- }: RefMetadata<R>): Ref<R> => {
25
+ export const ref = <R extends ResourceLike>(
26
+ id: string,
27
+ {
28
+ stack,
29
+ stage,
30
+ }: {
31
+ stack?: string;
32
+ stage?: string;
33
+ } = {},
34
+ ): Ref<R> => {
30
35
  const ref = new Proxy(
31
36
  {},
32
37
  {
package/src/Resource.ts CHANGED
@@ -7,6 +7,7 @@ import type { Input, InputProps } from "./Input.ts";
7
7
  import { CurrentNamespace, type NamespaceNode } from "./Namespace.ts";
8
8
  import * as Output from "./Output.ts";
9
9
  import { Provider } from "./Provider.ts";
10
+ import { ref as makeRef } from "./Ref.ts";
10
11
  import { RemovalPolicy } from "./RemovalPolicy.ts";
11
12
  import { Self } from "./Self.ts";
12
13
  import { Stack } from "./Stack.ts";
@@ -47,6 +48,10 @@ export type ResourceClassWithMethods<
47
48
  Effect.Effect<ResourceConstructor<R>> & {
48
49
  Self: Self<R>;
49
50
  Provider: Provider<R>;
51
+ ref(
52
+ id: string,
53
+ options?: { stage?: string; stack?: string },
54
+ ): Effect.Effect<R>;
50
55
  } & Methods;
51
56
 
52
57
  export type ResourceClass<R extends ResourceLike> = ResourceConstructor<
@@ -56,6 +61,10 @@ export type ResourceClass<R extends ResourceLike> = ResourceConstructor<
56
61
  Effect.Effect<ResourceConstructor<R>> & {
57
62
  Self: Self<R>;
58
63
  Provider: Provider<R>;
64
+ ref(
65
+ id: string,
66
+ options?: { stage?: string; stack?: string },
67
+ ): Effect.Effect<R>;
59
68
  };
60
69
 
61
70
  export type LogicalId = string;
@@ -286,6 +295,19 @@ export function Resource<R extends ResourceLike>(
286
295
  constructor(id, props),
287
296
  );
288
297
  },
298
+ /**
299
+ * Build a typed reference to a deployed instance of this resource
300
+ * — in the current stack/stage by default, or in another via
301
+ * `options`. Resolves to the same shape as `yield*
302
+ * MyResource("id", props)` so downstream code can read attributes
303
+ * (`ref.someAttr`) exactly the way it would for a locally-declared
304
+ * resource.
305
+ */
306
+ ref: (
307
+ id: string,
308
+ options?: { stage?: string; stack?: string },
309
+ ): Effect.Effect<R> =>
310
+ Effect.succeed(Output.of(makeRef<R>(id, options)) as unknown as R),
289
311
  Type: type,
290
312
  Provider: ProviderTag,
291
313
  Self: self,
@@ -14,6 +14,7 @@ export interface BaseRuntimeContext {
14
14
  exports?: Effect.Effect<Record<string, any>>;
15
15
  serve?<Req = never>(
16
16
  handler: HttpEffect<Req>,
17
+ options?: { shape?: Record<string, unknown> },
17
18
  ): Effect.Effect<void, never, Req>;
18
19
  }
19
20
 
package/src/Secret.ts ADDED
@@ -0,0 +1,87 @@
1
+ import * as Config from "effect/Config";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Redacted from "effect/Redacted";
4
+ import * as Output from "./Output.ts";
5
+
6
+ /**
7
+ * The shapes a {@link secret} can be derived from.
8
+ *
9
+ * - `string` — wrapped in `Redacted.make` and bound as a literal.
10
+ * - `Redacted<string>` — bound as-is.
11
+ * - `Effect<string | Redacted<string>>` — resolved at deploy time.
12
+ * - `Config<string | Redacted<string>>` — resolved against the active
13
+ * `ConfigProvider` at deploy time.
14
+ */
15
+ export type SecretInput<R = never> =
16
+ | string
17
+ | Redacted.Redacted<string>
18
+ | Effect.Effect<string | Redacted.Redacted<string>, any, R>
19
+ | Config.Config<string | Redacted.Redacted<string>>;
20
+
21
+ /**
22
+ * Bind a secret value into the active {@link RuntimeContext} (e.g. a
23
+ * Cloudflare Worker's `env`) under the given `name` so the resource
24
+ * provider deploys it as a secret (e.g. `secret_text` for Cloudflare).
25
+ *
26
+ * The result is an `Output<Redacted<string>>` — `yield*`-ing it returns
27
+ * an `Accessor<Redacted<string>>` that resolves the value at runtime.
28
+ *
29
+ * @example One-arg shortcut — reads `Config.redacted("API_KEY")`
30
+ * ```ts
31
+ * const apiKey = yield* Alchemy.Secret("API_KEY");
32
+ * ```
33
+ *
34
+ * @example From a literal
35
+ * ```ts
36
+ * const apiKey = yield* Alchemy.Secret("API_KEY", "sk-123");
37
+ * ```
38
+ *
39
+ * @example From an Effect
40
+ * ```ts
41
+ * const apiKey = yield* Alchemy.Secret(
42
+ * "API_KEY",
43
+ * Effect.succeed(Redacted.make("sk-123")),
44
+ * );
45
+ * ```
46
+ *
47
+ * @example From a Config
48
+ * ```ts
49
+ * const apiKey = yield* Alchemy.Secret("API_KEY", Config.redacted("API_KEY"));
50
+ * ```
51
+ */
52
+ export function Secret(
53
+ name: string,
54
+ ): Output.Output<Redacted.Redacted<string>, never>;
55
+ export function Secret<R = never>(
56
+ name: string,
57
+ value: SecretInput<R>,
58
+ ): Output.Output<Redacted.Redacted<string>, R>;
59
+ export function Secret<R = never>(
60
+ name: string,
61
+ value?: SecretInput<R>,
62
+ ): Output.Output<Redacted.Redacted<string>, R> {
63
+ const source = value ?? Config.redacted(name);
64
+ const base = toOutput<string | Redacted.Redacted<string>, R>(source);
65
+ return Output.named(
66
+ base.pipe(
67
+ Output.map((v) => (Redacted.isRedacted(v) ? v : Redacted.make(v))),
68
+ ) as Output.Output<Redacted.Redacted<string>, R>,
69
+ name,
70
+ );
71
+ }
72
+
73
+ /** @internal */
74
+ export const toOutput = <T, R = never>(
75
+ source: T | Effect.Effect<T, any, R> | Config.Config<T> | Output.Output<T, R>,
76
+ ): Output.Output<T, R> => {
77
+ if (Output.isOutput(source)) {
78
+ return source;
79
+ }
80
+ if (Config.isConfig(source)) {
81
+ return Output.asOutput(source.asEffect() as any) as Output.Output<T, R>;
82
+ }
83
+ if (Effect.isEffect(source)) {
84
+ return Output.asOutput(source as any) as Output.Output<T, R>;
85
+ }
86
+ return Output.asOutput(source as T) as Output.Output<T, R>;
87
+ };
@@ -3,7 +3,16 @@ import type { HttpEffect } from "../Http.ts";
3
3
  import type { BaseRuntimeContext } from "../RuntimeContext.ts";
4
4
 
5
5
  export interface FunctionContext extends BaseRuntimeContext {
6
- serve<Req = never>(handler: HttpEffect<Req>): Effect.Effect<void, never, Req>;
6
+ /**
7
+ * Register an HTTP handler. The optional `shape` payload is the user's
8
+ * full default-export shape — Cloudflare Workers use it to expose
9
+ * non-handler methods (e.g. `greet`) as RPC methods on the deployed
10
+ * `WorkerEntrypoint`. Other platforms (Lambda, etc.) ignore it.
11
+ */
12
+ serve<Req = never>(
13
+ handler: HttpEffect<Req>,
14
+ options?: { shape?: Record<string, unknown> },
15
+ ): Effect.Effect<void, never, Req>;
7
16
  listen<A, Req = never>(
8
17
  handler: FunctionListener<A, Req>,
9
18
  ): Effect.Effect<void, never, Req>;
package/src/Stack.ts CHANGED
@@ -6,7 +6,7 @@ import * as Layer from "effect/Layer";
6
6
  import * as Logger from "effect/Logger";
7
7
  import * as Option from "effect/Option";
8
8
  import { Path } from "effect/Path";
9
- import type { Scope } from "effect/Scope";
9
+ import * as Scope from "effect/Scope";
10
10
  import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
11
11
  import type { HttpClient } from "effect/unstable/http/HttpClient";
12
12
  import type { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
@@ -18,19 +18,18 @@ import { Profile, ProfileLive } from "./Auth/Profile.ts";
18
18
  import { Cli } from "./Cli/Cli.ts";
19
19
  import type { Input, InputProps } from "./Input.ts";
20
20
  import * as Output from "./Output.ts";
21
- import { ref } from "./Ref.ts";
22
21
  import type { ResourceBinding, ResourceLike } from "./Resource.ts";
23
22
  import { Stage } from "./Stage.ts";
24
23
  import type { State } from "./State/State.ts";
25
24
  import { loadConfigProvider } from "./Util/ConfigProvider.ts";
26
- import { taggedFunction } from "./Util/effect.ts";
25
+ import { effectClass, taggedFunction } from "./Util/effect.ts";
27
26
  import { fileLogger } from "./Util/FileLogger.ts";
28
27
  import { PlatformServices } from "./Util/PlatformServices.ts";
29
28
 
30
29
  export type StackServices =
31
30
  | Stack
32
31
  | Stage
33
- | Scope
32
+ | Scope.Scope
34
33
  | FileSystem
35
34
  | Path
36
35
  | AlchemyContext
@@ -46,7 +45,7 @@ export type StackEffect<A, Err = never, Req = never> = Effect.Effect<
46
45
  Err,
47
46
  | PlatformServices
48
47
  | HttpClient
49
- | Scope
48
+ | Scope.Scope
50
49
  | AuthProviders
51
50
  | AlchemyContext
52
51
  | Cli
@@ -98,6 +97,7 @@ export const Stack: Context.ServiceClass<
98
97
  (stackName: string): Effect.Effect<Self> & {
99
98
  new (_: never): Output.ToOutput<Shape>;
100
99
  make: <A, Req>(
100
+ options: StackProps<NoInfer<Req>>,
101
101
  effect: Effect.Effect<A, never, Req>,
102
102
  ) => Effect.Effect<CompiledStack<A>>;
103
103
  stage: {
@@ -114,36 +114,46 @@ export const Stack: Context.ServiceClass<
114
114
  taggedFunction(
115
115
  Context.Service<Stack, Omit<StackSpec, "output">>()("Stack"),
116
116
  <A, Req>(
117
- stackName: string,
118
- options: {
119
- providers: Layer.Layer<NoInfer<Req>, never, StackServices>;
120
- state: Layer.Layer<State, never, StackServices>;
121
- },
122
- eff: Effect.Effect<A, never, Req>,
123
- ) =>
124
- eff.pipe(
117
+ stackName?: string,
118
+ options?: StackProps<NoInfer<Req>>,
119
+ eff?: Effect.Effect<A, never, Req>,
120
+ ) => {
121
+ if (!stackName) {
122
+ return (stackName: string) =>
123
+ Object.assign(
124
+ // by default, reference the stack at the "current" stage of the importer
125
+ Output.stackRef<A>(stackName).pipe(effectClass),
126
+ {
127
+ stage: createStageProxy(stackName),
128
+ make: <Req = never>(
129
+ options: StackProps<NoInfer<Req>>,
130
+ eff: Effect.Effect<A, never, Req>,
131
+ ) => Stack(stackName, options, eff),
132
+ },
133
+ );
134
+ }
135
+ return eff!.pipe(
125
136
  make({
126
137
  name: stackName,
127
- ...options,
138
+ ...options!,
128
139
  }),
129
140
  (eff) =>
130
141
  Object.assign(eff, {
131
- stage: new Proxy(
132
- {},
133
- {
134
- get: (_, stage: string) =>
135
- ref({
136
- stack: stackName,
137
- stage,
138
- id: stackName,
139
- }),
140
- },
141
- ),
142
+ stage: createStageProxy(stackName),
142
143
  }),
143
- ),
144
+ );
145
+ },
144
146
  ),
145
147
  ) as any;
146
148
 
149
+ const createStageProxy = (stackName: string) =>
150
+ new Proxy(
151
+ {},
152
+ {
153
+ get: (_, stage: string) => Output.stackRef(stackName, { stage }),
154
+ },
155
+ );
156
+
147
157
  export interface StackSpec<Output = any> {
148
158
  name: string;
149
159
  stage: string;
@@ -283,9 +293,18 @@ export const evalStack = <A, B, Err, Req>(
283
293
  options: {
284
294
  stage: string;
285
295
  dev?: boolean;
296
+ /**
297
+ * Optional caller-supplied scope. When provided, scoped resources
298
+ * (e.g. the dev sidecar process) live until the caller closes this
299
+ * scope instead of being torn down when `evalStack` resolves. Used
300
+ * by the test harness so the sidecar survives across `beforeAll`,
301
+ * tests, and `afterAll`. When omitted, behaves as before with a
302
+ * private scope closed on completion.
303
+ */
304
+ scope?: Scope.Scope;
286
305
  },
287
- ) =>
288
- Effect.gen(function* () {
306
+ ) => {
307
+ const body = Effect.gen(function* () {
289
308
  const stack = yield* effect;
290
309
  const configProvider = yield* loadConfigProvider(Option.none());
291
310
 
@@ -305,5 +324,9 @@ export const evalStack = <A, B, Err, Req>(
305
324
  ),
306
325
  Effect.provide(Layer.succeed(Stage, options.stage)),
307
326
  Effect.provide(Layer.provideMerge(alchemy({ dev: options.dev }), platform)),
308
- Effect.scoped,
309
327
  );
328
+
329
+ return options.scope === undefined
330
+ ? Effect.scoped(body)
331
+ : Scope.provide(body, options.scope);
332
+ };
@@ -155,6 +155,25 @@ export const GetReplacedResources = HttpApiEndpoint.get(
155
155
  },
156
156
  );
157
157
 
158
+ export const GetStackOutput = HttpApiEndpoint.get(
159
+ "getStackOutput",
160
+ "/state/stacks/:stack/stages/:stage/output",
161
+ {
162
+ params: StackStage,
163
+ success: Schema.UndefinedOr(ResourceStateSchema),
164
+ },
165
+ );
166
+
167
+ export const SetStackOutput = HttpApiEndpoint.put(
168
+ "setStackOutput",
169
+ "/state/stacks/:stack/stages/:stage/output",
170
+ {
171
+ params: StackStage,
172
+ payload: ResourceStateSchema,
173
+ success: ResourceStateSchema,
174
+ },
175
+ );
176
+
158
177
  /** Response shape for the unauthenticated `/version` probe. */
159
178
  export const VersionResponse = Schema.Struct({
160
179
  version: Schema.Number,
@@ -179,6 +198,8 @@ export class StateGroup extends HttpApiGroup.make("state")
179
198
  .add(DeleteState)
180
199
  .add(GetReplacedResources)
181
200
  .add(DeleteStack)
201
+ .add(GetStackOutput)
202
+ .add(SetStackOutput)
182
203
  .middleware(StateAuth) {}
183
204
 
184
205
  export class VersionGroup extends HttpApiGroup.make("version").add(
@@ -129,6 +129,27 @@ export const makeHttpStateStore = ({
129
129
  query: request.stage === undefined ? {} : { stage: request.stage },
130
130
  })
131
131
  .pipe(Effect.asVoid, mapStateStoreError),
132
+ getOutput: (request) =>
133
+ state
134
+ .getStackOutput({
135
+ params: { stack: request.stack, stage: request.stage },
136
+ })
137
+ .pipe(
138
+ Effect.map((s) =>
139
+ s == null ? undefined : reviveStateRecursive(s),
140
+ ),
141
+ mapStateStoreError,
142
+ ),
143
+ setOutput: (request) =>
144
+ state
145
+ .setStackOutput({
146
+ params: { stack: request.stack, stage: request.stage },
147
+ payload: encodeState(request.value as any),
148
+ })
149
+ .pipe(
150
+ Effect.map(() => request.value),
151
+ mapStateStoreError,
152
+ ),
132
153
  };
133
154
  return service;
134
155
  });
@@ -13,14 +13,18 @@ export const inMemoryState = (
13
13
  StackId,
14
14
  Record<StageId, Record<Fqn, ResourceState>>
15
15
  > = {},
16
+ initialOutputs: Record<StackId, Record<StageId, unknown>> = {},
16
17
  ) =>
17
18
  Layer.effect(
18
19
  State,
19
- Effect.succeed(InMemoryService(initialState)).pipe(recordStateStoreInit),
20
+ Effect.succeed(InMemoryService(initialState, initialOutputs)).pipe(
21
+ recordStateStoreInit,
22
+ ),
20
23
  );
21
24
 
22
25
  export const InMemoryService = (
23
26
  state: Record<StackId, Record<StageId, Record<Fqn, ResourceState>>> = {},
27
+ outputs: Record<StackId, Record<StageId, unknown>> = {},
24
28
  ) =>
25
29
  State.of({
26
30
  id: "inmemory",
@@ -60,12 +64,13 @@ export const InMemoryService = (
60
64
  stage: string;
61
65
  fqn: string;
62
66
  value: V;
63
- }) => {
64
- const stackState = (state[stack] ??= {});
65
- const stageState = (stackState[stage] ??= {});
66
- stageState[fqn] = value;
67
- return Effect.succeed(value);
68
- },
67
+ }) =>
68
+ Effect.sync(() => {
69
+ const stackState = (state[stack] ??= {});
70
+ const stageState = (stackState[stage] ??= {});
71
+ stageState[fqn] = value;
72
+ return value;
73
+ }),
69
74
  delete: ({
70
75
  stack,
71
76
  stage,
@@ -74,7 +79,7 @@ export const InMemoryService = (
74
79
  stack: string;
75
80
  stage: string;
76
81
  fqn: string;
77
- }) => Effect.succeed(delete state[stack]?.[stage]?.[fqn]),
82
+ }) => Effect.sync(() => delete state[stack]?.[stage]?.[fqn]),
78
83
  deleteStack: ({ stack, stage }: { stack: string; stage?: string }) =>
79
84
  Effect.sync(() => {
80
85
  if (stage === undefined) {
@@ -87,4 +92,20 @@ export const InMemoryService = (
87
92
  Effect.succeed(
88
93
  Array.from(Object.keys(state[stack]?.[stage] ?? {}) ?? []),
89
94
  ),
95
+ getOutput: ({ stack, stage }: { stack: string; stage: string }) =>
96
+ Effect.succeed(outputs[stack]?.[stage]),
97
+ setOutput: ({
98
+ stack,
99
+ stage,
100
+ value,
101
+ }: {
102
+ stack: string;
103
+ stage: string;
104
+ value: unknown;
105
+ }) =>
106
+ Effect.sync(() => {
107
+ const stackOutputs = (outputs[stack] ??= {});
108
+ stackOutputs[stage] = value;
109
+ return value;
110
+ }),
90
111
  });