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