1import * as Config from "effect/Config";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { pipe } from "effect/Function";
5import type { Pipeable } from "effect/Pipeable";
6import { SingleShotGen } from "effect/Utils";
7import { getRefMetadata, isRef, type Ref } from "./Ref.ts";
8import { isResource, type Resource, type ResourceLike } from "./Resource.ts";
9import { RuntimeContext, sanitizeKey } from "./RuntimeContext.ts";
10import { Stack } from "./Stack.ts";
11import { Stage } from "./Stage.ts";
12import * as State from "./State/State.ts";
13import { isPlainData, isPrimitive, type Primitive } from "./Util/data.ts";
14
15const inspect = Symbol.for("nodejs.util.inspect.custom");
16
17export const of = <R extends ResourceLike>(
18 resource: Ref<R> | R,
19): R extends ResourceLike
20 ? ResourceExpr<R["Attributes"]>
21 : RefExpr<R["Attributes"]> => {
22 if (isRef(resource)) {
23 const metadata = getRefMetadata(resource);
24 return new RefExpr(
25 metadata.stack,
26 metadata.stage,
27 metadata.id,
28 // Surface the target's resource type and logical id as
29 // statically-known properties so duck-typing classifiers (Worker
30 // env bindings) and label/bind templates (`${resource.LogicalId}`,
31 // `host.bind\`...\``) identify the ref exactly like a
32 // locally-declared resource.
33 {
34 LogicalId: metadata.id,
35 ...(metadata.type !== undefined ? { Type: metadata.type } : {}),
36 },
37 ) as any;
38 }
39 return new ResourceExpr(resource) as any;
40};
41
42export const asOutput = <T>(t: T | Output<T> | Effect.Effect<T>): Output<T> =>
43 isOutput(t)
44 ? t
45 : Effect.isEffect(t)
46 ? new EffectExpr(VoidExpr, () => t)
47 : new LiteralExpr(t);
48
49/**
50 * Lift a plan-time Effect into an {@link Output}.
51 *
52 * The effect runs when the stack resolves the Output during plan/deploy —
53 * with the stack's services (cloud credentials, region, ...) provided — and
54 * never inside a deployed runtime: constructing the Output is inert, so
55 * helpers built on `fromEffect` (e.g. AMI lookups) are safe to call from
56 * composition code that is re-executed inside a Function/Worker/Instance
57 * bundle.
58 *
59 * The effect must not fail (`E = never`) — die with a descriptive error for
60 * unresolvable lookups.
61 */
62export const fromEffect = <A, Req = never>(
63 effect: Effect.Effect<A, never, Req>,
64): ToOutput<A, Req> => new EffectExpr(VoidExpr, () => effect) as any;
65
66export const isOutput = (value: any): value is Output<any> =>
67 value &&
68 (typeof value === "object" || typeof value === "function") &&
69 ExprSymbol in value;
70
71export interface Output<A = any, Req = any> extends Pipeable {
72 /** @internal phantom */
73 readonly kind: string;
74 /** @internal phantom */
75 readonly A: A;
76 /** @internal phantom */
77 readonly req: Req;
78 /** @internal phantom */
79 [Symbol.iterator](): Iterator<
80 Effect.Effect<void, never, Req>,
81 Accessor<A>,
82 void
83 >;
84 bind(id: string): Effect.Effect<Effect.Effect<A>, never, RuntimeContext>;
85 asEffect(): Effect.Effect<Accessor<A>, never, Req>;
86 as<T>(): Output<T, Req>;
87}
88
89export interface Accessor<A> extends Effect.Effect<A> {}
90
91export type ToOutput<A, Req = never> =
92 // Branded primitives (`string & Brand<"...">`) are assignable to `object`
93 // via the brand intersection, so they must short-circuit to a plain Output
94 // before the object check — otherwise they explode into an ObjectExpr
95 // mapped over every String/Number method. Date is opaque for the same
96 // reason (mirrors AttrOutput in Resource.ts).
97 [A] extends [Primitive | Date]
98 ? Output<A, Req>
99 : [Extract<A, object>] extends [never]
100 ? Output<A, Req>
101 : [Extract<A, any[]>] extends [never]
102 ? ObjectExpr<
103 {
104 [attr in keyof A]: A[attr];
105 },
106 Req
107 >
108 : ArrayExpr<Extract<A, any[]>, Req>;
109
110export const ExprSymbol = Symbol.for("alchemy/Expr");
111
112const exprKind = (node: any): unknown => node?.[ExprSymbol]?.kind ?? node?.kind;
113
114export const isExpr = (value: any): value is Expr<any> =>
115 value &&
116 (typeof value === "object" || typeof value === "function") &&
117 ExprSymbol in value;
118
119export type Expr<A = any, Req = any> =
120 | AllExpr<Expr<A, Req>[]>
121 | ApplyExpr<any, A, Req>
122 | EffectExpr<any, A, Req>
123 | FlatMapExpr<any, A, Req>
124 | LiteralExpr<A>
125 | NamedExpr<A, Req>
126 | PropExpr<A, keyof A, Req>
127 | ResourceExpr<A, Req>
128 | RefExpr<A>
129 | StackRefExpr<A>;
130
131export abstract class BaseExpr<A = any, Req = any> implements Output<A, Req> {
132 declare readonly kind: any;
133 declare readonly A: A;
134 declare readonly src: ResourceLike;
135 declare readonly req: Req;
136 // we use a kind tag instead of instanceof to protect ourselves from duplicate alchemy module imports
137 constructor() {}
138 as<T>(): Output<T, Req> {
139 return this as any;
140 }
141
142 [Symbol.iterator](): Iterator<
143 Effect.Effect<void, never, Req>,
144 Accessor<A>,
145 void
146 > {
147 // @ts-expect-error - TODO(sam): fix this (works at runtime, but maybe indicates a bad assumption)
148 return new SingleShotGen(this.asEffect());
149 }
150
151 asEffect(): any {
152 return this.bind(this.toString());
153 }
154
155 public bind(id: string): any {
156 // `set`/`get` store keys verbatim, so canonicalize here (the caller's job).
157 const key = sanitizeKey(id);
158 return RuntimeContext.pipe(
159 Effect.flatMap((ctx) =>
160 Effect.map(ctx.set(key, this), (k) => ctx.get<A>(k)),
161 ),
162 );
163 }
164
165 public pipe(...fns: any[]): any {
166 // @ts-expect-error
167 return pipe(this, ...fns);
168 }
169 public abstract [inspect](): string;
170 public toString(): string {
171 return this[inspect]();
172 }
173}
174export type ObjectExpr<A, Req = any> = Output<A, Req> & {
175 [Prop in keyof Exclude<A, undefined>]-?: ToOutput<
176 Exclude<A, undefined>[Prop] | Extract<A, undefined>,
177 Req
178 >;
179};
180
181export type ArrayExpr<A extends any[], Req = any> = Output<A, Req> & {
182 [i in Extract<keyof A, number>]: ToOutput<A[i], Req>;
183};
184
185export const isResourceExpr = <Value = any, Req = any>(
186 node: Expr<Value, Req> | any,
187): node is ResourceExpr<Value, Req> => exprKind(node) === "ResourceExpr";
188
189export class ResourceExpr<Value, Req = never> extends BaseExpr<Value, Req> {
190 readonly kind = "ResourceExpr";
191 constructor(
192 readonly src: ResourceLike,
193 readonly stables?: Record<string, any>,
194 ) {
195 super();
196 return proxy(this);
197 }
198 [inspect](): string {
199 return this.src.LogicalId;
200 }
201}
202
203export const isPropExpr = <A = any, Prop extends keyof A = keyof A, Req = any>(
204 node: any,
205): node is PropExpr<A, Prop, Req> => exprKind(node) === "PropExpr";
206
207export class PropExpr<
208 A = any,
209 Id extends keyof A = keyof A,
210 Req = any,
211> extends BaseExpr<A[Id], Req> {
212 readonly kind = "PropExpr";
213 constructor(
214 public readonly expr: Expr<A, Req>,
215 public readonly identifier: Id,
216 ) {
217 super();
218 return proxy(this);
219 }
220 [inspect](): string {
221 return `${this.expr[inspect]()}.${this.identifier.toString()}`;
222 }
223}
224
225export const literal = <A>(value: A) => new LiteralExpr(value);
226
227export const isLiteralExpr = <A = any>(node: any): node is LiteralExpr<A> =>
228 exprKind(node) === "LiteralExpr";
229
230export class LiteralExpr<A> extends BaseExpr<A, never> {
231 readonly kind = "LiteralExpr";
232 constructor(public readonly value: A) {
233 super();
234 return proxy(this);
235 }
236 [inspect](): string {
237 return String(this.value);
238 }
239}
240
241export const VoidExpr = new LiteralExpr(void 0);
242
243export const map: {
244 <A, B>(
245 fn: (value: A) => B,
246 ): <Req>(output: Output<A, Req>) => ToOutput<B, Req>;
247 <A, B, Req>(output: Output<A, Req>, fn: (value: A) => B): ToOutput<B, Req>;
248} = (<A, B, Req>(
249 ...args: [fn: (value: A) => B] | [output: Output<A, Req>, fn: (value: A) => B]
250) =>
251 args.length === 1
252 ? <Req>(output: Output<A, Req>): ToOutput<B, Req> =>
253 new ApplyExpr(output as Expr<A, Req>, args[0]) as any
254 : new ApplyExpr(args[0] as any, args[1])) as any;
255
256//Output.ApplyExpr<any, any, ResourceLike, any>
257export const isApplyExpr = <In = any, Out = any, Req = any>(
258 node: Output<Out, Req>,
259): node is ApplyExpr<In, Out, Req> => exprKind(node) === "ApplyExpr";
260
261export class ApplyExpr<A, B, Req = never> extends BaseExpr<B, Req> {
262 readonly kind = "ApplyExpr";
263 constructor(
264 public readonly expr: Expr<A, Req>,
265 public readonly f: (value: A) => B,
266 ) {
267 super();
268 return proxy(this);
269 }
270
271 [inspect](): string {
272 return `${this.expr[inspect]()}.map(${this.f.toString()})`;
273 }
274}
275
276export const mapEffect =
277 <A, B, Req2>(fn: (value: A) => Effect.Effect<B, never, Req2>) =>
278 <Req>(output: Output<A, Req>): ToOutput<B, Req | Req2> =>
279 new EffectExpr(output as Expr<A, Req>, fn) as any;
280
281export const flatMap: {
282 <A, B, Req2>(
283 fn: (value: A) => Output<B, Req2>,
284 ): <Req>(output: Output<A, Req>) => ToOutput<B, Req | Req2>;
285 <A, B, Req, Req2>(
286 output: Output<A, Req>,
287 fn: (value: A) => Output<B, Req2>,
288 ): ToOutput<B, Req | Req2>;
289} = (<A, B, Req, Req2>(
290 ...args:
291 | [fn: (value: A) => Output<B, Req2>]
292 | [output: Output<A, Req>, fn: (value: A) => Output<B, Req2>]
293) =>
294 args.length === 1
295 ? <Req>(output: Output<A, Req>): ToOutput<B, Req | Req2> =>
296 new FlatMapExpr(output as Expr<A, Req>, args[0]) as any
297 : new FlatMapExpr(args[0] as any, args[1])) as any;
298
299export const isFlatMapExpr = <In = any, Out = any, Req = any, Req2 = any>(
300 node: any,
301): node is FlatMapExpr<In, Out, Req, Req2> => exprKind(node) === "FlatMapExpr";
302
303export class FlatMapExpr<A, B, Req = never, Req2 = never> extends BaseExpr<
304 B,
305 Req | Req2
306> {
307 readonly kind = "FlatMapExpr";
308 constructor(
309 public readonly expr: Expr<A, Req>,
310 public readonly f: (value: A) => Output<B, Req2>,
311 ) {
312 super();
313 return proxy(this);
314 }
315 [inspect](): string {
316 return `${this.expr[inspect]()}.flatMap(${this.f.toString()})`;
317 }
318}
319
320export const isEffectExpr = <In = any, Out = any, Req = any, Req2 = any>(
321 node: any,
322): node is EffectExpr<In, Out, Req, Req2> => exprKind(node) === "EffectExpr";
323
324export class EffectExpr<A, B, Req = never, Req2 = never> extends BaseExpr<
325 B,
326 Req
327> {
328 readonly kind = "EffectExpr";
329 constructor(
330 public readonly expr: Expr<A, Req>,
331 public readonly f: (value: A) => Effect.Effect<B, never, Req2>,
332 ) {
333 super();
334 return proxy(this);
335 }
336 [inspect](): string {
337 return `${this.expr[inspect]()}.mapEffect(${this.f.toString()})`;
338 }
339}
340
341export const isNamedExpr = <A = any, Req = any>(
342 node: any,
343): node is NamedExpr<A, Req> => exprKind(node) === "NamedExpr";
344
345/**
346 * Wraps another `Expr` and overrides its `toString()` / inspect output.
347 *
348 * `BaseExpr` derives the binding id from `this.toString()`, so
349 * wrapping an expression in `NamedExpr` makes that derived id stable and
350 * caller-controlled (e.g. an env var name like `"API_KEY"`).
351 */
352export class NamedExpr<A, Req = never> extends BaseExpr<A, Req> {
353 readonly kind = "NamedExpr";
354 constructor(
355 public readonly expr: Expr<A, Req>,
356 public readonly bindingName: string,
357 ) {
358 super();
359 return proxy(this);
360 }
361 [inspect](): string {
362 return this.bindingName;
363 }
364}
365
366export const named = <A, Req>(
367 expr: Output<A, Req>,
368 name: string,
369): Output<A, Req> => new NamedExpr(expr as Expr<A, Req>, name) as any;
370
371export const all = <Outs extends (Output | Expr)[]>(...outs: Outs) =>
372 new AllExpr(outs as any) as unknown as All<Outs>;
373
374export type All<Outs extends (Output | Expr)[]> = number extends Outs["length"]
375 ? [Outs[number]] extends [
376 Output<infer V, infer Req> | Expr<infer V, infer Req>,
377 ]
378 ? Output<V, Req>
379 : never
380 : Tuple<Outs>;
381
382type Tuple<
383 Outs extends (Output | Expr)[],
384 Values extends any[] = [],
385 Req = never,
386> = Outs extends [infer H, ...infer Tail extends (Output | Expr)[]]
387 ? H extends Output<infer V, infer Req2>
388 ? Tuple<Tail, [...Values, V], Req | Req2>
389 : never
390 : Output<Values, Req>;
391
392export const isAllExpr = <Outs extends Expr[] = Expr[]>(
393 node: any,
394): node is AllExpr<Outs> => exprKind(node) === "AllExpr";
395
396export class AllExpr<Outs extends Expr[]> extends BaseExpr<Outs> {
397 readonly kind = "AllExpr";
398 constructor(public readonly outs: Outs) {
399 super();
400 return proxy(this);
401 }
402 [inspect](): string {
403 return `all(${this.outs.map((out) => out[inspect]()).join(", ")})`;
404 }
405}
406
407export const isRefExpr = <A = any>(node: any): node is RefExpr<A> =>
408 exprKind(node) === "RefExpr";
409
410export class RefExpr<A> extends BaseExpr<A, never> {
411 readonly kind = "RefExpr";
412 constructor(
413 public readonly stack: string | undefined,
414 public readonly stage: string | undefined,
415 public readonly resourceId: string,
416 /**
417 * Statically-known properties of the ref's target (currently its
418 * resource `Type`), served as literals by the proxy instead of
419 * `PropExpr`s — mirrors {@link ResourceExpr}'s `stables`.
420 */
421 readonly stables?: Record<string, any>,
422 ) {
423 super();
424 return proxy(this);
425 }
426 [inspect](): string {
427 return `ref(${this.resourceId}, { stack: ${this.stack}, stage: ${this.stage} })`;
428 }
429}
430
431export const isStackRefExpr = <A = any>(node: any): node is StackRefExpr<A> =>
432 exprKind(node) === "StackRefExpr";
433
434/**
435 * A reference to the persisted output of a Stack at `(stack, stage)`.
436 *
437 * Resolved at evaluation time by reading `state.getOutput({ stack,
438 * stage })`. Distinct from {@link RefExpr}, which references a
439 * single resource's attributes within a stack/stage. `stage` may be
440 * `undefined`, in which case it falls back to the current stage.
441 */
442export class StackRefExpr<A> extends BaseExpr<A, never> {
443 readonly kind = "StackRefExpr";
444 constructor(
445 public readonly stack: string,
446 public readonly stage: string | undefined,
447 ) {
448 super();
449 return proxy(this);
450 }
451 [inspect](): string {
452 return `stackRef(${this.stack}${
453 this.stage ? `, { stage: ${this.stage} }` : ""
454 })`;
455 }
456}
457
458/**
459 * Build an `Output<A>` referencing the persisted output of another
460 * Stack. The returned Effect resolves to a lazy `Output<A>` whose
461 * value is read from the state store at plan/apply time.
462 *
463 * Returns `Effect<Output<A>>` (not `Output<A>` directly) so that
464 * `yield* Output.stackRef(...)` reads ergonomically inside an Effect
465 * generator and lines up with `Resource.ref` and `Stack.stage.<name>`.
466 */
467export const stackRef = <A>(
468 stack: string,
469 options: { stage?: string } = {},
470): Effect.Effect<Output<A, never>> =>
471 Effect.succeed(new StackRefExpr<A>(stack, options.stage) as any);
472
473export const filter = <Outs extends any[]>(...outs: Outs) =>
474 outs.filter(isOutput) as unknown as Filter<Outs>;
475
476export type Filter<Outs extends any[]> = number extends Outs["length"]
477 ? Output<
478 Extract<Outs[number], Output>["value"],
479 Extract<Outs[number], Output>["req"]
480 >
481 : FilterTuple<Outs>;
482
483export type FilterTuple<
484 Outs extends (Output | Expr)[],
485 Values extends any[] = [],
486> = Outs extends [infer H, ...infer Tail extends (Output | Expr)[]]
487 ? H extends Output<infer V>
488 ? FilterTuple<Tail, [...Values, V]>
489 : FilterTuple<Tail, Values>
490 : Output<Values>;
491
492export const interpolate = <Args extends any[]>(
493 template: TemplateStringsArray,
494 ...args: Args
495): All<Args> extends Output<any, infer Req> ? Output<string, Req> : never => {
496 const outs = args.map((arg) => (isOutput(arg) ? arg : literal(arg)));
497 const expr = all(...outs).pipe(
498 map((args) =>
499 template
500 .map((str, i) => str + (args[i] == null ? "" : String(args[i])))
501 .join(""),
502 ),
503 );
504 // The binding id of a mapped Output embeds the mapper's source, which the
505 // runtime bundler reprints differently from the deploying process, so the
506 // two sides would derive different keys. Name it from the template and its
507 // arguments instead, which are identical on both sides.
508 const name = template
509 .map((str, i) =>
510 i < outs.length ? `${str}\${${(outs[i] as any)[inspect]()}}` : str,
511 )
512 .join("");
513 return named(expr as Output<string, any>, `interpolate(${name})`) as any;
514};
515
516function proxy(self: any): any {
517 const target = Object.assign(() => {}, self);
518 if (inspect in self) {
519 Object.defineProperty(target, inspect, {
520 value: self[inspect].bind(self),
521 configurable: true,
522 });
523 }
524 const proxy = new Proxy(target, {
525 has: (_, prop) =>
526 prop === ExprSymbol || prop === inspect
527 ? true
528 : // Statically-known literal props (`Type`, `LogicalId` on
529 // resource/ref exprs) are visible to `in` checks so duck-typing
530 // code paths (e.g. `"LogicalId" in arg`) treat them like real
531 // properties, matching what `get` serves.
532 ((isResourceExpr(self) || isRefExpr(self)) &&
533 self.stables !== undefined &&
534 prop in self.stables) ||
535 prop in self,
536 get: (target, prop) =>
537 prop === Symbol.toPrimitive
538 ? (hint: string) => {
539 // Any JS-level coercion of an unresolved Output produces a
540 // placeholder that *looks* like a real value but isn't:
541 //
542 // - `string` / `default` hints (`${output}`, `output + ""`,
543 // `==` against a primitive) previously fell through to
544 // `self.toString()` and returned the inspect form
545 // (e.g. "tunnel.tunnelId"). The bogus string flowed
546 // into resource props and into the cloud — only
547 // surfacing as an opaque downstream error (see PR
548 // description for a real Cloudflare DNS landing).
549 //
550 // - `number` hint (`+output`, `output * 2`,
551 // `Math.max(0, output)`) previously returned NaN, which
552 // propagates silently through arithmetic and lands as
553 // "the API rejected a NaN field" much later.
554 //
555 // All three hints fail loud at the coercion site with a
556 // pointer to the right composition API.
557 throw new Error(
558 `Cannot coerce Output<${self[inspect]()}> to a ` +
559 `${hint === "number" ? "number" : "string"} via JS coercion. ` +
560 `Use Output.interpolate\`...\` or Output.map(output, fn) ` +
561 `to compose Outputs — the value isn't known until deploy time.`,
562 );
563 }
564 : prop === ExprSymbol
565 ? self
566 : prop === inspect
567 ? target[inspect]
568 : (isResourceExpr(self) || isRefExpr(self)) &&
569 self.stables &&
570 prop in self.stables
571 ? self.stables[prop as keyof typeof self.stables]
572 : prop in self
573 ? typeof self[prop as keyof typeof self] === "function" &&
574 !("kind" in self)
575 ? new PropExpr(proxy, prop as never)
576 : self[prop as keyof typeof self]
577 : new PropExpr(proxy, prop as never),
578 apply: (_, thisArg, args) => {
579 if (isPropExpr(self)) {
580 // Method-style combinators on an Output proxy. `map`/`apply` and
581 // `mapEffect`/`effect` are aliases that mirror the standalone
582 // `Output.map` / `Output.mapEffect` / `Output.flatMap` functions.
583 if (self.identifier === "map" || self.identifier === "apply") {
584 return new ApplyExpr(self.expr, args[0]);
585 } else if (
586 self.identifier === "mapEffect" ||
587 self.identifier === "effect"
588 ) {
589 return new EffectExpr(self.expr, args[0]);
590 } else if (self.identifier === "flatMap") {
591 return new FlatMapExpr(self.expr, args[0]);
592 }
593 }
594 return undefined;
595 },
596 });
597 return proxy;
598}
599
600/// Evaluation
601
602export class MissingSourceError extends Data.TaggedError("MissingSourceError")<{
603 message: string;
604 srcId: string;
605}> {}
606
607export class InvalidReferenceError extends Data.TaggedError(
608 "InvalidReferenceError",
609)<{
610 message: string;
611 stack: string;
612 stage: string;
613 resourceId: string;
614}> {}
615
616export const evaluate: <A, Req = never>(
617 expr: Output<A, Req> | A,
618 upstream: {
619 [Id in string]: any;
620 },
621 // Ancestor-path cycle guard — a plain-data value that appears on its own
622 // ancestor chain is cut to `undefined` (it could never serialize anyway).
623 // Immutable per-level so legitimately-shared diamond references survive
624 // (#1082).
625 ancestors?: ReadonlySet<object>,
626) => Effect.Effect<
627 A,
628 InvalidReferenceError | MissingSourceError | Config.ConfigError,
629 State.State | Req
630> = (expr, upstream, ancestors = new Set()) =>
631 Effect.gen(function* () {
632 if (isResource(expr)) {
633 const srcId = expr.FQN;
634 const src = upstream[srcId as keyof typeof upstream];
635 if (!Object.hasOwn(upstream, srcId)) {
636 // type-safety should prevent this but let the caller decide how to handle it
637 return yield* new MissingSourceError({
638 message: `Source ${srcId} not found`,
639 srcId,
640 });
641 }
642 return src;
643 } else if (isOutput(expr)) {
644 if (isResourceExpr(expr)) {
645 const srcId = expr.src.FQN;
646 const src = upstream[srcId as keyof typeof upstream];
647 if (!Object.hasOwn(upstream, srcId)) {
648 // type-safety should prevent this but let the caller decide how to handle it
649 return yield* new MissingSourceError({
650 message: `Source ${srcId} not found`,
651 srcId,
652 });
653 }
654 return src;
655 } else if (isLiteralExpr(expr)) {
656 return expr.value;
657 } else if (isApplyExpr(expr)) {
658 return expr.f(yield* evaluate(expr.expr, upstream));
659 } else if (isEffectExpr(expr)) {
660 // TODO(sam): the same effect shoudl be memoized so that it's not run multiple times
661 return yield* expr.f(yield* evaluate(expr.expr, upstream));
662 } else if (isFlatMapExpr(expr)) {
663 // Resolve the source, hand it to `f` to produce a new Output, then
664 // recursively evaluate that Output (flattening one level).
665 const value = yield* evaluate(expr.expr, upstream);
666 return yield* evaluate(expr.f(value), upstream);
667 } else if (isAllExpr(expr)) {
668 return yield* Effect.all(
669 expr.outs.map((out) => evaluate(out, upstream)),
670 );
671 } else if (isPropExpr(expr)) {
672 return (yield* evaluate(expr.expr, upstream))?.[expr.identifier];
673 } else if (isNamedExpr(expr)) {
674 return yield* evaluate(expr.expr, upstream);
675 } else if (isRefExpr(expr)) {
676 const state = yield* yield* State.State;
677 const stack = expr.stack ?? (yield* Stack).name;
678 const stage = expr.stage ?? (yield* Stage);
679 const resource = yield* state.get({
680 stack,
681 stage,
682 fqn: expr.resourceId,
683 });
684 if (!resource) {
685 return yield* Effect.fail(
686 new InvalidReferenceError({
687 message: `Reference to '${expr.resourceId}' in stack '${stack}' and stage '${stage}' not found. Have you deployed '${stage}' or '${stack}'?`,
688 stack,
689 stage,
690 resourceId: expr.resourceId,
691 }),
692 );
693 }
694 // RefExpr targets persisted resources; tasks aren't cross-stack
695 // referenceable. Return the resource's output attrs, otherwise the
696 // task's output value, otherwise undefined.
697 return (resource as any).attr ?? (resource as any).output;
698 } else if (isStackRefExpr(expr)) {
699 const state = yield* yield* State.State;
700 const stack = expr.stack;
701 const stage = expr.stage ?? (yield* Stage);
702 const output = yield* state.getOutput({ stack, stage });
703 if (output == null) {
704 return yield* Effect.fail(
705 new InvalidReferenceError({
706 message: `Reference to stack '${stack}' at stage '${stage}' not found. Have you deployed stage '${stage}' of '${stack}'?`,
707 stack,
708 stage,
709 resourceId: stack,
710 }),
711 );
712 }
713 return output;
714 }
715 }
716 if (Config.isConfig(expr)) {
717 // Resolve Config against the deploy environment — see resolveInput in
718 // Plan.ts for rationale. `Config.Redacted` resolves to a `Redacted`,
719 // which stays opaque via the leaf fallthrough below.
720 return yield* evaluate(yield* expr, upstream, ancestors);
721 } else if (isPlainData(expr)) {
722 if (ancestors.has(expr)) {
723 return undefined;
724 }
725 const nested = new Set(ancestors).add(expr);
726 if (Array.isArray(expr)) {
727 return yield* Effect.all(
728 expr.map((item) => evaluate(item, upstream, nested)),
729 );
730 }
731 return Object.fromEntries(
732 yield* Effect.all(
733 Object.entries(expr).map(([key, value]) =>
734 evaluate(value, upstream, nested).pipe(
735 Effect.map((value) => [key, value]),
736 ),
737 ),
738 ),
739 );
740 }
741 // Everything else is a leaf returned by identity: Duration, Redacted,
742 // Date, and effect runtime values (a Worker's `exports` carries each
743 // DO's `constructor` Effect and captured `services` Context). Rebuilding
744 // a class instance entry-by-entry strips its prototype, and effect
745 // ≥4.0.0-beta.103's Context is cyclic (#1082). This sits after
746 // `Config.isConfig` on purpose — Configs are Effects but must resolve.
747 return expr;
748 }) as Effect.Effect<any>;
749
750export const hasOutputs = (value: any): value is Output<any, any> =>
751 Object.keys(upstreamAny(value)).length > 0;
752
753/**
754 * Cycle guard shared by the upstream walkers. Marks `value` as visited in
755 * `seen`; returns true when it was already visited (the caller returns `{}`
756 * — the first visit already contributed the subtree's resources to the
757 * FQN-keyed union, so skipping repeats is lossless).
758 */
759const alreadySeen = (value: object, seen: WeakSet<object>): boolean => {
760 if (seen.has(value)) {
761 return true;
762 }
763 seen.add(value);
764 return false;
765};
766
767// The dependency rule (#1082): a Resource or Output IS a dependency; plain
768// data (arrays, plain objects) is traversed to find them; every other value
769// — class instances like effect's Effect/Layer/Context, Dates, SDK objects,
770// functions — is a leaf. See `isPlainData` in Util/data.ts for why leaves
771// must never be walked.
772
773export const upstreamAny = (
774 value: any,
775 seen: WeakSet<object> = new WeakSet(),
776): {
777 [ID in string]: Resource;
778} => {
779 if (isResource(value)) {
780 return { [value.FQN]: value as Resource };
781 } else if (isExpr(value)) {
782 return upstream(value, seen);
783 } else if (isPlainData(value)) {
784 if (alreadySeen(value, seen)) {
785 return {};
786 }
787 return Object.assign(
788 {},
789 ...Object.values(value).map((value) => resolveUpstream(value, seen)),
790 );
791 }
792 return {};
793};
794
795// TODO(sam): add a type
796export const upstream = <E extends Output<any, any>>(
797 expr: E,
798 seen: WeakSet<object> = new WeakSet(),
799): any => {
800 if (isResource(expr)) {
801 return {
802 [(expr as unknown as Resource).FQN]: expr,
803 };
804 } else if (isResourceExpr(expr)) {
805 return {
806 [expr.src.FQN]: expr.src,
807 };
808 } else if (isPropExpr(expr)) {
809 return upstream(expr.expr, seen);
810 } else if (isAllExpr(expr)) {
811 return Object.assign({}, ...expr.outs.map((out) => upstream(out, seen)));
812 } else if (
813 isEffectExpr(expr) ||
814 isApplyExpr(expr) ||
815 isFlatMapExpr(expr) ||
816 isNamedExpr(expr)
817 ) {
818 return upstream(expr.expr, seen);
819 } else if (isPlainData(expr)) {
820 if (alreadySeen(expr, seen)) {
821 return {};
822 }
823 return Object.values(expr)
824 .map((v) => upstream(v as any, seen))
825 .reduce(toObject, {});
826 }
827 return {};
828};
829
830// TODO(sam): add a type
831export const resolveUpstream = <const A>(
832 value: A,
833 seen: WeakSet<object> = new WeakSet(),
834): any => {
835 if (isPrimitive(value)) {
836 return {} as any;
837 } else if (isResource(value)) {
838 return { [(value as unknown as Resource).FQN]: value } as any;
839 } else if (isOutput(value)) {
840 return upstream(value, seen) as any;
841 } else if (isPlainData(value)) {
842 if (alreadySeen(value, seen)) {
843 return {} as any;
844 }
845 return Object.fromEntries(
846 Object.values(value)
847 .map((v) => resolveUpstream(v, seen))
848 .flatMap(Object.entries),
849 ) as any;
850 }
851 return {} as any;
852};
853
854const toObject = <A, B>(acc: B, v: A) => ({
855 ...acc,
856 ...v,
857});
858
859export const log = <A>(_value: A) =>
860 Effect.gen(function* () {
861 // TODO(sam): implement a log effect
862 });
863
864export const toEnvKey = <const ID extends string, const Suffix extends string>(
865 id: ID,
866 suffix: Suffix,
867) => `${replace(toUpper(id))}_${replace(toUpper(suffix))}` as const;
868
869export const toUpper = <const S extends string>(str: S) =>
870 str.toUpperCase() as string extends S ? S : Uppercase<S>;
871
872const replace = <const S extends string>(str: S) =>
873 str.replace(/-/g, "_") as Replace<S>;
874
875type Replace<S extends string, Accum extends string = ""> = string extends S
876 ? S
877 : S extends ""
878 ? Accum
879 : S extends `${infer S}${infer Rest}`
880 ? S extends "-"
881 ? Replace<Rest, `${Accum}_`>
882 : Replace<Rest, `${Accum}${S}`>
883 : Accum;