Skip to content

Commit

Permalink
add ManagedRuntime module, to make incremental adoption easier (#2211)
Browse files Browse the repository at this point in the history
  • Loading branch information
tim-smart authored Mar 11, 2024
1 parent 8b552a2 commit 20e63fb
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 14 deletions.
29 changes: 29 additions & 0 deletions .changeset/famous-mugs-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"effect": patch
---

add ManagedRuntime module, to make incremental adoption easier

You can use a ManagedRuntime to run Effect's that can use the
dependencies from the given Layer. For example:

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

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

async function main() {
const runtime = ManagedRuntime.make(Notifications.Live);
await runtime.runPromise(Notifications.notify("Hello, world!"));
await runtime.dispose();
}

main();
```
11 changes: 11 additions & 0 deletions .changeset/strong-flowers-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"effect": patch
---

add Layer.toRuntimeWithMemoMap api

Similar to Layer.toRuntime, but allows you to share a Layer.MemoMap between
layer builds.

By sharing the MemoMap, layers are shared between each build - ensuring layers
are only built once between multiple calls to Layer.toRuntimeWithMemoMap.
17 changes: 17 additions & 0 deletions packages/effect/src/Layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -784,6 +784,23 @@ export const toRuntime: <RIn, E, ROut>(
self: Layer<ROut, E, RIn>
) => Effect.Effect<Runtime.Runtime<ROut>, E, Scope.Scope | RIn> = internal.toRuntime

/**
* Converts a layer that requires no services into a scoped runtime, which can
* be used to execute effects.
*
* @since 2.0.0
* @category conversions
*/
export const toRuntimeWithMemoMap: {
(
memoMap: MemoMap
): <RIn, E, ROut>(self: Layer<ROut, E, RIn>) => Effect.Effect<Runtime.Runtime<ROut>, E, Scope.Scope | RIn>
<RIn, E, ROut>(
self: Layer<ROut, E, RIn>,
memoMap: MemoMap
): Effect.Effect<Runtime.Runtime<ROut>, E, Scope.Scope | RIn>
} = internal.toRuntimeWithMemoMap

/**
* Feeds the output services of this builder into the input of the specified
* builder, resulting in a new builder with the inputs of this builder as
Expand Down
115 changes: 115 additions & 0 deletions packages/effect/src/ManagedRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @since 2.0.0
*/
import type * as Effect from "./Effect.js"
import type * as Exit from "./Exit.js"
import type * as Fiber from "./Fiber.js"
import * as internal from "./internal/managedRuntime.js"
import type * as Layer from "./Layer.js"
import type { Pipeable } from "./Pipeable.js"
import type * as Runtime from "./Runtime.js"

/**
* @since 2.0.0
* @category models
*/
export interface ManagedRuntime<in R, out ER> extends Pipeable {
readonly memoMap: Layer.MemoMap
readonly runtimeEffect: Effect.Effect<Runtime.Runtime<R>, ER>
readonly runtime: () => Promise<Runtime.Runtime<R>>

/**
* Executes the effect using the provided Scheduler or using the global
* Scheduler if not provided
*/
readonly runFork: <A, E>(
self: Effect.Effect<A, E, R>,
options?: Runtime.RunForkOptions
) => Fiber.RuntimeFiber<A, E | ER>

/**
* Executes the effect synchronously returning the exit.
*
* This method is effectful and should only be invoked at the edges of your
* program.
*/
readonly runSyncExit: <A, E>(effect: Effect.Effect<A, E, R>) => Exit.Exit<A, ER | E>

/**
* Executes the effect synchronously throwing in case of errors or async boundaries.
*
* This method is effectful and should only be invoked at the edges of your
* program.
*/
readonly runSync: <A, E>(effect: Effect.Effect<A, E, R>) => A

/**
* Executes the effect asynchronously, eventually passing the exit value to
* the specified callback.
*
* This method is effectful and should only be invoked at the edges of your
* program.
*/
readonly runCallback: <A, E>(
effect: Effect.Effect<A, E, R>,
options?: Runtime.RunCallbackOptions<A, E | ER> | undefined
) => Runtime.Cancel<A, E | ER>

/**
* Runs the `Effect`, returning a JavaScript `Promise` that will be resolved
* with the value of the effect once the effect has been executed, or will be
* rejected with the first error or exception throw by the effect.
*
* This method is effectful and should only be used at the edges of your
* program.
*/
readonly runPromise: <A, E>(effect: Effect.Effect<A, E, R>) => Promise<A>

/**
* Runs the `Effect`, returning a JavaScript `Promise` that will be resolved
* with the `Exit` state of the effect once the effect has been executed.
*
* This method is effectful and should only be used at the edges of your
* program.
*/
readonly runPromiseExit: <A, E>(effect: Effect.Effect<A, E, R>) => Promise<Exit.Exit<A, ER | E>>

/**
* Dispose of the resources associated with the runtime.
*/
readonly dispose: () => Promise<void>

/**
* Dispose of the resources associated with the runtime.
*/
readonly disposeEffect: Effect.Effect<void, never, never>
}

/**
* Convert a Layer into an ManagedRuntime, that can be used to run Effect's using
* your services.
*
* @since 2.0.0
* @category runtime class
* @example
* import { Console, Effect, Layer, ManagedRuntime } from "effect"
*
* class Notifications extends Effect.Tag("Notifications")<
* Notifications,
* { readonly notify: (message: string) => Effect.Effect<void> }
* >() {
* static Live = Layer.succeed(this, { notify: (message) => Console.log(message) })
* }
*
* async function main() {
* const runtime = ManagedRuntime.make(Notifications.Live)
* await runtime.runPromise(Notifications.notify("Hello, world!"))
* await runtime.dispose()
* }
*
* main()
*/
export const make: <R, E>(
layer: Layer.Layer<R, E, never>,
memoMap?: Layer.MemoMap | undefined
) => ManagedRuntime<R, E> = internal.make
5 changes: 5 additions & 0 deletions packages/effect/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,11 @@ export * as LogSpan from "./LogSpan.js"
*/
export * as Logger from "./Logger.js"

/**
* @since 2.0.0
*/
export * as ManagedRuntime from "./ManagedRuntime.js"

/**
* @since 1.0.0
*/
Expand Down
29 changes: 25 additions & 4 deletions packages/effect/src/internal/layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ export const makeMemoMap: Effect.Effect<Layer.MemoMap> = core.suspend(() =>
)
)

/** @internal */
export const unsafeMakeMemoMap = (): Layer.MemoMap => new MemoMapImpl(circular.unsafeMakeSynchronized(new Map()))

/** @internal */
export const build = <RIn, E, ROut>(
self: Layer.Layer<ROut, E, RIn>
Expand Down Expand Up @@ -988,17 +991,35 @@ export const tapErrorCause = dual<
/** @internal */
export const toRuntime = <RIn, E, ROut>(
self: Layer.Layer<ROut, E, RIn>
): Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope> => {
return pipe(
fiberRuntime.scopeWith((scope) => pipe(self, buildWithScope(scope))),
): Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope> =>
pipe(
fiberRuntime.scopeWith((scope) => buildWithScope(self, scope)),
core.flatMap((context) =>
pipe(
runtime.runtime<ROut>(),
core.provideContext(context)
)
)
)
}

/** @internal */
export const toRuntimeWithMemoMap = dual<
(
memoMap: Layer.MemoMap
) => <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>) => Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope>,
<RIn, E, ROut>(
self: Layer.Layer<ROut, E, RIn>,
memoMap: Layer.MemoMap
) => Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope>
>(2, (self, memoMap) =>
core.flatMap(
fiberRuntime.scopeWith((scope) => buildWithMemoMap(self, memoMap, scope)),
(context) =>
pipe(
runtime.runtime<any>(),
core.provideContext(context)
)
))

/** @internal */
export const provide = dual<
Expand Down
111 changes: 111 additions & 0 deletions packages/effect/src/internal/managedRuntime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type * as Effect from "../Effect.js"
import type { Exit } from "../Exit.js"
import type * as Fiber from "../Fiber.js"
import type * as Layer from "../Layer.js"
import type { ManagedRuntime } from "../ManagedRuntime.js"
import { pipeArguments } from "../Pipeable.js"
import type * as Runtime from "../Runtime.js"
import * as Scope from "../Scope.js"
import * as effect from "./core-effect.js"
import * as core from "./core.js"
import * as fiberRuntime from "./fiberRuntime.js"
import * as internalLayer from "./layer.js"
import * as internalRuntime from "./runtime.js"

interface ManagedRuntimeImpl<R, E> extends ManagedRuntime<R, E> {
readonly scope: Scope.CloseableScope
cachedRuntime: Runtime.Runtime<R> | undefined
}

function provide<R, ER, A, E>(
managed: ManagedRuntimeImpl<R, ER>,
effect: Effect.Effect<A, E, R>
): Effect.Effect<A, E | ER> {
return core.flatMap(
managed.runtimeEffect,
(rt) =>
core.withFiberRuntime((fiber) => {
fiber.setFiberRefs(rt.fiberRefs)
fiber._runtimeFlags = rt.runtimeFlags
return core.provideContext(effect, rt.context)
})
)
}

/** @internal */
export const make = <R, ER>(
layer: Layer.Layer<R, ER, never>,
memoMap?: Layer.MemoMap
): ManagedRuntime<R, ER> => {
memoMap = memoMap ?? internalLayer.unsafeMakeMemoMap()
const scope = internalRuntime.unsafeRunSyncEffect(fiberRuntime.scopeMake())
const self: ManagedRuntimeImpl<R, ER> = {
memoMap,
scope,
runtimeEffect: internalRuntime
.unsafeRunSyncEffect(
effect.memoize(
core.tap(
Scope.extend(
internalLayer.toRuntimeWithMemoMap(layer, memoMap),
scope
),
(rt) => {
self.cachedRuntime = rt
}
)
)
),
cachedRuntime: undefined,
pipe() {
return pipeArguments(this, arguments)
},
runtime() {
return self.cachedRuntime === undefined ?
internalRuntime.unsafeRunPromiseEffect(self.runtimeEffect) :
Promise.resolve(self.cachedRuntime)
},
dispose(): Promise<void> {
return internalRuntime.unsafeRunPromiseEffect(self.disposeEffect)
},
disposeEffect: core.suspend(() => {
;(self as any).runtime = core.die("ManagedRuntime disposed")
self.cachedRuntime = undefined
return Scope.close(self.scope, core.exitUnit)
}),
runFork<A, E>(effect: Effect.Effect<A, E, R>, options?: Runtime.RunForkOptions): Fiber.RuntimeFiber<A, E | ER> {
return self.cachedRuntime === undefined ?
internalRuntime.unsafeForkEffect(provide(self, effect), options) :
internalRuntime.unsafeFork(self.cachedRuntime)(effect, options)
},
runSyncExit<A, E>(effect: Effect.Effect<A, E, R>): Exit<A, E | ER> {
return self.cachedRuntime === undefined ?
internalRuntime.unsafeRunSyncExitEffect(provide(self, effect)) :
internalRuntime.unsafeRunSyncExit(self.cachedRuntime)(effect)
},
runSync<A, E>(effect: Effect.Effect<A, E, R>): A {
return self.cachedRuntime === undefined ?
internalRuntime.unsafeRunSyncEffect(provide(self, effect)) :
internalRuntime.unsafeRunSync(self.cachedRuntime)(effect)
},
runPromiseExit<A, E>(effect: Effect.Effect<A, E, R>): Promise<Exit<A, E | ER>> {
return self.cachedRuntime === undefined ?
internalRuntime.unsafeRunPromiseExitEffect(provide(self, effect)) :
internalRuntime.unsafeRunPromiseExit(self.cachedRuntime)(effect)
},
runCallback<A, E>(
effect: Effect.Effect<A, E, R>,
options?: Runtime.RunCallbackOptions<A, E | ER> | undefined
): Runtime.Cancel<A, E | ER> {
return self.cachedRuntime === undefined ?
internalRuntime.unsafeRunCallback(internalRuntime.defaultRuntime)(provide(self, effect), options) :
internalRuntime.unsafeRunCallback(self.cachedRuntime)(effect, options)
},
runPromise<A, E>(effect: Effect.Effect<A, E, R>): Promise<A> {
return self.cachedRuntime === undefined ?
internalRuntime.unsafeRunPromiseEffect(provide(self, effect)) :
internalRuntime.unsafeRunPromise(self.cachedRuntime)(effect)
}
}
return self
}
Loading

0 comments on commit 20e63fb

Please sign in to comment.