import * as Config from "effect/Config"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import { pipe } from "effect/Function"; import type { Pipeable } from "effect/Pipeable"; import { SingleShotGen } from "effect/Utils"; import { getRefMetadata, isRef, type Ref } from "./Ref.ts"; import { isResource, type Resource, type ResourceLike } from "./Resource.ts"; import { RuntimeContext, sanitizeKey } from "./RuntimeContext.ts"; import { Stack } from "./Stack.ts"; import { Stage } from "./Stage.ts"; import * as State from "./State/State.ts"; import { isPlainData, isPrimitive, type Primitive } from "./Util/data.ts"; const inspect = Symbol.for("nodejs.util.inspect.custom"); export const of = ( resource: Ref | R, ): R extends ResourceLike ? ResourceExpr : RefExpr => { if (isRef(resource)) { const metadata = getRefMetadata(resource); return new RefExpr( metadata.stack, metadata.stage, metadata.id, // Surface the target's resource type and logical id as // statically-known properties so duck-typing classifiers (Worker // env bindings) and label/bind templates (`${resource.LogicalId}`, // `host.bind\`...\``) identify the ref exactly like a // locally-declared resource. { LogicalId: metadata.id, ...(metadata.type !== undefined ? { Type: metadata.type } : {}), }, ) as any; } return new ResourceExpr(resource) as any; }; export const asOutput = (t: T | Output | Effect.Effect): Output => isOutput(t) ? t : Effect.isEffect(t) ? new EffectExpr(VoidExpr, () => t) : new LiteralExpr(t); /** * Lift a plan-time Effect into an {@link Output}. * * The effect runs when the stack resolves the Output during plan/deploy — * with the stack's services (cloud credentials, region, ...) provided — and * never inside a deployed runtime: constructing the Output is inert, so * helpers built on `fromEffect` (e.g. AMI lookups) are safe to call from * composition code that is re-executed inside a Function/Worker/Instance * bundle. * * The effect must not fail (`E = never`) — die with a descriptive error for * unresolvable lookups. */ export const fromEffect = ( effect: Effect.Effect, ): ToOutput => new EffectExpr(VoidExpr, () => effect) as any; export const isOutput = (value: any): value is Output => value && (typeof value === "object" || typeof value === "function") && ExprSymbol in value; export interface Output extends Pipeable { /** @internal phantom */ readonly kind: string; /** @internal phantom */ readonly A: A; /** @internal phantom */ readonly req: Req; /** @internal phantom */ [Symbol.iterator](): Iterator< Effect.Effect, Accessor, void >; bind(id: string): Effect.Effect, never, RuntimeContext>; asEffect(): Effect.Effect, never, Req>; as(): Output; } export interface Accessor extends Effect.Effect {} export type ToOutput = // Branded primitives (`string & Brand<"...">`) are assignable to `object` // via the brand intersection, so they must short-circuit to a plain Output // before the object check — otherwise they explode into an ObjectExpr // mapped over every String/Number method. Date is opaque for the same // reason (mirrors AttrOutput in Resource.ts). [A] extends [Primitive | Date] ? Output : [Extract] extends [never] ? Output : [Extract] extends [never] ? ObjectExpr< { [attr in keyof A]: A[attr]; }, Req > : ArrayExpr, Req>; export const ExprSymbol = Symbol.for("alchemy/Expr"); const exprKind = (node: any): unknown => node?.[ExprSymbol]?.kind ?? node?.kind; export const isExpr = (value: any): value is Expr => value && (typeof value === "object" || typeof value === "function") && ExprSymbol in value; export type Expr = | AllExpr[]> | ApplyExpr | EffectExpr | FlatMapExpr | LiteralExpr | NamedExpr | PropExpr | ResourceExpr | RefExpr | StackRefExpr; export abstract class BaseExpr implements Output { declare readonly kind: any; declare readonly A: A; declare readonly src: ResourceLike; declare readonly req: Req; // we use a kind tag instead of instanceof to protect ourselves from duplicate alchemy module imports constructor() {} as(): Output { return this as any; } [Symbol.iterator](): Iterator< Effect.Effect, Accessor, void > { // @ts-expect-error - TODO(sam): fix this (works at runtime, but maybe indicates a bad assumption) return new SingleShotGen(this.asEffect()); } asEffect(): any { return this.bind(this.toString()); } public bind(id: string): any { // `set`/`get` store keys verbatim, so canonicalize here (the caller's job). const key = sanitizeKey(id); return RuntimeContext.pipe( Effect.flatMap((ctx) => Effect.map(ctx.set(key, this), (k) => ctx.get(k)), ), ); } public pipe(...fns: any[]): any { // @ts-expect-error return pipe(this, ...fns); } public abstract [inspect](): string; public toString(): string { return this[inspect](); } } export type ObjectExpr = Output & { [Prop in keyof Exclude]-?: ToOutput< Exclude[Prop] | Extract, Req >; }; export type ArrayExpr = Output & { [i in Extract]: ToOutput; }; export const isResourceExpr = ( node: Expr | any, ): node is ResourceExpr => exprKind(node) === "ResourceExpr"; export class ResourceExpr extends BaseExpr { readonly kind = "ResourceExpr"; constructor( readonly src: ResourceLike, readonly stables?: Record, ) { super(); return proxy(this); } [inspect](): string { return this.src.LogicalId; } } export const isPropExpr = ( node: any, ): node is PropExpr => exprKind(node) === "PropExpr"; export class PropExpr< A = any, Id extends keyof A = keyof A, Req = any, > extends BaseExpr { readonly kind = "PropExpr"; constructor( public readonly expr: Expr, public readonly identifier: Id, ) { super(); return proxy(this); } [inspect](): string { return `${this.expr[inspect]()}.${this.identifier.toString()}`; } } export const literal = (value: A) => new LiteralExpr(value); export const isLiteralExpr = (node: any): node is LiteralExpr => exprKind(node) === "LiteralExpr"; export class LiteralExpr extends BaseExpr { readonly kind = "LiteralExpr"; constructor(public readonly value: A) { super(); return proxy(this); } [inspect](): string { return String(this.value); } } export const VoidExpr = new LiteralExpr(void 0); export const map: { ( fn: (value: A) => B, ): (output: Output) => ToOutput; (output: Output, fn: (value: A) => B): ToOutput; } = (( ...args: [fn: (value: A) => B] | [output: Output, fn: (value: A) => B] ) => args.length === 1 ? (output: Output): ToOutput => new ApplyExpr(output as Expr, args[0]) as any : new ApplyExpr(args[0] as any, args[1])) as any; //Output.ApplyExpr export const isApplyExpr = ( node: Output, ): node is ApplyExpr => exprKind(node) === "ApplyExpr"; export class ApplyExpr extends BaseExpr { readonly kind = "ApplyExpr"; constructor( public readonly expr: Expr, public readonly f: (value: A) => B, ) { super(); return proxy(this); } [inspect](): string { return `${this.expr[inspect]()}.map(${this.f.toString()})`; } } export const mapEffect = (fn: (value: A) => Effect.Effect) => (output: Output): ToOutput => new EffectExpr(output as Expr, fn) as any; export const flatMap: { ( fn: (value: A) => Output, ): (output: Output) => ToOutput; ( output: Output, fn: (value: A) => Output, ): ToOutput; } = (( ...args: | [fn: (value: A) => Output] | [output: Output, fn: (value: A) => Output] ) => args.length === 1 ? (output: Output): ToOutput => new FlatMapExpr(output as Expr, args[0]) as any : new FlatMapExpr(args[0] as any, args[1])) as any; export const isFlatMapExpr = ( node: any, ): node is FlatMapExpr => exprKind(node) === "FlatMapExpr"; export class FlatMapExpr extends BaseExpr< B, Req | Req2 > { readonly kind = "FlatMapExpr"; constructor( public readonly expr: Expr, public readonly f: (value: A) => Output, ) { super(); return proxy(this); } [inspect](): string { return `${this.expr[inspect]()}.flatMap(${this.f.toString()})`; } } export const isEffectExpr = ( node: any, ): node is EffectExpr => exprKind(node) === "EffectExpr"; export class EffectExpr extends BaseExpr< B, Req > { readonly kind = "EffectExpr"; constructor( public readonly expr: Expr, public readonly f: (value: A) => Effect.Effect, ) { super(); return proxy(this); } [inspect](): string { return `${this.expr[inspect]()}.mapEffect(${this.f.toString()})`; } } export const isNamedExpr = ( node: any, ): node is NamedExpr => exprKind(node) === "NamedExpr"; /** * Wraps another `Expr` and overrides its `toString()` / inspect output. * * `BaseExpr` derives the binding id from `this.toString()`, so * wrapping an expression in `NamedExpr` makes that derived id stable and * caller-controlled (e.g. an env var name like `"API_KEY"`). */ export class NamedExpr extends BaseExpr { readonly kind = "NamedExpr"; constructor( public readonly expr: Expr, public readonly bindingName: string, ) { super(); return proxy(this); } [inspect](): string { return this.bindingName; } } export const named = ( expr: Output, name: string, ): Output => new NamedExpr(expr as Expr, name) as any; export const all = (...outs: Outs) => new AllExpr(outs as any) as unknown as All; export type All = number extends Outs["length"] ? [Outs[number]] extends [ Output | Expr, ] ? Output : never : Tuple; type Tuple< Outs extends (Output | Expr)[], Values extends any[] = [], Req = never, > = Outs extends [infer H, ...infer Tail extends (Output | Expr)[]] ? H extends Output ? Tuple : never : Output; export const isAllExpr = ( node: any, ): node is AllExpr => exprKind(node) === "AllExpr"; export class AllExpr extends BaseExpr { readonly kind = "AllExpr"; constructor(public readonly outs: Outs) { super(); return proxy(this); } [inspect](): string { return `all(${this.outs.map((out) => out[inspect]()).join(", ")})`; } } export const isRefExpr = (node: any): node is RefExpr => exprKind(node) === "RefExpr"; export class RefExpr extends BaseExpr { readonly kind = "RefExpr"; constructor( public readonly stack: string | undefined, public readonly stage: string | undefined, public readonly resourceId: string, /** * Statically-known properties of the ref's target (currently its * resource `Type`), served as literals by the proxy instead of * `PropExpr`s — mirrors {@link ResourceExpr}'s `stables`. */ readonly stables?: Record, ) { super(); return proxy(this); } [inspect](): string { return `ref(${this.resourceId}, { stack: ${this.stack}, stage: ${this.stage} })`; } } export const isStackRefExpr = (node: any): node is StackRefExpr => exprKind(node) === "StackRefExpr"; /** * A reference to the persisted output of a Stack at `(stack, stage)`. * * Resolved at evaluation time by reading `state.getOutput({ stack, * stage })`. Distinct from {@link RefExpr}, which references a * single resource's attributes within a stack/stage. `stage` may be * `undefined`, in which case it falls back to the current stage. */ export class StackRefExpr extends BaseExpr { readonly kind = "StackRefExpr"; constructor( public readonly stack: string, public readonly stage: string | undefined, ) { super(); return proxy(this); } [inspect](): string { return `stackRef(${this.stack}${ this.stage ? `, { stage: ${this.stage} }` : "" })`; } } /** * Build an `Output` referencing the persisted output of another * Stack. The returned Effect resolves to a lazy `Output` whose * value is read from the state store at plan/apply time. * * Returns `Effect>` (not `Output` directly) so that * `yield* Output.stackRef(...)` reads ergonomically inside an Effect * generator and lines up with `Resource.ref` and `Stack.stage.`. */ export const stackRef = ( stack: string, options: { stage?: string } = {}, ): Effect.Effect> => Effect.succeed(new StackRefExpr(stack, options.stage) as any); export const filter = (...outs: Outs) => outs.filter(isOutput) as unknown as Filter; export type Filter = number extends Outs["length"] ? Output< Extract["value"], Extract["req"] > : FilterTuple; export type FilterTuple< Outs extends (Output | Expr)[], Values extends any[] = [], > = Outs extends [infer H, ...infer Tail extends (Output | Expr)[]] ? H extends Output ? FilterTuple : FilterTuple : Output; export const interpolate = ( template: TemplateStringsArray, ...args: Args ): All extends Output ? Output : never => all(...args.map((arg) => (isOutput(arg) ? arg : literal(arg)))).pipe( map((args) => template .map((str, i) => str + (args[i] == null ? "" : String(args[i]))) .join(""), ), ) as any; function proxy(self: any): any { const target = Object.assign(() => {}, self); if (inspect in self) { Object.defineProperty(target, inspect, { value: self[inspect].bind(self), configurable: true, }); } const proxy = new Proxy(target, { has: (_, prop) => prop === ExprSymbol || prop === inspect ? true : // Statically-known literal props (`Type`, `LogicalId` on // resource/ref exprs) are visible to `in` checks so duck-typing // code paths (e.g. `"LogicalId" in arg`) treat them like real // properties, matching what `get` serves. ((isResourceExpr(self) || isRefExpr(self)) && self.stables !== undefined && prop in self.stables) || prop in self, get: (target, prop) => prop === Symbol.toPrimitive ? (hint: string) => { // Any JS-level coercion of an unresolved Output produces a // placeholder that *looks* like a real value but isn't: // // - `string` / `default` hints (`${output}`, `output + ""`, // `==` against a primitive) previously fell through to // `self.toString()` and returned the inspect form // (e.g. "tunnel.tunnelId"). The bogus string flowed // into resource props and into the cloud — only // surfacing as an opaque downstream error (see PR // description for a real Cloudflare DNS landing). // // - `number` hint (`+output`, `output * 2`, // `Math.max(0, output)`) previously returned NaN, which // propagates silently through arithmetic and lands as // "the API rejected a NaN field" much later. // // All three hints fail loud at the coercion site with a // pointer to the right composition API. throw new Error( `Cannot coerce Output<${self[inspect]()}> to a ` + `${hint === "number" ? "number" : "string"} via JS coercion. ` + `Use Output.interpolate\`...\` or Output.map(output, fn) ` + `to compose Outputs — the value isn't known until deploy time.`, ); } : prop === ExprSymbol ? self : prop === inspect ? target[inspect] : (isResourceExpr(self) || isRefExpr(self)) && self.stables && prop in self.stables ? self.stables[prop as keyof typeof self.stables] : prop in self ? typeof self[prop as keyof typeof self] === "function" && !("kind" in self) ? new PropExpr(proxy, prop as never) : self[prop as keyof typeof self] : new PropExpr(proxy, prop as never), apply: (_, thisArg, args) => { if (isPropExpr(self)) { // Method-style combinators on an Output proxy. `map`/`apply` and // `mapEffect`/`effect` are aliases that mirror the standalone // `Output.map` / `Output.mapEffect` / `Output.flatMap` functions. if (self.identifier === "map" || self.identifier === "apply") { return new ApplyExpr(self.expr, args[0]); } else if ( self.identifier === "mapEffect" || self.identifier === "effect" ) { return new EffectExpr(self.expr, args[0]); } else if (self.identifier === "flatMap") { return new FlatMapExpr(self.expr, args[0]); } } return undefined; }, }); return proxy; } /// Evaluation export class MissingSourceError extends Data.TaggedError("MissingSourceError")<{ message: string; srcId: string; }> {} export class InvalidReferenceError extends Data.TaggedError( "InvalidReferenceError", )<{ message: string; stack: string; stage: string; resourceId: string; }> {} export const evaluate: ( expr: Output | A, upstream: { [Id in string]: any; }, // Ancestor-path cycle guard — a plain-data value that appears on its own // ancestor chain is cut to `undefined` (it could never serialize anyway). // Immutable per-level so legitimately-shared diamond references survive // (#1082). ancestors?: ReadonlySet, ) => Effect.Effect< A, InvalidReferenceError | MissingSourceError | Config.ConfigError, State.State | Req > = (expr, upstream, ancestors = new Set()) => Effect.gen(function* () { if (isResource(expr)) { const srcId = expr.FQN; const src = upstream[srcId as keyof typeof upstream]; if (!src) { // type-safety should prevent this but let the caller decide how to handle it return yield* new MissingSourceError({ message: `Source ${srcId} not found`, srcId, }); } return src; } else if (isOutput(expr)) { if (isResourceExpr(expr)) { const srcId = expr.src.FQN; const src = upstream[srcId as keyof typeof upstream]; if (!src) { // type-safety should prevent this but let the caller decide how to handle it return yield* new MissingSourceError({ message: `Source ${srcId} not found`, srcId, }); } return src; } else if (isLiteralExpr(expr)) { return expr.value; } else if (isApplyExpr(expr)) { return expr.f(yield* evaluate(expr.expr, upstream)); } else if (isEffectExpr(expr)) { // TODO(sam): the same effect shoudl be memoized so that it's not run multiple times return yield* expr.f(yield* evaluate(expr.expr, upstream)); } else if (isFlatMapExpr(expr)) { // Resolve the source, hand it to `f` to produce a new Output, then // recursively evaluate that Output (flattening one level). const value = yield* evaluate(expr.expr, upstream); return yield* evaluate(expr.f(value), upstream); } else if (isAllExpr(expr)) { return yield* Effect.all( expr.outs.map((out) => evaluate(out, upstream)), ); } else if (isPropExpr(expr)) { return (yield* evaluate(expr.expr, upstream))?.[expr.identifier]; } else if (isNamedExpr(expr)) { return yield* evaluate(expr.expr, upstream); } else if (isRefExpr(expr)) { const state = yield* yield* State.State; const stack = expr.stack ?? (yield* Stack).name; const stage = expr.stage ?? (yield* Stage); const resource = yield* state.get({ stack, stage, fqn: expr.resourceId, }); if (!resource) { return yield* Effect.fail( new InvalidReferenceError({ message: `Reference to '${expr.resourceId}' in stack '${stack}' and stage '${stage}' not found. Have you deployed '${stage}' or '${stack}'?`, stack, stage, resourceId: expr.resourceId, }), ); } // RefExpr targets persisted resources; tasks aren't cross-stack // referenceable. Return the resource's output attrs, otherwise the // task's output value, otherwise undefined. return (resource as any).attr ?? (resource as any).output; } else if (isStackRefExpr(expr)) { const state = yield* yield* State.State; const stack = expr.stack; const stage = expr.stage ?? (yield* Stage); const output = yield* state.getOutput({ stack, stage }); if (output == null) { return yield* Effect.fail( new InvalidReferenceError({ message: `Reference to stack '${stack}' at stage '${stage}' not found. Have you deployed stage '${stage}' of '${stack}'?`, stack, stage, resourceId: stack, }), ); } return output; } } if (Config.isConfig(expr)) { // Resolve Config against the deploy environment — see resolveInput in // Plan.ts for rationale. `Config.redacted` resolves to a `Redacted`, // which stays opaque via the leaf fallthrough below. return yield* evaluate(yield* expr, upstream, ancestors); } else if (isPlainData(expr)) { if (ancestors.has(expr)) { return undefined; } const nested = new Set(ancestors).add(expr); if (Array.isArray(expr)) { return yield* Effect.all( expr.map((item) => evaluate(item, upstream, nested)), ); } return Object.fromEntries( yield* Effect.all( Object.entries(expr).map(([key, value]) => evaluate(value, upstream, nested).pipe( Effect.map((value) => [key, value]), ), ), ), ); } // Everything else is a leaf returned by identity: Duration, Redacted, // Date, and effect runtime values (a Worker's `exports` carries each // DO's `constructor` Effect and captured `services` Context). Rebuilding // a class instance entry-by-entry strips its prototype, and effect // ≥4.0.0-beta.103's Context is cyclic (#1082). This sits after // `Config.isConfig` on purpose — Configs are Effects but must resolve. return expr; }) as Effect.Effect; export const hasOutputs = (value: any): value is Output => Object.keys(upstreamAny(value)).length > 0; /** * Cycle guard shared by the upstream walkers. Marks `value` as visited in * `seen`; returns true when it was already visited (the caller returns `{}` * — the first visit already contributed the subtree's resources to the * FQN-keyed union, so skipping repeats is lossless). */ const alreadySeen = (value: object, seen: WeakSet): boolean => { if (seen.has(value)) { return true; } seen.add(value); return false; }; // The dependency rule (#1082): a Resource or Output IS a dependency; plain // data (arrays, plain objects) is traversed to find them; every other value // — class instances like effect's Effect/Layer/Context, Dates, SDK objects, // functions — is a leaf. See `isPlainData` in Util/data.ts for why leaves // must never be walked. export const upstreamAny = ( value: any, seen: WeakSet = new WeakSet(), ): { [ID in string]: Resource; } => { if (isResource(value)) { return { [value.FQN]: value as Resource }; } else if (isExpr(value)) { return upstream(value, seen); } else if (isPlainData(value)) { if (alreadySeen(value, seen)) { return {}; } return Object.assign( {}, ...Object.values(value).map((value) => resolveUpstream(value, seen)), ); } return {}; }; // TODO(sam): add a type export const upstream = >( expr: E, seen: WeakSet = new WeakSet(), ): any => { if (isResource(expr)) { return { [(expr as unknown as Resource).FQN]: expr, }; } else if (isResourceExpr(expr)) { return { [expr.src.FQN]: expr.src, }; } else if (isPropExpr(expr)) { return upstream(expr.expr, seen); } else if (isAllExpr(expr)) { return Object.assign({}, ...expr.outs.map((out) => upstream(out, seen))); } else if ( isEffectExpr(expr) || isApplyExpr(expr) || isFlatMapExpr(expr) || isNamedExpr(expr) ) { return upstream(expr.expr, seen); } else if (isPlainData(expr)) { if (alreadySeen(expr, seen)) { return {}; } return Object.values(expr) .map((v) => upstream(v as any, seen)) .reduce(toObject, {}); } return {}; }; // TODO(sam): add a type export const resolveUpstream = ( value: A, seen: WeakSet = new WeakSet(), ): any => { if (isPrimitive(value)) { return {} as any; } else if (isResource(value)) { return { [(value as unknown as Resource).FQN]: value } as any; } else if (isOutput(value)) { return upstream(value, seen) as any; } else if (isPlainData(value)) { if (alreadySeen(value, seen)) { return {} as any; } return Object.fromEntries( Object.values(value) .map((v) => resolveUpstream(v, seen)) .flatMap(Object.entries), ) as any; } return {} as any; }; const toObject = (acc: B, v: A) => ({ ...acc, ...v, }); export const log = (_value: A) => Effect.gen(function* () { // TODO(sam): implement a log effect }); export const toEnvKey = ( id: ID, suffix: Suffix, ) => `${replace(toUpper(id))}_${replace(toUpper(suffix))}` as const; export const toUpper = (str: S) => str.toUpperCase() as string extends S ? S : Uppercase; const replace = (str: S) => str.replace(/-/g, "_") as Replace; type Replace = string extends S ? S : S extends "" ? Accum : S extends `${infer S}${infer Rest}` ? S extends "-" ? Replace : Replace : Accum;