| 1 | import * as Data from "effect/Data"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Effectable from "effect/Effectable"; |
| 4 | import * as Layer from "effect/Layer"; |
| 5 | import * as Option from "effect/Option"; |
| 6 | import type { Pipeable } from "effect/Pipeable"; |
| 7 | import { AdoptPolicy } from "./AdoptPolicy.ts"; |
| 8 | import { toFqn } from "./FQN.ts"; |
| 9 | import type { Input, InputProps, PropsInput } from "./Input.ts"; |
| 10 | import { CurrentNamespace, type NamespaceNode } from "./Namespace.ts"; |
| 11 | import * as Output from "./Output.ts"; |
| 12 | import { Provider } from "./Provider.ts"; |
| 13 | import { |
| 14 | ConflictingProviderModeError, |
| 15 | ProviderModePolicy, |
| 16 | type ProviderMode, |
| 17 | } from "./ProviderMode.ts"; |
| 18 | import { ref as makeRef } from "./Ref.ts"; |
| 19 | import { RemovalPolicy } from "./RemovalPolicy.ts"; |
| 20 | import { RenamePolicy } from "./Rename.ts"; |
| 21 | import { Self } from "./Self.ts"; |
| 22 | import { Stack } from "./Stack.ts"; |
| 23 | |
| 24 | export type ResourceConstructor<R extends ResourceLike, Req = never> = { |
| 25 | Type: R["Type"]; |
| 26 | Props: R["Props"]; |
| 27 | <const Methods extends { [key: string]: any }>( |
| 28 | methods: Methods, |
| 29 | ): ResourceClassWithMethods<R, Methods>; |
| 30 | ( |
| 31 | id: string, |
| 32 | // PropsInput distributes over union Props so discriminated-union |
| 33 | // resources keep the correlation between discriminant and payload. |
| 34 | ...args: {} extends R["Props"] |
| 35 | ? [props?: PropsInput<R["Props"]>] |
| 36 | : [props: PropsInput<R["Props"]>] |
| 37 | ): Effect.Effect<R, never, Req>; |
| 38 | <PropsReq = never>( |
| 39 | id: string, |
| 40 | props: Effect.Effect<InputProps<R["Props"]>, never, PropsReq>, |
| 41 | ): Effect.Effect<R, never, PropsReq | Req>; |
| 42 | }; |
| 43 | |
| 44 | export interface ResourceClassLike<R extends ResourceLike> { |
| 45 | Type: R["Type"]; |
| 46 | Props: R["Props"]; |
| 47 | Self: Self<R>; |
| 48 | Provider: Provider<R>; |
| 49 | /** |
| 50 | * Legacy type names this resource was previously registered under |
| 51 | * (see {@link ResourceOptions.aliases}). Copied onto the |
| 52 | * `ProviderService` by `Provider.succeed`/`Provider.effect` so provider |
| 53 | * lookup can resolve state persisted under a pre-rename type. |
| 54 | * |
| 55 | * `undefined` is accepted explicitly so `ResourceClass` (whose `Aliases` |
| 56 | * is `readonly string[] | undefined`) stays assignable to |
| 57 | * `ResourceClassLike` under `exactOptionalPropertyTypes`. |
| 58 | */ |
| 59 | Aliases?: readonly string[] | undefined; |
| 60 | } |
| 61 | |
| 62 | export type ResourceClass<R extends ResourceLike> = ResourceConstructor< |
| 63 | R, |
| 64 | R["Providers"] extends undefined ? Provider<R> : R["Providers"] |
| 65 | > & |
| 66 | Effect.Effect<ResourceConstructor<R>> & { |
| 67 | Self: Self<R>; |
| 68 | Provider: Provider<R>; |
| 69 | Aliases: readonly string[] | undefined; |
| 70 | ref( |
| 71 | id: string, |
| 72 | options?: { stage?: string; stack?: string }, |
| 73 | ): Effect.Effect<R>; |
| 74 | }; |
| 75 | |
| 76 | export type ResourceClassWithMethods< |
| 77 | R extends ResourceLike, |
| 78 | Methods extends { [key: string]: any }, |
| 79 | > = ResourceConstructor< |
| 80 | R, |
| 81 | R["Providers"] extends undefined ? Provider<R> : R["Providers"] |
| 82 | > & |
| 83 | Effect.Effect<ResourceConstructor<R>> & { |
| 84 | Self: Self<R>; |
| 85 | Provider: Provider<R>; |
| 86 | Aliases: readonly string[] | undefined; |
| 87 | ref( |
| 88 | id: string, |
| 89 | options?: { stage?: string; stack?: string }, |
| 90 | ): Effect.Effect<R>; |
| 91 | } & Methods; |
| 92 | |
| 93 | export type LogicalId = string; |
| 94 | |
| 95 | export interface ResourceBinding<Data = any> { |
| 96 | sid: string; |
| 97 | data: Data; |
| 98 | } |
| 99 | |
| 100 | export interface ResourceLike< |
| 101 | Type extends string = string, |
| 102 | Props extends object | undefined = any, |
| 103 | Attributes extends object = object, |
| 104 | Binding = any, |
| 105 | Providers = any, |
| 106 | > { |
| 107 | /** |
| 108 | * Namespace containing this Resource. |
| 109 | */ |
| 110 | Namespace: NamespaceNode | undefined; |
| 111 | /** |
| 112 | * Fully Qualified Name (namespace path + logical ID). |
| 113 | * Used as the unique key for state storage. |
| 114 | */ |
| 115 | FQN: string; |
| 116 | /** |
| 117 | * Type of the Resource (e.g. AWS.Lambda.Function) |
| 118 | */ |
| 119 | Type: Type; |
| 120 | /** |
| 121 | * Logical ID of the Resource (e.g. MyFunction) |
| 122 | */ |
| 123 | LogicalId: LogicalId; |
| 124 | /** |
| 125 | * Properties of the Resource. |
| 126 | */ |
| 127 | Props: Props; |
| 128 | /** |
| 129 | * Removal Policy of the Resource. |
| 130 | */ |
| 131 | RemovalPolicy: RemovalPolicy["Service"]; |
| 132 | /** |
| 133 | * Per-resource adoption policy captured from the ambient {@link AdoptPolicy} |
| 134 | * at registration time (e.g. via `.pipe(adopt(true))`). `undefined` means no |
| 135 | * resource-scoped override — the planner falls back to the stack/CLI default. |
| 136 | */ |
| 137 | Adopt: boolean | undefined; |
| 138 | /** |
| 139 | * Per-resource provider mode captured from the ambient |
| 140 | * {@link ProviderModePolicy} at registration time. `"live"` when the |
| 141 | * resource was pinned via `.pipe(remote())` (opting out of local emulation |
| 142 | * during dev); `undefined` means the run default (`AlchemyContext.dev`). |
| 143 | */ |
| 144 | Mode: ProviderMode | undefined; |
| 145 | /** |
| 146 | * Copied from {@link ResourceOptions.requiresImplementation} at |
| 147 | * registration: `true` for platform-typed resources, whose registrations |
| 148 | * must have resolved {@link Props} by plan time. `Plan.make` fails fast |
| 149 | * with {@link MissingImplementationError} when this is set and `Props` |
| 150 | * are still `undefined` after the whole program has evaluated — a bare |
| 151 | * tag was yielded but its `.make(props, impl)` Layer was never provided. |
| 152 | */ |
| 153 | RequiresImplementation: boolean | undefined; |
| 154 | /** |
| 155 | * Former FQNs this resource's state may still be persisted under, |
| 156 | * captured from the ambient {@link RenamePolicy} at registration (via |
| 157 | * `.pipe(renamedFrom("OldId"))`) and resolved against the same namespace |
| 158 | * as the resource's own FQN. The planner migrates a state row found at a |
| 159 | * former FQN to {@link FQN} instead of planning a create+delete |
| 160 | * replacement — see `renamedFrom` in Rename.ts for the full semantics. |
| 161 | */ |
| 162 | FormerFqns: readonly string[] | undefined; |
| 163 | /** @internal phantom */ |
| 164 | Attributes: Attributes; |
| 165 | /** @internal phantom */ |
| 166 | Binding: Binding; |
| 167 | /** @internal phantom */ |
| 168 | Providers: Providers; |
| 169 | } |
| 170 | |
| 171 | export const isResource = (value: any): value is ResourceLike => { |
| 172 | // Require the full resource identity (Type AND FQN), not just `Type`: |
| 173 | // user-authored prop objects legitimately carry a `Type` field (e.g. |
| 174 | // Amazon States Language states like `{ Type: "Pass", End: true }` in a |
| 175 | // Step Functions definition) and must not be mistaken for resources. |
| 176 | // Locally-declared resources always expose both as own keys; refs |
| 177 | // (`Resource.ref(...)`) deliberately report neither via `in` so they keep |
| 178 | // routing through Output resolution. |
| 179 | return ( |
| 180 | typeof value === "object" && |
| 181 | value !== null && |
| 182 | "Type" in value && |
| 183 | "FQN" in value |
| 184 | ); |
| 185 | }; |
| 186 | |
| 187 | /** |
| 188 | * Does `value` reference an instance of the resource type `type` — |
| 189 | * either a locally-declared resource or a `Resource.ref(...)` to one? |
| 190 | * |
| 191 | * Two constraints that ad-hoc guards get wrong for refs, which resolve |
| 192 | * to Output-expression proxies: |
| 193 | * |
| 194 | * - Read `.Type` via property access (never `in`): the proxy answers |
| 195 | * property reads with statically-known values but deliberately does |
| 196 | * not report key existence (so {@link isResource} keeps routing refs |
| 197 | * through Output resolution instead of the upstream-node lookup). |
| 198 | * - Accept `typeof value === "function"`: the proxy's target is |
| 199 | * callable (it needs an `apply` trap), so refs are not `"object"`. |
| 200 | * |
| 201 | * Either mistake silently rejects refs — in a Worker `env` that |
| 202 | * degrades the binding to a plain JSON var. |
| 203 | */ |
| 204 | export const isResourceOfType = (value: unknown, type: string): boolean => |
| 205 | (typeof value === "object" || typeof value === "function") && |
| 206 | value !== null && |
| 207 | (value as { Type?: unknown }).Type === type; |
| 208 | |
| 209 | export type Resource< |
| 210 | Type extends string = any, |
| 211 | Props extends object | undefined = any, |
| 212 | Attributes extends object = any, |
| 213 | Binding = never, |
| 214 | Providers = undefined, |
| 215 | > = Pipeable & |
| 216 | ResourceLike<Type, Props, Attributes, Binding, Providers> & { |
| 217 | bind(sid: Input<string>, binding: Input<Binding>): Effect.Effect<void>; |
| 218 | bind( |
| 219 | template: TemplateStringsArray, |
| 220 | ...args: any[] |
| 221 | ): (binding: Input<Binding>) => Effect.Effect<void>; |
| 222 | } & { |
| 223 | [attr in keyof Attributes]-?: AttrOutput<Attributes[attr]>; |
| 224 | }; |
| 225 | |
| 226 | /** |
| 227 | * Accessor type for one attribute. Pure object attributes upgrade to |
| 228 | * {@link Output.ObjectExpr} so nested access is typed — |
| 229 | * `hyperpod.instanceGroups.workers` — while primitives, unions with |
| 230 | * `undefined`, branded string unions (`"a" | (string & {})`), and arrays |
| 231 | * stay plain {@link Output.Output} (running them through `ToOutput` would |
| 232 | * classify the `string & {}` branch as an object and explode into |
| 233 | * String-method mapped types). |
| 234 | */ |
| 235 | type AttrOutput<A> = [A] extends [ |
| 236 | string | number | boolean | bigint | null | undefined | Date | any[], |
| 237 | ] |
| 238 | ? Output.Output<A, never> |
| 239 | : [A] extends [Record<string, any>] |
| 240 | ? Output.ObjectExpr<A, never> |
| 241 | : Output.Output<A, never>; |
| 242 | |
| 243 | export interface ResourceOptions { |
| 244 | /** |
| 245 | * Default removal policy for this resource type when the caller has not |
| 246 | * explicitly provided one via `RemovalPolicy` / `destroy()` / `retain()`. |
| 247 | * |
| 248 | * Useful for resources that wrap unrecoverable real-world identifiers |
| 249 | * (DNS zones, customer accounts, etc.) where the safe default is to |
| 250 | * leave the cloud object alone on stack destroy. |
| 251 | * |
| 252 | * @default "destroy" |
| 253 | */ |
| 254 | defaultRemovalPolicy?: RemovalPolicy["Service"]; |
| 255 | /** |
| 256 | * Legacy type names this resource was previously registered under. |
| 257 | * |
| 258 | * When a resource type is renamed (e.g. `"Cloudflare.Queue"` → |
| 259 | * `"Cloudflare.Queues.Queue"`), state persisted under the old name must |
| 260 | * still resolve to this resource's provider. Listing the old names here |
| 261 | * makes provider lookup fall back from the legacy name to this type, so |
| 262 | * existing stacks keep planning, updating, and deleting cleanly across |
| 263 | * the rename. The state row migrates to the new type on its next write. |
| 264 | * |
| 265 | * ```ts |
| 266 | * export const Queue = Resource<Queue>("Cloudflare.Queues.Queue", { |
| 267 | * aliases: ["Cloudflare.Queue"], |
| 268 | * }); |
| 269 | * ``` |
| 270 | */ |
| 271 | aliases?: string[]; |
| 272 | /** |
| 273 | * Marks every registration of this type as requiring resolved props by |
| 274 | * plan time. Set by `Platform(...)` on its resource class: every |
| 275 | * legitimate platform construction (a `.make(props, impl)` Layer build, |
| 276 | * a tag declared with props, a plain `Worker("id", props)` call) produces |
| 277 | * defined `Props` — the only way a platform-typed registration reaches |
| 278 | * the planner with `Props === undefined` is a bare-tag FORWARD REFERENCE |
| 279 | * whose `.make` Layer never built. `Plan.make` fails fast with |
| 280 | * {@link MissingImplementationError} in that case, instead of letting a |
| 281 | * provider read `undefined` props. Plain (non-platform) resources leave |
| 282 | * this unset so a no-props reference yield (`yield* Queue("MyQueue")`) |
| 283 | * keeps planning as a noop. |
| 284 | */ |
| 285 | requiresImplementation?: boolean; |
| 286 | } |
| 287 | |
| 288 | /** |
| 289 | * A tagged platform resource declared with neither props nor an inline |
| 290 | * implementation was `yield*`ed, but its `.make(props, impl)` Layer never |
| 291 | * built. |
| 292 | * |
| 293 | * Such a tag carries no configuration on its own — props AND impl both live |
| 294 | * on the Layer — so its registration is a forward reference with `undefined` |
| 295 | * props that the Layer's build repairs (in either order; see the #874 |
| 296 | * circular env-tag pattern). Props still `undefined` once the whole program |
| 297 | * has evaluated means the Layer was never provided; `Plan.make` fails fast |
| 298 | * with this error instead of letting the failure surface deep inside |
| 299 | * whichever provider first reads a prop (e.g. `TypeError: undefined is not |
| 300 | * an object (evaluating 'news.name')` in the Cloudflare Worker pre-create). |
| 301 | */ |
| 302 | export class MissingImplementationError extends Data.TaggedError( |
| 303 | "MissingImplementationError", |
| 304 | )<{ |
| 305 | message: string; |
| 306 | /** Resource type of the platform, e.g. `Cloudflare.Worker`. */ |
| 307 | type: string; |
| 308 | /** Logical id of the tagged resource (its class name by convention). */ |
| 309 | id: string; |
| 310 | }> {} |
| 311 | |
| 312 | export const missingImplementation = (type: string, id: string) => |
| 313 | new MissingImplementationError({ |
| 314 | type, |
| 315 | id, |
| 316 | message: [ |
| 317 | `${type}<${id}> was yielded without its implementation.`, |
| 318 | "", |
| 319 | `\`${id}\` is declared as a bare tag — no props, no inline implementation — so both come from its \`.make(...)\` Layer:`, |
| 320 | "", |
| 321 | ` export class ${id} extends ${type}<${id}>()("${id}") {}`, |
| 322 | ` export const ${id}Live = ${id}.make({ /* props */ }, Effect.gen(function* () { /* ... */ }));`, |
| 323 | "", |
| 324 | `That Layer is not in scope where \`${id}\` is yielded. Provide it to the Stack's program:`, |
| 325 | "", |
| 326 | " Alchemy.Stack(", |
| 327 | ` "my-stack",`, |
| 328 | " { providers, state },", |
| 329 | " Effect.gen(function* () {", |
| 330 | ` const instance = yield* ${id};`, |
| 331 | ` }).pipe(Effect.provide([${id}Live])),`, |
| 332 | " )", |
| 333 | "", |
| 334 | `If \`${id}\` is not Effect-native, declare it with props instead: \`()("${id}", { /* props */ })\`.`, |
| 335 | ].join("\n"), |
| 336 | }); |
| 337 | |
| 338 | /** |
| 339 | * Creates a resource constructor for a concrete resource type. |
| 340 | * |
| 341 | * The returned constructor registers the resource on the current stack, |
| 342 | * resolves input props, exposes output attributes as `Output` expressions, and |
| 343 | * records bindings contributed by policies and event sources. Resource |
| 344 | * providers are attached separately through `.provider`. |
| 345 | */ |
| 346 | export function Resource<R extends ResourceLike>( |
| 347 | type: R["Type"], |
| 348 | options?: ResourceOptions, |
| 349 | ): ResourceClass<R> { |
| 350 | const defaultRemovalPolicy = options?.defaultRemovalPolicy ?? "destroy"; |
| 351 | type Props = Input<R["Props"]>; |
| 352 | const self = Self<R>(type); |
| 353 | const constructor = ( |
| 354 | id: string, |
| 355 | props: Props | Effect.Effect<Props> | undefined, |
| 356 | ) => |
| 357 | Effect.gen(function* () { |
| 358 | const stack = yield* Stack; |
| 359 | const namespace = yield* CurrentNamespace; |
| 360 | const fqn = toFqn(namespace, id); |
| 361 | |
| 362 | // `remote()` opts resources out of local emulation during dev. The |
| 363 | // captured Mode is either "live" (pinned) or undefined (run default). |
| 364 | // The Reference default is `undefined` — "no explicit decoration" — |
| 365 | // which is distinct from an explicit `remote(false)`. |
| 366 | const ambientPolicy = yield* ProviderModePolicy; |
| 367 | const ambientMode: ProviderMode | undefined = ambientPolicy |
| 368 | ? "live" |
| 369 | : undefined; |
| 370 | |
| 371 | const existing = stack.resources[fqn]; |
| 372 | if (existing) { |
| 373 | // A resource may be `yield*`ed from several places (idempotent |
| 374 | // registration). If a later site carries an *explicit* ambient |
| 375 | // ProviderModePolicy that disagrees with what the resource was |
| 376 | // registered with, the decorations are conflicting — fail loudly |
| 377 | // instead of silently picking one. A later site with NO ambient |
| 378 | // policy simply inherits the registered resource (the common |
| 379 | // "reference it from elsewhere" pattern). |
| 380 | if (ambientPolicy !== undefined && existing.Mode !== ambientMode) { |
| 381 | return yield* Effect.die( |
| 382 | new ConflictingProviderModeError({ |
| 383 | message: |
| 384 | `Resource '${fqn}' was registered with provider mode ` + |
| 385 | `'${existing.Mode ?? "default"}' but is now being registered ` + |
| 386 | `with conflicting mode '${ambientMode ?? "default"}'. A ` + |
| 387 | "resource must resolve to a single provider mode: register " + |
| 388 | "it once and close over the returned value, or make both " + |
| 389 | "registration sites agree (e.g. wrap both in the same " + |
| 390 | "`remote()` scope).", |
| 391 | fqn, |
| 392 | existingMode: existing.Mode, |
| 393 | conflictingMode: ambientMode, |
| 394 | }), |
| 395 | ); |
| 396 | } |
| 397 | // // TODO(sam): check if props are different and die |
| 398 | return existing; |
| 399 | } |
| 400 | const bind = ( |
| 401 | ...args: |
| 402 | | [sid: string, data: R["Binding"]] |
| 403 | | [template: TemplateStringsArray, ...args: any[]] |
| 404 | ) => |
| 405 | typeof args[0] === "string" |
| 406 | ? Effect.gen(function* () { |
| 407 | const [sid, data] = args as [sid: string, data: R["Binding"]]; |
| 408 | (stack.bindings[fqn] ??= []).push({ |
| 409 | sid, |
| 410 | data, |
| 411 | }); |
| 412 | return undefined; |
| 413 | }) |
| 414 | : (data: R["Binding"]) => { |
| 415 | const stringifyBindArg = (arg: any): string | undefined => { |
| 416 | if (arg === undefined) { |
| 417 | return undefined; |
| 418 | } |
| 419 | |
| 420 | if (Array.isArray(arg)) { |
| 421 | return arg |
| 422 | .flatMap((item) => { |
| 423 | const stringified = stringifyBindArg(item); |
| 424 | return stringified === undefined ? [] : [stringified]; |
| 425 | }) |
| 426 | .join(", "); |
| 427 | } |
| 428 | |
| 429 | if ( |
| 430 | arg && |
| 431 | (typeof arg === "object" || typeof arg === "function") |
| 432 | ) { |
| 433 | if ("LogicalId" in arg && typeof arg.LogicalId === "string") { |
| 434 | return arg.LogicalId; |
| 435 | } |
| 436 | |
| 437 | if ("id" in arg && typeof arg.id === "string") { |
| 438 | return arg.id; |
| 439 | } |
| 440 | } |
| 441 | |
| 442 | return String(arg); |
| 443 | }; |
| 444 | |
| 445 | return bind( |
| 446 | `${(args[0] as TemplateStringsArray) |
| 447 | .flatMap((text, i) => { |
| 448 | const stringified = stringifyBindArg(args[i + 1]); |
| 449 | return stringified !== undefined |
| 450 | ? [text, stringified] |
| 451 | : [text]; |
| 452 | }) |
| 453 | .join("")}`, |
| 454 | data, |
| 455 | ); |
| 456 | }; |
| 457 | |
| 458 | const target: any = { |
| 459 | Type: type, |
| 460 | Namespace: namespace, |
| 461 | FQN: fqn, |
| 462 | LogicalId: id, |
| 463 | Props: props, |
| 464 | Provider: ProviderTag as Provider<any>, |
| 465 | RemovalPolicy: yield* Effect.serviceOption(RemovalPolicy).pipe( |
| 466 | Effect.map(Option.getOrElse(() => defaultRemovalPolicy)), |
| 467 | ), |
| 468 | Adopt: yield* Effect.serviceOption(AdoptPolicy).pipe( |
| 469 | Effect.map(Option.getOrUndefined), |
| 470 | ), |
| 471 | Mode: ambientMode, |
| 472 | RequiresImplementation: options?.requiresImplementation || undefined, |
| 473 | // Bare-string former ids resolve against the SAME namespace as the |
| 474 | // resource's own id, so `renamedFrom("Site/Worker")` declared at the |
| 475 | // caller's level claims `<callerNs>/Site/Worker`; the `{ fqn }` form |
| 476 | // is absolute (cross-namespace moves). |
| 477 | FormerFqns: yield* Effect.serviceOption(RenamePolicy).pipe( |
| 478 | Effect.map( |
| 479 | Option.match({ |
| 480 | onNone: () => undefined, |
| 481 | onSome: (formerIds) => |
| 482 | formerIds.map((formerId) => |
| 483 | typeof formerId === "string" |
| 484 | ? toFqn(namespace, formerId) |
| 485 | : formerId.fqn, |
| 486 | ), |
| 487 | }), |
| 488 | ), |
| 489 | ), |
| 490 | bind, |
| 491 | toString(this: typeof target) { |
| 492 | return `Resource<${this.Type}>(${this.LogicalId})`; |
| 493 | }, |
| 494 | [Symbol.toPrimitive](this: typeof target, hint: string) { |
| 495 | return hint === "number" ? NaN : this.toString(); |
| 496 | }, |
| 497 | }; |
| 498 | |
| 499 | const Resource: R = (stack.resources[fqn] = new Proxy(target, { |
| 500 | set: (t, prop, value) => { |
| 501 | t[prop as keyof typeof t] = value; |
| 502 | return true; |
| 503 | }, |
| 504 | get: (t, prop) => |
| 505 | typeof prop === "symbol" || prop in t |
| 506 | ? t[prop as keyof typeof t] |
| 507 | : new Output.PropExpr<any, string>(Output.of(Resource), prop), |
| 508 | })) as R; |
| 509 | Resource.Props = Effect.isEffect(props) |
| 510 | ? // @effect-diagnostics-next-line anyUnknownInErrorContext:off |
| 511 | yield* props.pipe( |
| 512 | Effect.provide( |
| 513 | Layer.mergeAll( |
| 514 | Layer.succeed(Self, Resource), |
| 515 | Layer.succeed(Self(type), Resource), |
| 516 | ), |
| 517 | ), |
| 518 | ) |
| 519 | : props; |
| 520 | return Resource; |
| 521 | }); |
| 522 | |
| 523 | const ProviderTag = Provider(type); |
| 524 | |
| 525 | const Service = { |
| 526 | /** |
| 527 | * Build a typed reference to a deployed instance of this resource |
| 528 | * — in the current stack/stage by default, or in another via |
| 529 | * `options`. Resolves to the same shape as `yield* |
| 530 | * MyResource("id", props)` so downstream code can read attributes |
| 531 | * (`ref.someAttr`) exactly the way it would for a locally-declared |
| 532 | * resource. |
| 533 | */ |
| 534 | ref: ( |
| 535 | id: string, |
| 536 | options?: { stage?: string; stack?: string }, |
| 537 | ): Effect.Effect<R> => |
| 538 | Effect.succeed(Output.of(makeRef<R>(id, options, type)) as unknown as R), |
| 539 | |
| 540 | Type: type, |
| 541 | Provider: ProviderTag, |
| 542 | Self: self, |
| 543 | Aliases: options?.aliases, |
| 544 | }; |
| 545 | |
| 546 | const ResourceClass = Object.assign( |
| 547 | (...args: [id: string, props: R["Props"]] | [methods: object]) => |
| 548 | typeof args[0] === "object" |
| 549 | ? Object.assign(ResourceClass, args[0]) |
| 550 | : constructor(...(args as [string, R["Props"]])), |
| 551 | Service, |
| 552 | // Make the constructor itself a real Effect: `yield* MyResource` resolves |
| 553 | // to the constructor function (same as the old `asEffect()`), and |
| 554 | // `Effect.isEffect(MyResource)` is now true so `Effect.all`/`forEach` work. |
| 555 | Effectable.Prototype({ |
| 556 | label: `Resource<${type}>`, |
| 557 | evaluate: () => |
| 558 | Effect.succeed((id: string, props: R["Props"]) => |
| 559 | constructor(id, props), |
| 560 | ), |
| 561 | }), |
| 562 | ) as any; |
| 563 | |
| 564 | return ResourceClass; |
| 565 | } |