Skip to content

Commit

Permalink
add Layer.RuntimeClass, to make incremental adoption easier
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart committed Feb 25, 2024
1 parent 12345bf commit 696fd01
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 4 deletions.
36 changes: 36 additions & 0 deletions .changeset/famous-mugs-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
"effect": patch
---

add Layer.RuntimeClass, to make incremental adoption easier

You can use a RuntimeClass to build an Effect runner that can use the
dependencies from the given Layer. For example:

```ts
import type { Effect } from "effect";
import { Console, Context, Layer } from "effect";

class Notifications extends Context.Tag("Notifications")<
Notifications,
{ readonly notify: (message: string) => Effect.Effect<void> }
>() {
static Live = Layer.succeed(this, {
notify: (message) => Console.log(message),
});
}

class NotificationsClass extends Layer.RuntimeClass(() => Notifications.Live) {
notify: (message: string) => Promise<void> = this.$serviceFn(
Notifications,
(_) => _.notify,
);
}

async function main() {
await using notifications = new NotificationsClass();
await notifications.notify("Hello, world!");
}

main();
```
137 changes: 133 additions & 4 deletions packages/effect/src/Layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import type * as Cause from "./Cause.js"
import type * as Clock from "./Clock.js"
import type { ConfigProvider } from "./ConfigProvider.js"
import * as Context from "./Context.js"
import type * as Effect from "./Effect.js"
import type * as Exit from "./Exit.js"
import * as Effect from "./Effect.js"
import * as Exit from "./Exit.js"
import type { FiberRef } from "./FiberRef.js"
import type { LazyArg } from "./Function.js"
import { clockTag } from "./internal/clock.js"
Expand All @@ -36,10 +36,10 @@ import type { LogLevel } from "./LogLevel.js"
import type * as Option from "./Option.js"
import type { Pipeable } from "./Pipeable.js"
import type * as Request from "./Request.js"
import type * as Runtime from "./Runtime.js"
import * as Runtime from "./Runtime.js"
import type * as Schedule from "./Schedule.js"
import * as Scheduler from "./Scheduler.js"
import type * as Scope from "./Scope.js"
import * as Scope from "./Scope.js"
import type * as Tracer from "./Tracer.js"
import type * as Types from "./Types.js"

Expand Down Expand Up @@ -1055,3 +1055,132 @@ export const buildWithMemoMap: {
scope: Scope.Scope
): Effect.Effect<Context.Context<ROut>, E, RIn>
} = internal.buildWithMemoMap

// -----------------------------------------------------------------------------
// runtime class
// -----------------------------------------------------------------------------

/**
* @since 2.0.0
* @category runtime class
*/
export interface RuntimeClassConstructor<Args extends ReadonlyArray<any>, R> {
new(...args: Args): RuntimeClass<R>
}

/**
* @since 2.0.0
* @category runtime class
*/
export interface RuntimeClass<R> extends AsyncDisposable {
readonly dispose: () => Promise<void>
readonly $effect: <E, A>(effect: Effect.Effect<A, E, R>) => Promise<A>
readonly $effectExit: <E, A>(effect: Effect.Effect<A, E, R>) => Promise<Exit.Exit<A, E>>
readonly $effectFn: <
F extends (...args: Array<any>) => Effect.Effect<any, any, R>
>(
fn: F
) => (...args: Parameters<F>) => Promise<Effect.Effect.Success<ReturnType<F>>>
readonly $service: <I extends R, S, A, E>(
tag: Context.Tag<I, S>,
fn: (service: S) => Effect.Effect<A, E, R>
) => () => Promise<A>
readonly $serviceFn: <
I extends R,
S,
F extends (...args: Array<any>) => Effect.Effect<any, any, R>
>(
tag: Context.Tag<I, S>,
fn: (service: S) => F
) => (...args: Parameters<F>) => Promise<Effect.Effect.Success<ReturnType<F>>>
}

/**
* Convert a Layer into a class, than can be used to integrate Effect's with Promise
* based code.
*
* @since 2.0.0
* @category runtime class
* @example
* import type { Effect } from "effect"
* import { Console, Context, Layer } from "effect"
*
* class Notifications extends Context.Tag("Notifications")<
* Notifications,
* {
* readonly notify: (message: string) => Effect.Effect<void>
* }
* >() {
* static Live = Layer.succeed(this, {
* notify: (message) => Console.log(message)
* })
* }
*
* class NotificationsClass extends Layer.RuntimeClass(() => Notifications.Live) {
* notify: (message: string) => Promise<void> = this.$serviceFn(Notifications, (_) => _.notify)
* }
*
* async function main() {
* await using notifications = new NotificationsClass()
* await notifications.notify("Hello, world!")
* }
*
* main()
*/
export const RuntimeClass = <Args extends ReadonlyArray<any>, R, E>(
evaluate: (...args: Args) => Layer<R, E, never>
): RuntimeClassConstructor<Args, R> =>
class {
#scope = Effect.runSync(Scope.make())
#runtime: Promise<Runtime.Runtime<R>>

constructor(...args: Args) {
this.#runtime = Effect.runPromise(
toRuntime(evaluate(...args)).pipe(Scope.extend(this.#scope))
)
}

dispose(): Promise<void> {
return Effect.runPromise(Scope.close(this.#scope, Exit.unit))
}

[Symbol.asyncDispose]() {
return this.dispose()
}

$effect<E, A>(effect: Effect.Effect<A, E, R>): Promise<A> {
return this.#runtime.then((runtime) => Runtime.runPromise(runtime)(effect))
}

$effectExit<E, A>(effect: Effect.Effect<A, E, R>): Promise<Exit.Exit<A, E>> {
return this.#runtime.then((runtime) => Runtime.runPromiseExit(runtime)(effect))
}

$effectFn<F extends (...args: Array<any>) => Effect.Effect<any, any, R>>(
fn: F
): (
...args: Parameters<F>
) => Promise<Effect.Effect.Success<ReturnType<F>>> {
return (...args) => this.$effect(fn(...args))
}

$service<I extends R, S, A, E>(
tag: Context.Tag<I, S>,
fn: (service: S) => Effect.Effect<A, E, R>
): () => Promise<A> {
return () => this.$effect(Effect.flatMap(tag, (_) => fn(_)))
}

$serviceFn<
I extends R,
S,
F extends (...args: Array<any>) => Effect.Effect<any, any, R>
>(
tag: Context.Tag<I, S>,
fn: (service: S) => F
): (
...args: Parameters<F>
) => Promise<Effect.Effect.Success<ReturnType<F>>> {
return (...args) => this.$effect(Effect.flatMap(tag, (_) => fn(_).apply(_, args)))
}
}

0 comments on commit 696fd01

Please sign in to comment.