import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import { SingleShotGen } from "effect/Utils"; import { ExecutionContext } from "./ExecutionContext.ts"; import * as Namespace from "./Namespace.ts"; import { ALCHEMY_PHASE } from "./Phase.ts"; import type { ResourceLike } from "./Resource.ts"; import { Self } from "./Self.ts"; import { CurrentStack } from "./Stack.ts"; export interface ServiceLike { kind: "Service"; } export interface ServiceShape< Identifier extends string, Shape extends (...args: any[]) => Effect.Effect, > extends Context.ServiceClass.Shape, ServiceLike {} export interface Service< Self, Identifier extends string, Shape extends (...args: any[]) => Effect.Effect, > extends Context.Service, ServiceLike { readonly key: Identifier; new (_: never): ServiceShape; bind: ( ...args: BindParameters, Req> ) => Effect.Effect< Effect.Success>, Effect.Error>, Self | Effect.Services> | Req >; } type BindParameters< Parameters extends any[], Req = never, > = Parameters extends [infer First, ...infer Rest] ? [First | Effect.Effect, ...BindParameters] : []; /** * Creates a runtime binding service. * * A `Binding.Service` is the runtime-facing half of an operation such as * `GetItem`, `PutObject`, or `Fetch`. It is provided on the function or worker * effect so user code can call `.bind(resource)` and receive a typed runtime * API that already knows how to talk to the target resource. */ export const Service = Effect.Effect>() => (id: Identifier) => { const self = Context.Service(id) as Service< Self, Identifier, Shape >; return Object.assign(self, { bind: (...args: Parameters) => self.use((f) => Effect.all( args.map((arg) => Effect.isEffect(arg) ? arg : Effect.succeed(arg), ), { concurrency: "unbounded", }, ).pipe(Effect.flatMap((args) => f(...args))), ), }); }; export interface PolicyLike { kind: "Policy"; } export interface PolicyShape< Identifier extends string, Shape extends (...args: any[]) => Effect.Effect, > extends Context.ServiceClass.Shape, PolicyLike {} export interface Policy< in out Self, in out Identifier extends string, in out Shape extends (...args: any[]) => Effect.Effect, > extends Effect.Effect { readonly key: Identifier; new (_: never): PolicyShape; layer: { succeed( fn: ( ctx: ResourceLike, ...args: Parameters ) => Effect.Effect, ): Layer.Layer; effect( fn: Effect.Effect< (ctx: ResourceLike, ...args: Parameters) => Effect.Effect, never, Req >, ): Layer.Layer; }; bind( ...args: Parameters ): Effect.Effect< Effect.Success>, Effect.Error>, Self | ExecutionContext | Effect.Services> >; } /** * Creates a deploy-time binding policy. * * A `Binding.Policy` attaches the infrastructure-side permissions or bindings * that make a runtime binding usable. At deploy time it records IAM statements * or host bindings on the target function/worker. At runtime the layer is * absent, so the policy gracefully becomes a no-op. */ export const Policy = Effect.Effect>() => ( Identifier: Identifier, ): Policy`, Shape> => { const self = Context.Service(`Policy<${Identifier}>`); // we use a service option because at runtime (e.g. in a Lambda Function or Cloudflare Worker) // the Policy Layer is not provided and this becomes a no-op const Service = Effect.serviceOption(self) .asEffect() .pipe( Effect.map(Option.getOrUndefined), Effect.flatMap((service) => service ? Effect.succeed(service) : Effect.all([CurrentStack, ALCHEMY_PHASE.asEffect()]).pipe( Effect.flatMap(([stack, phase]) => stack && phase === "plan" ? Effect.die( `Binding.Policy provider '${Identifier}' was not provided at Plan Time in Stack '${stack.name}'`, ) : Effect.succeed((() => Effect.void) as any as Shape), ), ), ), ); const asEffect = () => Effect.all([Self.asEffect(), Service]).pipe( Effect.map( ([resource, fn]) => (...args: any[]) => Effect.all( args.map((arg) => Effect.isEffect(arg) ? arg : Effect.succeed(arg), ), ).pipe( Effect.flatMap((args) => fn(...args).pipe( Namespace.push((resource as ResourceLike).LogicalId), ), ), ), ), ); // @ts-expect-error return Object.assign(self, { [Symbol.iterator]() { return new SingleShotGen(this); }, asEffect, bind: (...args: any[]) => asEffect().pipe(Effect.flatMap((fn) => fn(...args))), layer: { succeed: ( fn: ( self: ResourceLike, ...args: Parameters ) => Effect.Effect, ) => Layer.succeed( self, // @ts-expect-error (...args: Parameters) => Self.asEffect().pipe( Effect.flatMap((self) => fn(self as ResourceLike, ...args)), ), ), effect: ( fn: Effect.Effect< ( self: ResourceLike, ...args: Parameters ) => Effect.Effect >, ) => Layer.effect( self, // @ts-expect-error Effect.map( fn, (fn) => (...args: Parameters) => Effect.flatMap(Self.asEffect(), (self) => fn(self as ResourceLike, ...args), ), ), ), }, }); };