Skip to content

Commit 20e63fb

Browse files
authoredMar 11, 2024··
add ManagedRuntime module, to make incremental adoption easier (#2211)
1 parent 8b552a2 commit 20e63fb

10 files changed

+379
-14
lines changed
 

‎.changeset/famous-mugs-attack.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
add ManagedRuntime module, to make incremental adoption easier
6+
7+
You can use a ManagedRuntime to run Effect's that can use the
8+
dependencies from the given Layer. For example:
9+
10+
```ts
11+
import { Console, Effect, Layer, ManagedRuntime } from "effect";
12+
13+
class Notifications extends Effect.Tag("Notifications")<
14+
Notifications,
15+
{ readonly notify: (message: string) => Effect.Effect<void> }
16+
>() {
17+
static Live = Layer.succeed(this, {
18+
notify: (message) => Console.log(message),
19+
});
20+
}
21+
22+
async function main() {
23+
const runtime = ManagedRuntime.make(Notifications.Live);
24+
await runtime.runPromise(Notifications.notify("Hello, world!"));
25+
await runtime.dispose();
26+
}
27+
28+
main();
29+
```

‎.changeset/strong-flowers-laugh.md

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
add Layer.toRuntimeWithMemoMap api
6+
7+
Similar to Layer.toRuntime, but allows you to share a Layer.MemoMap between
8+
layer builds.
9+
10+
By sharing the MemoMap, layers are shared between each build - ensuring layers
11+
are only built once between multiple calls to Layer.toRuntimeWithMemoMap.

‎packages/effect/src/Layer.ts

+17
Original file line numberDiff line numberDiff line change
@@ -784,6 +784,23 @@ export const toRuntime: <RIn, E, ROut>(
784784
self: Layer<ROut, E, RIn>
785785
) => Effect.Effect<Runtime.Runtime<ROut>, E, Scope.Scope | RIn> = internal.toRuntime
786786

787+
/**
788+
* Converts a layer that requires no services into a scoped runtime, which can
789+
* be used to execute effects.
790+
*
791+
* @since 2.0.0
792+
* @category conversions
793+
*/
794+
export const toRuntimeWithMemoMap: {
795+
(
796+
memoMap: MemoMap
797+
): <RIn, E, ROut>(self: Layer<ROut, E, RIn>) => Effect.Effect<Runtime.Runtime<ROut>, E, Scope.Scope | RIn>
798+
<RIn, E, ROut>(
799+
self: Layer<ROut, E, RIn>,
800+
memoMap: MemoMap
801+
): Effect.Effect<Runtime.Runtime<ROut>, E, Scope.Scope | RIn>
802+
} = internal.toRuntimeWithMemoMap
803+
787804
/**
788805
* Feeds the output services of this builder into the input of the specified
789806
* builder, resulting in a new builder with the inputs of this builder as

‎packages/effect/src/ManagedRuntime.ts

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @since 2.0.0
3+
*/
4+
import type * as Effect from "./Effect.js"
5+
import type * as Exit from "./Exit.js"
6+
import type * as Fiber from "./Fiber.js"
7+
import * as internal from "./internal/managedRuntime.js"
8+
import type * as Layer from "./Layer.js"
9+
import type { Pipeable } from "./Pipeable.js"
10+
import type * as Runtime from "./Runtime.js"
11+
12+
/**
13+
* @since 2.0.0
14+
* @category models
15+
*/
16+
export interface ManagedRuntime<in R, out ER> extends Pipeable {
17+
readonly memoMap: Layer.MemoMap
18+
readonly runtimeEffect: Effect.Effect<Runtime.Runtime<R>, ER>
19+
readonly runtime: () => Promise<Runtime.Runtime<R>>
20+
21+
/**
22+
* Executes the effect using the provided Scheduler or using the global
23+
* Scheduler if not provided
24+
*/
25+
readonly runFork: <A, E>(
26+
self: Effect.Effect<A, E, R>,
27+
options?: Runtime.RunForkOptions
28+
) => Fiber.RuntimeFiber<A, E | ER>
29+
30+
/**
31+
* Executes the effect synchronously returning the exit.
32+
*
33+
* This method is effectful and should only be invoked at the edges of your
34+
* program.
35+
*/
36+
readonly runSyncExit: <A, E>(effect: Effect.Effect<A, E, R>) => Exit.Exit<A, ER | E>
37+
38+
/**
39+
* Executes the effect synchronously throwing in case of errors or async boundaries.
40+
*
41+
* This method is effectful and should only be invoked at the edges of your
42+
* program.
43+
*/
44+
readonly runSync: <A, E>(effect: Effect.Effect<A, E, R>) => A
45+
46+
/**
47+
* Executes the effect asynchronously, eventually passing the exit value to
48+
* the specified callback.
49+
*
50+
* This method is effectful and should only be invoked at the edges of your
51+
* program.
52+
*/
53+
readonly runCallback: <A, E>(
54+
effect: Effect.Effect<A, E, R>,
55+
options?: Runtime.RunCallbackOptions<A, E | ER> | undefined
56+
) => Runtime.Cancel<A, E | ER>
57+
58+
/**
59+
* Runs the `Effect`, returning a JavaScript `Promise` that will be resolved
60+
* with the value of the effect once the effect has been executed, or will be
61+
* rejected with the first error or exception throw by the effect.
62+
*
63+
* This method is effectful and should only be used at the edges of your
64+
* program.
65+
*/
66+
readonly runPromise: <A, E>(effect: Effect.Effect<A, E, R>) => Promise<A>
67+
68+
/**
69+
* Runs the `Effect`, returning a JavaScript `Promise` that will be resolved
70+
* with the `Exit` state of the effect once the effect has been executed.
71+
*
72+
* This method is effectful and should only be used at the edges of your
73+
* program.
74+
*/
75+
readonly runPromiseExit: <A, E>(effect: Effect.Effect<A, E, R>) => Promise<Exit.Exit<A, ER | E>>
76+
77+
/**
78+
* Dispose of the resources associated with the runtime.
79+
*/
80+
readonly dispose: () => Promise<void>
81+
82+
/**
83+
* Dispose of the resources associated with the runtime.
84+
*/
85+
readonly disposeEffect: Effect.Effect<void, never, never>
86+
}
87+
88+
/**
89+
* Convert a Layer into an ManagedRuntime, that can be used to run Effect's using
90+
* your services.
91+
*
92+
* @since 2.0.0
93+
* @category runtime class
94+
* @example
95+
* import { Console, Effect, Layer, ManagedRuntime } from "effect"
96+
*
97+
* class Notifications extends Effect.Tag("Notifications")<
98+
* Notifications,
99+
* { readonly notify: (message: string) => Effect.Effect<void> }
100+
* >() {
101+
* static Live = Layer.succeed(this, { notify: (message) => Console.log(message) })
102+
* }
103+
*
104+
* async function main() {
105+
* const runtime = ManagedRuntime.make(Notifications.Live)
106+
* await runtime.runPromise(Notifications.notify("Hello, world!"))
107+
* await runtime.dispose()
108+
* }
109+
*
110+
* main()
111+
*/
112+
export const make: <R, E>(
113+
layer: Layer.Layer<R, E, never>,
114+
memoMap?: Layer.MemoMap | undefined
115+
) => ManagedRuntime<R, E> = internal.make

‎packages/effect/src/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,11 @@ export * as LogSpan from "./LogSpan.js"
385385
*/
386386
export * as Logger from "./Logger.js"
387387

388+
/**
389+
* @since 2.0.0
390+
*/
391+
export * as ManagedRuntime from "./ManagedRuntime.js"
392+
388393
/**
389394
* @since 1.0.0
390395
*/

‎packages/effect/src/internal/layer.ts

+25-4
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,9 @@ export const makeMemoMap: Effect.Effect<Layer.MemoMap> = core.suspend(() =>
312312
)
313313
)
314314

315+
/** @internal */
316+
export const unsafeMakeMemoMap = (): Layer.MemoMap => new MemoMapImpl(circular.unsafeMakeSynchronized(new Map()))
317+
315318
/** @internal */
316319
export const build = <RIn, E, ROut>(
317320
self: Layer.Layer<ROut, E, RIn>
@@ -988,17 +991,35 @@ export const tapErrorCause = dual<
988991
/** @internal */
989992
export const toRuntime = <RIn, E, ROut>(
990993
self: Layer.Layer<ROut, E, RIn>
991-
): Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope> => {
992-
return pipe(
993-
fiberRuntime.scopeWith((scope) => pipe(self, buildWithScope(scope))),
994+
): Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope> =>
995+
pipe(
996+
fiberRuntime.scopeWith((scope) => buildWithScope(self, scope)),
994997
core.flatMap((context) =>
995998
pipe(
996999
runtime.runtime<ROut>(),
9971000
core.provideContext(context)
9981001
)
9991002
)
10001003
)
1001-
}
1004+
1005+
/** @internal */
1006+
export const toRuntimeWithMemoMap = dual<
1007+
(
1008+
memoMap: Layer.MemoMap
1009+
) => <RIn, E, ROut>(self: Layer.Layer<ROut, E, RIn>) => Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope>,
1010+
<RIn, E, ROut>(
1011+
self: Layer.Layer<ROut, E, RIn>,
1012+
memoMap: Layer.MemoMap
1013+
) => Effect.Effect<Runtime.Runtime<ROut>, E, RIn | Scope.Scope>
1014+
>(2, (self, memoMap) =>
1015+
core.flatMap(
1016+
fiberRuntime.scopeWith((scope) => buildWithMemoMap(self, memoMap, scope)),
1017+
(context) =>
1018+
pipe(
1019+
runtime.runtime<any>(),
1020+
core.provideContext(context)
1021+
)
1022+
))
10021023

10031024
/** @internal */
10041025
export const provide = dual<
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type * as Effect from "../Effect.js"
2+
import type { Exit } from "../Exit.js"
3+
import type * as Fiber from "../Fiber.js"
4+
import type * as Layer from "../Layer.js"
5+
import type { ManagedRuntime } from "../ManagedRuntime.js"
6+
import { pipeArguments } from "../Pipeable.js"
7+
import type * as Runtime from "../Runtime.js"
8+
import * as Scope from "../Scope.js"
9+
import * as effect from "./core-effect.js"
10+
import * as core from "./core.js"
11+
import * as fiberRuntime from "./fiberRuntime.js"
12+
import * as internalLayer from "./layer.js"
13+
import * as internalRuntime from "./runtime.js"
14+
15+
interface ManagedRuntimeImpl<R, E> extends ManagedRuntime<R, E> {
16+
readonly scope: Scope.CloseableScope
17+
cachedRuntime: Runtime.Runtime<R> | undefined
18+
}
19+
20+
function provide<R, ER, A, E>(
21+
managed: ManagedRuntimeImpl<R, ER>,
22+
effect: Effect.Effect<A, E, R>
23+
): Effect.Effect<A, E | ER> {
24+
return core.flatMap(
25+
managed.runtimeEffect,
26+
(rt) =>
27+
core.withFiberRuntime((fiber) => {
28+
fiber.setFiberRefs(rt.fiberRefs)
29+
fiber._runtimeFlags = rt.runtimeFlags
30+
return core.provideContext(effect, rt.context)
31+
})
32+
)
33+
}
34+
35+
/** @internal */
36+
export const make = <R, ER>(
37+
layer: Layer.Layer<R, ER, never>,
38+
memoMap?: Layer.MemoMap
39+
): ManagedRuntime<R, ER> => {
40+
memoMap = memoMap ?? internalLayer.unsafeMakeMemoMap()
41+
const scope = internalRuntime.unsafeRunSyncEffect(fiberRuntime.scopeMake())
42+
const self: ManagedRuntimeImpl<R, ER> = {
43+
memoMap,
44+
scope,
45+
runtimeEffect: internalRuntime
46+
.unsafeRunSyncEffect(
47+
effect.memoize(
48+
core.tap(
49+
Scope.extend(
50+
internalLayer.toRuntimeWithMemoMap(layer, memoMap),
51+
scope
52+
),
53+
(rt) => {
54+
self.cachedRuntime = rt
55+
}
56+
)
57+
)
58+
),
59+
cachedRuntime: undefined,
60+
pipe() {
61+
return pipeArguments(this, arguments)
62+
},
63+
runtime() {
64+
return self.cachedRuntime === undefined ?
65+
internalRuntime.unsafeRunPromiseEffect(self.runtimeEffect) :
66+
Promise.resolve(self.cachedRuntime)
67+
},
68+
dispose(): Promise<void> {
69+
return internalRuntime.unsafeRunPromiseEffect(self.disposeEffect)
70+
},
71+
disposeEffect: core.suspend(() => {
72+
;(self as any).runtime = core.die("ManagedRuntime disposed")
73+
self.cachedRuntime = undefined
74+
return Scope.close(self.scope, core.exitUnit)
75+
}),
76+
runFork<A, E>(effect: Effect.Effect<A, E, R>, options?: Runtime.RunForkOptions): Fiber.RuntimeFiber<A, E | ER> {
77+
return self.cachedRuntime === undefined ?
78+
internalRuntime.unsafeForkEffect(provide(self, effect), options) :
79+
internalRuntime.unsafeFork(self.cachedRuntime)(effect, options)
80+
},
81+
runSyncExit<A, E>(effect: Effect.Effect<A, E, R>): Exit<A, E | ER> {
82+
return self.cachedRuntime === undefined ?
83+
internalRuntime.unsafeRunSyncExitEffect(provide(self, effect)) :
84+
internalRuntime.unsafeRunSyncExit(self.cachedRuntime)(effect)
85+
},
86+
runSync<A, E>(effect: Effect.Effect<A, E, R>): A {
87+
return self.cachedRuntime === undefined ?
88+
internalRuntime.unsafeRunSyncEffect(provide(self, effect)) :
89+
internalRuntime.unsafeRunSync(self.cachedRuntime)(effect)
90+
},
91+
runPromiseExit<A, E>(effect: Effect.Effect<A, E, R>): Promise<Exit<A, E | ER>> {
92+
return self.cachedRuntime === undefined ?
93+
internalRuntime.unsafeRunPromiseExitEffect(provide(self, effect)) :
94+
internalRuntime.unsafeRunPromiseExit(self.cachedRuntime)(effect)
95+
},
96+
runCallback<A, E>(
97+
effect: Effect.Effect<A, E, R>,
98+
options?: Runtime.RunCallbackOptions<A, E | ER> | undefined
99+
): Runtime.Cancel<A, E | ER> {
100+
return self.cachedRuntime === undefined ?
101+
internalRuntime.unsafeRunCallback(internalRuntime.defaultRuntime)(provide(self, effect), options) :
102+
internalRuntime.unsafeRunCallback(self.cachedRuntime)(effect, options)
103+
},
104+
runPromise<A, E>(effect: Effect.Effect<A, E, R>): Promise<A> {
105+
return self.cachedRuntime === undefined ?
106+
internalRuntime.unsafeRunPromiseEffect(provide(self, effect)) :
107+
internalRuntime.unsafeRunPromise(self.cachedRuntime)(effect)
108+
}
109+
}
110+
return self
111+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ManagedRuntime } from "effect"
2+
import * as Context from "effect/Context"
3+
import * as Effect from "effect/Effect"
4+
import * as FiberRef from "effect/FiberRef"
5+
import * as Layer from "effect/Layer"
6+
import { assert, describe, test } from "vitest"
7+
8+
describe.concurrent("ManagedRuntime", () => {
9+
test("memoizes the layer build", async () => {
10+
let count = 0
11+
const layer = Layer.effectDiscard(Effect.sync(() => {
12+
count++
13+
}))
14+
const runtime = ManagedRuntime.make(layer)
15+
await runtime.runPromise(Effect.unit)
16+
await runtime.runPromise(Effect.unit)
17+
await runtime.dispose()
18+
assert.strictEqual(count, 1)
19+
})
20+
21+
test("provides context", async () => {
22+
const tag = Context.GenericTag<string>("string")
23+
const layer = Layer.succeed(tag, "test")
24+
const runtime = ManagedRuntime.make(layer)
25+
const result = await runtime.runPromise(tag)
26+
await runtime.dispose()
27+
assert.strictEqual(result, "test")
28+
})
29+
30+
test("provides fiberRefs", async () => {
31+
const layer = Layer.setRequestCaching(true)
32+
const runtime = ManagedRuntime.make(layer)
33+
const result = await runtime.runPromise(FiberRef.get(FiberRef.currentRequestCacheEnabled))
34+
await runtime.dispose()
35+
assert.strictEqual(result, true)
36+
})
37+
38+
test("allows sharing a MemoMap", async () => {
39+
let count = 0
40+
const layer = Layer.effectDiscard(Effect.sync(() => {
41+
count++
42+
}))
43+
const runtimeA = ManagedRuntime.make(layer)
44+
const runtimeB = ManagedRuntime.make(layer, runtimeA.memoMap)
45+
await runtimeA.runPromise(Effect.unit)
46+
await runtimeB.runPromise(Effect.unit)
47+
await runtimeA.dispose()
48+
await runtimeB.dispose()
49+
assert.strictEqual(count, 1)
50+
})
51+
})

‎packages/effect/test/utils/extend.ts

+12-10
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ const TestEnv = TestEnvironment.TestContext.pipe(
2121
export const effect = (() => {
2222
const f = <E, A>(
2323
name: string,
24-
self: () => Effect.Effect<A, E, TestServices.TestServices>,
24+
self: Effect.Effect<A, E, TestServices.TestServices> | (() => Effect.Effect<A, E, TestServices.TestServices>),
2525
timeout: number | V.TestOptions = 5_000
2626
) => {
2727
return it(
2828
name,
2929
() =>
3030
pipe(
31-
Effect.suspend(self),
31+
Effect.isEffect(self) ? self : Effect.suspend(self),
3232
Effect.provide(TestEnv),
3333
Effect.runPromise
3434
),
@@ -38,14 +38,14 @@ export const effect = (() => {
3838
return Object.assign(f, {
3939
skip: <E, A>(
4040
name: string,
41-
self: () => Effect.Effect<A, E, TestServices.TestServices>,
41+
self: Effect.Effect<A, E, TestServices.TestServices> | (() => Effect.Effect<A, E, TestServices.TestServices>),
4242
timeout = 5_000
4343
) => {
4444
return it.skip(
4545
name,
4646
() =>
4747
pipe(
48-
Effect.suspend(self),
48+
Effect.isEffect(self) ? self : Effect.suspend(self),
4949
Effect.provide(TestEnv),
5050
Effect.runPromise
5151
),
@@ -54,14 +54,14 @@ export const effect = (() => {
5454
},
5555
only: <E, A>(
5656
name: string,
57-
self: () => Effect.Effect<A, E, TestServices.TestServices>,
57+
self: Effect.Effect<A, E, TestServices.TestServices> | (() => Effect.Effect<A, E, TestServices.TestServices>),
5858
timeout = 5_000
5959
) => {
6060
return it.only(
6161
name,
6262
() =>
6363
pipe(
64-
Effect.suspend(self),
64+
Effect.isEffect(self) ? self : Effect.suspend(self),
6565
Effect.provide(TestEnv),
6666
Effect.runPromise
6767
),
@@ -73,14 +73,14 @@ export const effect = (() => {
7373

7474
export const live = <E, A>(
7575
name: string,
76-
self: () => Effect.Effect<A, E>,
76+
self: Effect.Effect<A, E> | (() => Effect.Effect<A, E>),
7777
timeout = 5_000
7878
) => {
7979
return it(
8080
name,
8181
() =>
8282
pipe(
83-
Effect.suspend(self),
83+
Effect.isEffect(self) ? self : Effect.suspend(self),
8484
Effect.runPromise
8585
),
8686
timeout
@@ -106,14 +106,16 @@ export const flakyTest = <A, E, R>(
106106

107107
export const scoped = <E, A>(
108108
name: string,
109-
self: () => Effect.Effect<A, E, Scope.Scope | TestServices.TestServices>,
109+
self:
110+
| Effect.Effect<A, E, Scope.Scope | TestServices.TestServices>
111+
| (() => Effect.Effect<A, E, Scope.Scope | TestServices.TestServices>),
110112
timeout = 5_000
111113
) => {
112114
return it(
113115
name,
114116
() =>
115117
pipe(
116-
Effect.suspend(self),
118+
Effect.isEffect(self) ? self : Effect.suspend(self),
117119
Effect.scoped,
118120
Effect.provide(TestEnv),
119121
Effect.runPromise

‎vitest.shared.ts

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ const alias = (pkg: string) => ({
88

99
// This is a workaround, see https://github.com/vitest-dev/vitest/issues/4744
1010
const config: UserConfig = {
11+
esbuild: {
12+
target: "es2020"
13+
},
1114
test: {
1215
fakeTimers: {
1316
toFake: undefined

0 commit comments

Comments
 (0)
Please sign in to comment.