alchemy-effect 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/bin/alchemy-effect.js +9 -7
  2. package/bin/alchemy-effect.js.map +1 -1
  3. package/bin/alchemy-effect.ts +11 -0
  4. package/package.json +1 -1
  5. package/src/AWS/AutoScaling/LaunchTemplate.ts +5 -4
  6. package/src/AWS/EC2/Instance.ts +3 -4
  7. package/src/AWS/ECS/Task.ts +36 -34
  8. package/src/AWS/Lambda/Function.ts +68 -68
  9. package/src/Binding.ts +10 -3
  10. package/src/Cloudflare/Container.ts +68 -67
  11. package/src/Cloudflare/D1/D1Database.ts +32 -32
  12. package/src/Cloudflare/KV/Delete.ts +7 -5
  13. package/src/Cloudflare/KV/Get.ts +5 -5
  14. package/src/Cloudflare/KV/GetWithMetadata.ts +5 -5
  15. package/src/Cloudflare/KV/{Namespace.ts → KVNamespace.ts} +6 -6
  16. package/src/Cloudflare/KV/{NamespaceBinding.ts → KVNamespaceBinding.ts} +2 -2
  17. package/src/Cloudflare/KV/List.ts +5 -5
  18. package/src/Cloudflare/KV/Put.ts +5 -5
  19. package/src/Cloudflare/KV/index.ts +1 -1
  20. package/src/Cloudflare/Providers.ts +2 -8
  21. package/src/Cloudflare/R2/{Bucket.ts → R2Bucket.ts} +19 -19
  22. package/src/Cloudflare/R2/R2BucketBinding.ts +299 -0
  23. package/src/Cloudflare/R2/index.ts +2 -9
  24. package/src/Cloudflare/Workers/InferEnv.ts +23 -0
  25. package/src/Cloudflare/Workers/Worker.ts +185 -108
  26. package/src/Cloudflare/Workers/index.ts +1 -0
  27. package/src/Cloudflare/index.ts +2 -2
  28. package/src/Plan.ts +12 -0
  29. package/src/Platform.ts +39 -55
  30. package/src/Test/Vitest.ts +15 -2
  31. package/src/Util/effect.ts +24 -0
  32. package/src/Cloudflare/R2/BucketBinding.ts +0 -31
  33. package/src/Cloudflare/R2/CreateMultipartUpload.ts +0 -59
  34. package/src/Cloudflare/R2/DeleteObject.ts +0 -41
  35. package/src/Cloudflare/R2/GetObject.ts +0 -47
  36. package/src/Cloudflare/R2/HeadObject.ts +0 -41
  37. package/src/Cloudflare/R2/ListObjects.ts +0 -45
  38. package/src/Cloudflare/R2/MultipartUploadClient.ts +0 -40
  39. package/src/Cloudflare/R2/PutObject.ts +0 -55
  40. package/src/Cloudflare/R2/ResumeMultipartUpload.ts +0 -48
  41. package/src/Cloudflare/R2/UploadValue.ts +0 -10
@@ -24,7 +24,7 @@ import { findCwdForBundle } from "../../Bundle/TempRoot.ts";
24
24
  import type { ScopedPlanStatusSession } from "../../Cli/Cli.ts";
25
25
  import { isResolved } from "../../Diff.ts";
26
26
  import type { HttpEffect } from "../../Http.ts";
27
- import type { Input } from "../../Input.ts";
27
+ import type { Input, InputProps } from "../../Input.ts";
28
28
  import * as Output from "../../Output.ts";
29
29
  import { createPhysicalName } from "../../PhysicalName.ts";
30
30
  import {
@@ -39,7 +39,9 @@ import { Self } from "../../Self.ts";
39
39
  import * as Serverless from "../../Serverless/index.ts";
40
40
  import { Stack } from "../../Stack.ts";
41
41
  import { Account } from "../Account.ts";
42
+ import { D1Database } from "../D1/D1Database.ts";
42
43
  import { CloudflareLogs } from "../Logs.ts";
44
+ import type { R2Bucket } from "../R2/R2Bucket.ts";
43
45
  import type { AssetsConfig, AssetsProps } from "./Assets.ts";
44
46
  import * as Assets from "./Assets.ts";
45
47
  import cloudflare_workers from "./cloudflare_workers.ts";
@@ -142,7 +144,29 @@ export const ExportedHandlerMethods = [
142
144
  "queue",
143
145
  ] as const satisfies (keyof cf.ExportedHandler)[];
144
146
 
145
- export interface WorkerProps extends PlatformProps {
147
+ export interface WorkerExecutionContext extends Serverless.FunctionContext {
148
+ export(name: string, value: any): Effect.Effect<void>;
149
+ }
150
+
151
+ export type WorkerServices = Worker | WorkerEnvironment | Request;
152
+
153
+ export type WorkerShape = Main<WorkerServices>;
154
+
155
+ export type WorkerBindingResource = R2Bucket | D1Database;
156
+
157
+ export type WorkerBindings = {
158
+ [bindingName in string]: WorkerBindingResource;
159
+ };
160
+
161
+ export type WorkerBindingProps = {
162
+ [bindingName in string]:
163
+ | WorkerBindingResource
164
+ | Effect.Effect<WorkerBindingResource, any, any>;
165
+ };
166
+
167
+ export interface WorkerProps<
168
+ Bindings extends WorkerBindingProps = any,
169
+ > extends PlatformProps {
146
170
  /**
147
171
  * Worker name override. If omitted, Alchemy derives a deterministic physical
148
172
  * name from the stack, stage, and logical ID.
@@ -185,19 +209,12 @@ export interface WorkerProps extends PlatformProps {
185
209
  placement?: WorkerPlacement;
186
210
  env?: Record<string, any>;
187
211
  exports?: string[];
212
+ bindings?: Bindings;
188
213
  }
189
214
 
190
- export interface WorkerExecutionContext extends Serverless.FunctionContext {
191
- export(name: string, value: any): Effect.Effect<void>;
192
- }
193
-
194
- export type WorkerServices = Worker | WorkerEnvironment | Request;
195
-
196
- export type WorkerShape = Main<WorkerServices>;
197
-
198
- export interface Worker extends Resource<
215
+ export type Worker<Bindings extends WorkerBindings = any> = Resource<
199
216
  WorkerTypeId,
200
- WorkerProps,
217
+ WorkerProps<Bindings>,
201
218
  {
202
219
  workerId: string;
203
220
  workerName: string;
@@ -215,7 +232,7 @@ export interface Worker extends Resource<
215
232
  bindings: WorkerBinding[];
216
233
  containers?: { className: string }[];
217
234
  }
218
- > {}
235
+ >;
219
236
 
220
237
  /**
221
238
  * A Cloudflare Worker host with deploy-time binding support and runtime export
@@ -238,105 +255,165 @@ export const Worker: Platform<
238
255
  WorkerServices,
239
256
  WorkerShape,
240
257
  WorkerExecutionContext
241
- > = Platform(WorkerTypeId, (id: string): WorkerExecutionContext => {
242
- const listeners: Effect.Effect<Serverless.FunctionListener>[] = [];
243
- const exports: Record<string, any> = {};
244
- const env: Record<string, any> = {};
245
-
246
- const ctx = {
247
- Type: WorkerTypeId,
248
- id,
249
- env,
250
- get: (key: string) =>
251
- Effect.serviceOption(WorkerEnvironment).pipe(
252
- Effect.map(Option.getOrUndefined),
253
- Effect.flatMap((env) =>
254
- env
255
- ? Effect.succeed(env[key])
256
- : Effect.die("WorkerEnvironment not found"),
257
- ),
258
- Effect.flatMap((value) =>
259
- value
260
- ? Effect.succeed(value)
261
- : Effect.die(`Environment variable '${key}' not found`),
262
- ),
263
- Effect.map((json) => {
264
- try {
265
- const value = JSON.parse(json);
266
- if (!Redacted.isRedacted(value)) {
267
- return Redacted.make(value.value);
258
+ > & {
259
+ <const Bindings extends WorkerBindingProps>(
260
+ id: string,
261
+ props: InputProps<WorkerProps<Bindings>>,
262
+ ): Effect.Effect<
263
+ Worker<{
264
+ [B in keyof Bindings]: Bindings[B] extends Effect.Effect<
265
+ infer T extends WorkerBindingResource,
266
+ any,
267
+ any
268
+ >
269
+ ? T
270
+ : Extract<Bindings[B], WorkerBindingResource>;
271
+ }>
272
+ >;
273
+ } = Platform(WorkerTypeId, {
274
+ onCreate: Effect.fnUntraced(function* (
275
+ resource: Worker,
276
+ props: InputProps<WorkerProps<WorkerBindingProps>>,
277
+ ) {
278
+ if (props.bindings) {
279
+ for (const bindingName in props.bindings) {
280
+ // @ts-expect-error
281
+ const bindingEff = props.bindings?.[bindingName] as
282
+ | WorkerBindingResource
283
+ | Effect.Effect<WorkerBindingResource>;
284
+ const binding = Effect.isEffect(bindingEff)
285
+ ? yield* bindingEff
286
+ : bindingEff;
287
+
288
+ const bindingMeta: InputProps<WorkerBinding> | undefined =
289
+ binding.Type === "Cloudflare.D1Database"
290
+ ? {
291
+ type: "d1",
292
+ id: binding.databaseId,
293
+ name: bindingName,
294
+ }
295
+ : binding.Type === "Cloudflare.R2Bucket"
296
+ ? {
297
+ type: "r2_bucket",
298
+ name: bindingName,
299
+ bucketName: binding.bucketName,
300
+ jurisdiction: binding.jurisdiction.pipe(
301
+ Output.map((jurisdiction) =>
302
+ jurisdiction === "default" ? undefined : jurisdiction,
303
+ ),
304
+ ),
305
+ }
306
+ : // TODO(sam): handle others
307
+ undefined;
308
+
309
+ if (bindingMeta) {
310
+ yield* resource.bind`${bindingName}`({
311
+ bindings: [bindingMeta],
312
+ });
313
+ }
314
+ }
315
+ }
316
+ }),
317
+ createExecutionContext: (id: string): WorkerExecutionContext => {
318
+ const listeners: Effect.Effect<Serverless.FunctionListener>[] = [];
319
+ const exports: Record<string, any> = {};
320
+ const env: Record<string, any> = {};
321
+
322
+ const ctx = {
323
+ Type: WorkerTypeId,
324
+ id,
325
+ env,
326
+ get: (key: string) =>
327
+ Effect.serviceOption(WorkerEnvironment).pipe(
328
+ Effect.map(Option.getOrUndefined),
329
+ Effect.flatMap((env) =>
330
+ env
331
+ ? Effect.succeed(env[key])
332
+ : Effect.die("WorkerEnvironment not found"),
333
+ ),
334
+ Effect.flatMap((value) =>
335
+ value
336
+ ? Effect.succeed(value)
337
+ : Effect.die(`Environment variable '${key}' not found`),
338
+ ),
339
+ Effect.map((json) => {
340
+ try {
341
+ const value = JSON.parse(json);
342
+ if (!Redacted.isRedacted(value)) {
343
+ return Redacted.make(value.value);
344
+ }
345
+ return value;
346
+ } catch {
347
+ return json;
268
348
  }
269
- return value;
270
- } catch {
271
- return json;
272
- }
349
+ }),
350
+ ) as any,
351
+ set: (id: string, output: Output.Output) =>
352
+ Effect.sync(() => {
353
+ const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
354
+ env[key] = output.pipe(
355
+ Output.map((value) =>
356
+ Redacted.isRedacted(value)
357
+ ? JSON.stringify({
358
+ _tag: "Redacted",
359
+ value: Redacted.value(value),
360
+ })
361
+ : JSON.stringify(value),
362
+ ),
363
+ );
364
+ return key;
273
365
  }),
274
- ) as any,
275
- set: (id: string, output: Output.Output) =>
276
- Effect.sync(() => {
277
- const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
278
- env[key] = output.pipe(
279
- Output.map((value) =>
280
- Redacted.isRedacted(value)
281
- ? JSON.stringify({
282
- _tag: "Redacted",
283
- value: Redacted.value(value),
284
- })
285
- : JSON.stringify(value),
286
- ),
287
- );
288
- return key;
289
- }),
290
- serve: <Req = never>(handler: HttpEffect<Req>) =>
291
- ctx.listen(workersHttpHandler(handler)),
292
- listen: ((
293
- handler:
294
- | Serverless.FunctionListener
295
- | Effect.Effect<Serverless.FunctionListener>,
296
- ) =>
297
- Effect.sync(() =>
298
- Effect.isEffect(handler)
299
- ? listeners.push(handler)
300
- : listeners.push(Effect.succeed(handler)),
301
- )) as any as Serverless.FunctionContext["listen"],
302
- export: (name: string, value: any) =>
303
- Effect.sync(() => {
304
- exports[name] = value;
305
- }),
306
- exports: Effect.gen(function* () {
307
- const handlers = yield* Effect.all(listeners, {
308
- concurrency: "unbounded",
309
- });
310
- const handle =
311
- (type: WorkerEvent["type"]) =>
312
- (request: any, env: unknown, context: cf.ExecutionContext) => {
313
- const event: WorkerEvent = {
314
- kind: "Cloudflare.Workers.WorkerEvent",
315
- type,
316
- input: request,
317
- env,
318
- context,
319
- };
320
- for (const handler of handlers) {
321
- const eff = handler(event);
322
- if (Effect.isEffect(eff)) {
323
- return eff.pipe(
324
- Effect.provide(Layer.succeed(ExecutionContext, context)),
325
- Effect.runPromise,
326
- );
366
+ serve: <Req = never>(handler: HttpEffect<Req>) =>
367
+ ctx.listen(workersHttpHandler(handler)),
368
+ listen: ((
369
+ handler:
370
+ | Serverless.FunctionListener
371
+ | Effect.Effect<Serverless.FunctionListener>,
372
+ ) =>
373
+ Effect.sync(() =>
374
+ Effect.isEffect(handler)
375
+ ? listeners.push(handler)
376
+ : listeners.push(Effect.succeed(handler)),
377
+ )) as any as Serverless.FunctionContext["listen"],
378
+ export: (name: string, value: any) =>
379
+ Effect.sync(() => {
380
+ exports[name] = value;
381
+ }),
382
+ exports: Effect.gen(function* () {
383
+ const handlers = yield* Effect.all(listeners, {
384
+ concurrency: "unbounded",
385
+ });
386
+ const handle =
387
+ (type: WorkerEvent["type"]) =>
388
+ (request: any, env: unknown, context: cf.ExecutionContext) => {
389
+ const event: WorkerEvent = {
390
+ kind: "Cloudflare.Workers.WorkerEvent",
391
+ type,
392
+ input: request,
393
+ env,
394
+ context,
395
+ };
396
+ for (const handler of handlers) {
397
+ const eff = handler(event);
398
+ if (Effect.isEffect(eff)) {
399
+ return eff.pipe(
400
+ Effect.provide(Layer.succeed(ExecutionContext, context)),
401
+ Effect.runPromise,
402
+ );
403
+ }
327
404
  }
328
- }
329
- return Promise.reject(new Error("No event handler found"));
405
+ return Promise.reject(new Error("No event handler found"));
406
+ };
407
+ return {
408
+ ...exports,
409
+ default: Object.fromEntries(
410
+ ExportedHandlerMethods.map((method) => [method, handle(method)]),
411
+ ),
330
412
  };
331
- return {
332
- ...exports,
333
- default: Object.fromEntries(
334
- ExportedHandlerMethods.map((method) => [method, handle(method)]),
335
- ),
336
- };
337
- }),
338
- };
339
- return ctx;
413
+ }),
414
+ };
415
+ return ctx;
416
+ },
340
417
  });
341
418
 
342
419
  export const bindWorker = Effect.fnUntraced(function* <Shape, Req = never>(
@@ -4,6 +4,7 @@ export * from "./DurableObject.ts";
4
4
  export * from "./DynamicWorker.ts";
5
5
  export * from "./Fetch.ts";
6
6
  export * from "./HttpServer.ts";
7
+ export * from "./InferEnv.ts";
7
8
  export * from "./Request.ts";
8
9
  export * from "./Rpc.ts";
9
10
  export * from "./WebSocket.ts";
@@ -1,8 +1,8 @@
1
1
  export * from "./Container.ts";
2
2
  export * from "./D1/index.ts";
3
- export * as KV from "./KV/index.ts";
3
+ export * from "./KV/index.ts";
4
4
  export * from "./Providers.ts";
5
- export * as R2 from "./R2/index.ts";
5
+ export * from "./R2/index.ts";
6
6
  export * from "./StageConfig.ts";
7
7
  export * from "./Website/index.ts";
8
8
  export * from "./Workers/index.ts";
package/src/Plan.ts CHANGED
@@ -148,8 +148,13 @@ export type Plan<Output = any> = {
148
148
  output: Output;
149
149
  };
150
150
 
151
+ export interface MakePlanOptions {
152
+ force?: boolean;
153
+ }
154
+
151
155
  export const make = <A>(
152
156
  stack: StackSpec<A>,
157
+ options: MakePlanOptions = {},
153
158
  ): Effect.Effect<Plan<A>, never, State> =>
154
159
  // @ts-expect-error
155
160
  ensureArtifactStore(
@@ -486,6 +491,13 @@ export const make = <A>(
486
491
  : "noop",
487
492
  } as UpdateDiff | NoopDiff),
488
493
  ),
494
+ Effect.map((diff) =>
495
+ options.force && diff.action === "noop"
496
+ ? ({
497
+ action: "update",
498
+ } satisfies UpdateDiff)
499
+ : diff,
500
+ ),
489
501
  );
490
502
 
491
503
  if (oldState.status === "creating") {
package/src/Platform.ts CHANGED
@@ -23,7 +23,6 @@ import { Self } from "./Self.ts";
23
23
  import type { Stack, StackServices } from "./Stack.ts";
24
24
  import type { Stage } from "./Stage.ts";
25
25
  import { effectClass } from "./Util/effect.ts";
26
- import type { IsAny } from "./Util/types.ts";
27
26
 
28
27
  export interface PlatformProps {
29
28
  /**
@@ -85,7 +84,6 @@ export interface Platform<
85
84
  | Exclude<PropsReq | InitReq, Services | PlatformServices>
86
85
  >;
87
86
  new (_: never): MakeShape<Shape, BaseShape>;
88
- promise(): PlatformPromise<Self>;
89
87
  of(shape: Shape & MainShape): MakeShape<Shape, BaseShape>;
90
88
  };
91
89
  };
@@ -108,7 +106,6 @@ export interface Platform<
108
106
  | Exclude<InitReq, Services | PlatformServices>
109
107
  > & {
110
108
  new (_: never): MakeShape<Shape, BaseShape>;
111
- promise(): PlatformPromise<Self>;
112
109
  };
113
110
  <Shape, PropsReq = never>(
114
111
  id: string,
@@ -129,7 +126,6 @@ export interface Platform<
129
126
  | Exclude<PropsReq | InitReq, Services | PlatformServices>
130
127
  >;
131
128
  new (_: never): MakeShape<Shape, BaseShape>;
132
- promise(): PlatformPromise<Self>;
133
129
  } & (<InitReq extends Services | PlatformServices = never>(
134
130
  impl: Effect.Effect<Shape, never, InitReq>,
135
131
  ) => Effect.Effect<
@@ -140,18 +136,18 @@ export interface Platform<
140
136
  | Exclude<InitReq, Services | PlatformServices>
141
137
  >);
142
138
  };
143
- <PropsReq = never, InitReq extends Services | PlatformServices = never>(
144
- id: string,
145
- props:
146
- | InputProps<Resource["Props"]>
147
- | Effect.Effect<InputProps<Resource["Props"]>, never, PropsReq>,
148
- ): Effect.Effect<
149
- Resource,
150
- never,
151
- | Provider<Resource>
152
- | PropsReq
153
- | Exclude<InitReq, Services | PlatformServices>
154
- >;
139
+ // <PropsReq = never, InitReq extends Services | PlatformServices = never>(
140
+ // id: string,
141
+ // props:
142
+ // | InputProps<Resource["Props"]>
143
+ // | Effect.Effect<InputProps<Resource["Props"]>, never, PropsReq>,
144
+ // ): Effect.Effect<
145
+ // Resource,
146
+ // never,
147
+ // | Provider<Resource>
148
+ // | PropsReq
149
+ // | Exclude<InitReq, Services | PlatformServices>
150
+ // >;
155
151
  <
156
152
  Shape extends MainShape,
157
153
  PropsReq = never,
@@ -168,9 +164,7 @@ export interface Platform<
168
164
  | Provider<Resource>
169
165
  | PropsReq
170
166
  | Exclude<InitReq, Services | PlatformServices>
171
- > & {
172
- promise(): PlatformPromise<Shape>;
173
- };
167
+ >;
174
168
  }
175
169
 
176
170
  type MakeShape<Shape, BaseShape> = Shape extends never | undefined | void
@@ -188,7 +182,10 @@ export const Platform = <
188
182
  >,
189
183
  >(
190
184
  type: R["Type"],
191
- createExecutionContext: (id: string) => BaseExecutionContext,
185
+ hooks: {
186
+ createExecutionContext: (id: string) => BaseExecutionContext;
187
+ onCreate?: (resource: R, props: any) => Effect.Effect<void>;
188
+ },
192
189
  ): any => {
193
190
  type Props = any;
194
191
  type Impl = Effect.Effect<any>;
@@ -213,7 +210,7 @@ export const Platform = <
213
210
  } else if (!impl) {
214
211
  const cls = makeClass(id, props);
215
212
  const asEffect = () =>
216
- !isTag
213
+ (!isTag
217
214
  ? // this is a non-tagged resource yielded without providing an implementation
218
215
  // e.g.
219
216
  // yield* Cloudflare.Worker("id", { main: "./src/worker.ts" })
@@ -236,7 +233,15 @@ export const Platform = <
236
233
  onNone: () => resource(id, props),
237
234
  onSome: Effect.succeed,
238
235
  }),
239
- );
236
+ )
237
+ ).pipe(
238
+ Effect.flatMap(
239
+ (resource) =>
240
+ hooks
241
+ .onCreate?.(resource as R, props)
242
+ .pipe(Effect.map(() => resource)) ?? Effect.succeed(resource),
243
+ ),
244
+ );
240
245
  return Object.assign(
241
246
  function (impl: Impl) {
242
247
  return cls.asEffect().pipe(Effect.provide(cls.make(impl)));
@@ -249,6 +254,8 @@ export const Platform = <
249
254
  cls,
250
255
  {
251
256
  asEffect,
257
+ // @ts-expect-error
258
+ pipe: (...args: any[]) => asEffect().pipe(...args),
252
259
  [Symbol.iterator]: () => new SingleShotGen({ asEffect }),
253
260
  },
254
261
  );
@@ -289,7 +296,7 @@ export const Platform = <
289
296
  Effect.flatMap(
290
297
  Effect.all([
291
298
  Effect.isEffect(props) ? props : Effect.succeed(props ?? {}),
292
- Effect.sync(() => createExecutionContext(id)),
299
+ Effect.sync(() => hooks.createExecutionContext(id)),
293
300
  Effect.services<never>(),
294
301
  ]),
295
302
  Effect.fnUntraced(function* ([
@@ -298,7 +305,15 @@ export const Platform = <
298
305
  outerServices,
299
306
  ]) {
300
307
  const instance = Object.assign(
301
- yield* resource(id, props as any),
308
+ yield* resource(id, props as any).pipe(
309
+ Effect.flatMap(
310
+ (resource) =>
311
+ hooks
312
+ .onCreate?.(resource, props)
313
+ .pipe(Effect.map(() => resource)) ??
314
+ Effect.succeed(resource),
315
+ ),
316
+ ),
302
317
  executionContext,
303
318
  );
304
319
 
@@ -365,34 +380,3 @@ export const Platform = <
365
380
  }) as any;
366
381
  return instance;
367
382
  };
368
-
369
- /**
370
- * Bridge between the Effect and the Promise.
371
- *
372
- * Only map types if needed.
373
- *
374
- * TODO(sam): probably over engineering? Maybe just let the user run into the wall of Effect.runPromise and fix? Good friction is good!
375
- */
376
- export type PlatformPromise<Shape> = [
377
- HasRequirements<Extract<Shape[keyof Shape], Effect.Effect<any, any, any>>>,
378
- Effect.Services<Extract<Shape[keyof Shape], Effect.Effect<any, any, any>>>,
379
- ] extends [true, never]
380
- ? Promise<Shape>
381
- : Promise<{
382
- [key in keyof Shape]: Shape[key] extends (
383
- ...args: infer Args
384
- ) => Effect.Effect<infer A, infer Err, any>
385
- ? (...args: Args) => Effect.Effect<A, Err, never>
386
- : Shape[key] extends Effect.Effect<infer A, infer Err, infer Req>
387
- ? Req extends never
388
- ? Shape[key]
389
- : Effect.Effect<A, Err, never>
390
- : Shape[key];
391
- }>;
392
-
393
- type HasRequirements<E extends Effect.Effect<any, any, any>> =
394
- IsAny<Effect.Services<E>> extends true
395
- ? true
396
- : Effect.Services<E> extends never
397
- ? false
398
- : true;
@@ -148,7 +148,10 @@ const deriveStackName = (testPath: string, suffix: string) => {
148
148
  const runWithContext = <A, Err>(
149
149
  stackName: string,
150
150
  effect: Effect.Effect<A, Err, Provided>,
151
- options: { state?: Layer.Layer<State.State, never, Stack.Stack> } = {},
151
+ options: {
152
+ state?: Layer.Layer<State.State, never, Stack.Stack>;
153
+ providers?: boolean;
154
+ } = {},
152
155
  ): Effect.Effect<
153
156
  A,
154
157
  aws.Credentials.CredentialsError | Config.ConfigError | Err,
@@ -194,7 +197,9 @@ const runWithContext = <A, Err>(
194
197
  }).pipe(
195
198
  Effect.provide(
196
199
  Layer.provideMerge(
197
- Layer.mergeAll(awsProviders, cfProviders),
200
+ options.providers === false
201
+ ? Layer.empty
202
+ : Layer.mergeAll(awsProviders, cfProviders),
198
203
  Layer.provideMerge(alchemy, platform),
199
204
  ),
200
205
  ),
@@ -216,6 +221,7 @@ export function test(
216
221
  options: {
217
222
  timeout?: number;
218
223
  state?: Layer.Layer<State.State, never, Stack.Stack>;
224
+ providers?: boolean;
219
225
  },
220
226
  testCase: Effect.Effect<void, any, Provided>,
221
227
  ): void;
@@ -232,6 +238,7 @@ export function test(
232
238
  {
233
239
  timeout?: number;
234
240
  state?: Layer.Layer<State.State, never, Stack.Stack>;
241
+ providers?: boolean;
235
242
  },
236
243
  Effect.Effect<void, any, Provided>,
237
244
  ]
@@ -253,6 +260,7 @@ export namespace test {
253
260
  options: {
254
261
  timeout?: number;
255
262
  state?: Layer.Layer<State.State, never, Stack.Stack>;
263
+ providers?: boolean;
256
264
  },
257
265
  testCase: Effect.Effect<void, any, Provided>,
258
266
  ): void;
@@ -269,6 +277,7 @@ export namespace test {
269
277
  {
270
278
  timeout?: number;
271
279
  state?: Layer.Layer<State.State, never, Stack.Stack>;
280
+ providers?: boolean;
272
281
  },
273
282
  Effect.Effect<void, any, Provided>,
274
283
  ]
@@ -287,6 +296,7 @@ export namespace test {
287
296
  {
288
297
  timeout?: number;
289
298
  state?: Layer.Layer<State.State, never, Stack.Stack>;
299
+ providers?: boolean;
290
300
  },
291
301
  Effect.Effect<void, any, Provided>,
292
302
  ]
@@ -436,6 +446,7 @@ export function skip(
436
446
  options: {
437
447
  timeout?: number;
438
448
  state?: Layer.Layer<State.State, never, Stack.Stack>;
449
+ providers?: boolean;
439
450
  },
440
451
  testCase: Effect.Effect<void, any, Provided>,
441
452
  ): void;
@@ -452,6 +463,7 @@ export function skip(
452
463
  {
453
464
  timeout?: number;
454
465
  state?: Layer.Layer<State.State, never, Stack.Stack>;
466
+ providers?: boolean;
455
467
  },
456
468
  Effect.Effect<void, any, Provided>,
457
469
  ]
@@ -470,6 +482,7 @@ export function skipIf(condition: boolean) {
470
482
  {
471
483
  timeout?: number;
472
484
  state?: Layer.Layer<State.State, never, Stack.Stack>;
485
+ providers?: boolean;
473
486
  },
474
487
  Effect.Effect<void, any, Provided>,
475
488
  ]
@@ -55,3 +55,27 @@ export const taggedFunction = <
55
55
  },
56
56
  toString: () => `${tag.toString()}.${fn.name}`,
57
57
  });
58
+
59
+ export type UnwrapEffect<T> =
60
+ T extends Effect.Effect<infer A, any, any> ? A : T;
61
+
62
+ export type ToEffectInterface<T> = {
63
+ raw: T;
64
+ } & {
65
+ [K in keyof T]: T[K] extends (...args: any[]) => any
66
+ ? (...args: Parameters<T[K]>) => Effect.Effect<Awaited<ReturnType<T[K]>>>
67
+ : T[K];
68
+ };
69
+
70
+ export const toEffectInterface = <T extends object>(raw: T) =>
71
+ ({
72
+ raw,
73
+ ...Object.fromEntries(
74
+ Object.entries(raw).map(([key, value]) => [
75
+ key,
76
+ typeof value === "function"
77
+ ? (...args: any[]) => Effect.tryPromise(async () => value(...args))
78
+ : value,
79
+ ]),
80
+ ),
81
+ }) as ToEffectInterface<T>;