1/**
2 * Runs many effects against services built once from a `Layer`.
3 *
4 * A `ManagedRuntime` builds the services from a layer, keeps those services
5 * available for repeated effect runs, and releases acquired resources when it
6 * is disposed. This module includes the runtime type, a constructor, a guard,
7 * and runners for connecting Effect programs to JavaScript entry points such as
8 * promises, callbacks, and synchronous code.
9 *
10 * @since 2.0.0
11 */
12import type * as Context from "./Context.ts"
13import * as Effect from "./Effect.ts"
14import * as Exit from "./Exit.ts"
15import * as Fiber from "./Fiber.ts"
16import * as Layer from "./Layer.ts"
17import { hasProperty } from "./Predicate.ts"
18import * as Scope from "./Scope.ts"
19import type { Mutable } from "./Types.ts"
20
21const TypeId = "~effect/ManagedRuntime"
22
23/**
24 * Checks whether the provided argument is a `ManagedRuntime`.
25 *
26 * **When to use**
27 *
28 * Use to narrow an unknown value before treating it as a `ManagedRuntime`.
29 *
30 * **Details**
31 *
32 * The guard checks the internal `ManagedRuntime` marker property. It does not
33 * build the layer or inspect the runtime's services.
34 *
35 * **Gotchas**
36 *
37 * Disposed runtimes still carry the marker, so this guard does not prove the
38 * runtime is still usable.
39 *
40 * @see {@link make} for creating managed runtimes this guard recognizes
41 *
42 * @category guards
43 * @since 3.9.0
44 */
45export const isManagedRuntime = (input: unknown): input is ManagedRuntime<unknown, unknown> =>
46 hasProperty(input, TypeId)
47
48/**
49 * Type helpers associated with `ManagedRuntime`.
50 *
51 * **When to use**
52 *
53 * Use to reference type-level helpers for extracting managed runtime services
54 * and layer errors.
55 *
56 * @since 3.4.0
57 */
58export declare namespace ManagedRuntime {
59 /**
60 * Extracts the services available from a `ManagedRuntime`.
61 *
62 * **When to use**
63 *
64 * Use to derive the service requirements provided by an existing
65 * `ManagedRuntime` type.
66 *
67 * @category utility types
68 * @since 3.4.0
69 */
70 export type Services<T extends ManagedRuntime<never, any>> = [T] extends [ManagedRuntime<infer R, infer _E>] ? R
71 : never
72 /**
73 * Extracts the layer construction error type of a `ManagedRuntime`.
74 *
75 * **When to use**
76 *
77 * Use to derive the layer construction error type from an existing
78 * `ManagedRuntime` type.
79 *
80 * @category utility types
81 * @since 3.4.0
82 */
83 export type Error<T extends ManagedRuntime<never, any>> = [T] extends [ManagedRuntime<infer _R, infer E>] ? E : never
84}
85
86/**
87 * A runtime built from a layer that can execute effects requiring that layer's
88 * services.
89 *
90 * **When to use**
91 *
92 * Use as the reusable runtime value returned by `make` when application entry
93 * points or integration code need to run many effects against the same
94 * layer-built services.
95 *
96 * **Details**
97 *
98 * The runtime builds and caches its service context and owns the scope for
99 * resources acquired by the layer.
100 *
101 * **Gotchas**
102 *
103 * Dispose the runtime with `dispose` or `disposeEffect` when it is no longer
104 * needed.
105 *
106 * @see {@link make} for constructing a managed runtime from a layer
107 * @see {@link Layer.build} for lower-level scoped layer construction
108 *
109 * @category models
110 * @since 2.0.0
111 */
112export interface ManagedRuntime<in R, out ER> {
113 readonly [TypeId]: typeof TypeId
114 readonly memoMap: Layer.MemoMap
115 readonly contextEffect: Effect.Effect<Context.Context<R>, ER>
116 readonly context: () => Promise<Context.Context<R>>
117
118 // internal
119 readonly scope: Scope.Closeable
120 // internal
121 cachedContext: Context.Context<R> | undefined
122
123 /**
124 * Executes the effect using the provided Scheduler or using the global
125 * Scheduler if not provided
126 *
127 * **When to use**
128 *
129 * Use to fork an effect against this runtime's services and get the running
130 * fiber.
131 */
132 readonly runFork: <A, E>(
133 self: Effect.Effect<A, E, R>,
134 options?: Effect.RunOptions
135 ) => Fiber.Fiber<A, E | ER>
136
137 /**
138 * Executes the effect synchronously returning the exit.
139 *
140 * **When to use**
141 *
142 * Use when invoking this effectful method at the edges of your
143 * program.
144 */
145 readonly runSyncExit: <A, E>(effect: Effect.Effect<A, E, R>) => Exit.Exit<A, ER | E>
146
147 /**
148 * Executes the effect synchronously throwing in case of errors or async boundaries.
149 *
150 * **When to use**
151 *
152 * Use when invoking this effectful method at the edges of your
153 * program.
154 */
155 readonly runSync: <A, E>(effect: Effect.Effect<A, E, R>) => A
156
157 /**
158 * Executes the effect asynchronously, eventually passing the exit value to
159 * the specified callback.
160 *
161 * **When to use**
162 *
163 * Use when invoking this effectful method at the edges of your
164 * program.
165 */
166 readonly runCallback: <A, E>(
167 effect: Effect.Effect<A, E, R>,
168 options?:
169 | Effect.RunOptions & {
170 readonly onExit: (exit: Exit.Exit<A, E | ER>) => void
171 }
172 | undefined
173 ) => (interruptor?: number | undefined) => void
174
175 /**
176 * Runs the `Effect`, returning a JavaScript `Promise` that will be resolved
177 * with the value of the effect once the effect has been executed, or will be
178 * rejected with the first error or exception throw by the effect.
179 *
180 * **When to use**
181 *
182 * Use when invoking this effectful method at the edges of your
183 * program.
184 */
185 readonly runPromise: <A, E>(effect: Effect.Effect<A, E, R>, options?: Effect.RunOptions) => Promise<A>
186
187 /**
188 * Runs the `Effect`, returning a JavaScript `Promise` that will be resolved
189 * with the `Exit` state of the effect once the effect has been executed.
190 *
191 * **When to use**
192 *
193 * Use when invoking this effectful method at the edges of your
194 * program.
195 */
196 readonly runPromiseExit: <A, E>(
197 effect: Effect.Effect<A, E, R>,
198 options?: Effect.RunOptions
199 ) => Promise<Exit.Exit<A, ER | E>>
200
201 /**
202 * Dispose of the resources associated with the runtime.
203 *
204 * **When to use**
205 *
206 * Use to release this runtime's layer resources from Promise-based code.
207 */
208 readonly dispose: () => Promise<void>
209
210 /**
211 * Dispose of the resources associated with the runtime.
212 *
213 * **When to use**
214 *
215 * Use with the `await using` syntax to automatically dispose the runtime
216 * when it goes out of scope.
217 */
218 readonly [Symbol.asyncDispose]: () => Promise<void>
219
220 /**
221 * Dispose of the resources associated with the runtime.
222 *
223 * **When to use**
224 *
225 * Use to release this runtime's layer resources from an `Effect` workflow.
226 */
227 readonly disposeEffect: Effect.Effect<void, never, never>
228}
229
230/**
231 * Creates a `ManagedRuntime` from a layer.
232 *
233 * **When to use**
234 *
235 * Use to create a reusable runtime from a `Layer` for application entry points
236 * or integration code that runs many effects without rebuilding services.
237 *
238 * **Details**
239 *
240 * The layer is built lazily on first use and its context is cached for
241 * subsequent runs. Resources acquired by the layer are owned by the runtime and
242 * are released when `dispose` or `disposeEffect` is run. `options.memoMap` can
243 * be used to share layer memoization with other layer builds.
244 *
245 * **Gotchas**
246 *
247 * Dispose the runtime when it is no longer needed. A runtime cannot be reused
248 * after disposal.
249 *
250 * **Example** (Creating a managed runtime)
251 *
252 * ```ts import.meta.vitest
253 * import { Context, Effect, Layer, ManagedRuntime } from "effect"
254 *
255 * const notifications: Array<string> = []
256 *
257 * class Notifications extends Context.Service<Notifications, {
258 * readonly notify: (message: string) => Effect.Effect<void>
259 * }>()("Notifications") {
260 * static readonly layer = Layer.succeed(this)({
261 * notify: Effect.fn("Notifications.notify")((message) =>
262 * Effect.sync(() => notifications.push(message))
263 * )
264 * })
265 * }
266 *
267 * const runtime = ManagedRuntime.make(Notifications.layer)
268 *
269 * const program = Effect.flatMap(
270 * Notifications,
271 * (_) => _.notify("Hello, world!")
272 * ).pipe(Effect.ensuring(runtime.disposeEffect))
273 *
274 * await runtime.runPromise(program)
275 * notifications // => ["Hello, world!"]
276 * ```
277 *
278 * @see {@link ManagedRuntime} for the returned runtime interface
279 * @see {@link Layer.MemoMap} for shared layer memoization
280 * @see {@link Layer.build} for lower-level scoped layer construction
281 *
282 * @category constructors
283 * @since 2.0.0
284 */
285export const make = <R, ER>(
286 layer: Layer.Layer<R, ER, never>,
287 options?: {
288 readonly memoMap?: Layer.MemoMap | undefined
289 } | undefined
290): ManagedRuntime<R, ER> => {
291 const memoMap = options?.memoMap ?? Layer.makeMemoMapUnsafe()
292 const scope = Scope.makeUnsafe("sequential")
293 const layerScope = Scope.forkUnsafe(scope, "sequential")
294 const fiberScope = Scope.forkUnsafe(scope, "parallel")
295 const defaultRunOptions: Effect.RunOptions = {
296 onFiberStart: Fiber.runIn(fiberScope)
297 }
298 const mergeRunOptions = <O extends Effect.RunOptions>(options?: O): O =>
299 options
300 ? {
301 ...options,
302 onFiberStart: options.onFiberStart ?
303 (fiber) => {
304 defaultRunOptions.onFiberStart!(fiber)
305 options.onFiberStart!(fiber)
306 } :
307 defaultRunOptions.onFiberStart
308 }
309 : defaultRunOptions as O
310 let buildFiber: Fiber.Fiber<Context.Context<R>, ER> | undefined
311 const contextEffect = Effect.withFiber<Context.Context<R>, ER>((fiber) => {
312 if (!buildFiber) {
313 buildFiber = Effect.runFork(
314 Effect.tap(
315 Layer.buildWithMemoMap(layer, memoMap, layerScope),
316 (context) =>
317 Effect.sync(() => {
318 self.cachedContext = context
319 })
320 ),
321 { ...defaultRunOptions, scheduler: fiber.cache.scheduler }
322 )
323 }
324 return Effect.flatten(Fiber.await(buildFiber))
325 })
326 const self: ManagedRuntime<R, ER> = {
327 [TypeId]: TypeId,
328 memoMap,
329 scope,
330 contextEffect: contextEffect,
331 cachedContext: undefined,
332 context() {
333 return self.cachedContext === undefined ?
334 Effect.runPromise(self.contextEffect) :
335 Promise.resolve(self.cachedContext)
336 },
337 dispose(): Promise<void> {
338 return Effect.runPromise(self.disposeEffect)
339 },
340 [Symbol.asyncDispose](): Promise<void> {
341 return self.dispose()
342 },
343 disposeEffect: Effect.suspend(() => {
344 ;(self as Mutable<ManagedRuntime<R, ER>>).contextEffect = Effect.die("ManagedRuntime disposed")
345 self.cachedContext = undefined
346 return Scope.close(self.scope, Exit.void)
347 }),
348 runFork<A, E>(effect: Effect.Effect<A, E, R>, options?: Effect.RunOptions): Fiber.Fiber<A, E | ER> {
349 return self.cachedContext === undefined ?
350 Effect.runFork(provide(self, effect), mergeRunOptions(options)) :
351 Effect.runForkWith(self.cachedContext)(effect, mergeRunOptions(options))
352 },
353 runCallback<A, E>(
354 effect: Effect.Effect<A, E, R>,
355 options?: Effect.RunOptions & {
356 readonly onExit: (exit: Exit.Exit<A, E | ER>) => void
357 }
358 ): (interruptor?: number | undefined) => void {
359 return self.cachedContext === undefined ?
360 Effect.runCallback(provide(self, effect), mergeRunOptions(options)) :
361 Effect.runCallbackWith(self.cachedContext)(effect, mergeRunOptions(options))
362 },
363 runSyncExit<A, E>(effect: Effect.Effect<A, E, R>): Exit.Exit<A, E | ER> {
364 return self.cachedContext === undefined ?
365 Effect.runSyncExit(provide(self, effect)) :
366 Effect.runSyncExitWith(self.cachedContext)(effect)
367 },
368 runSync<A, E>(effect: Effect.Effect<A, E, R>): A {
369 return self.cachedContext === undefined ?
370 Effect.runSync(provide(self, effect)) :
371 Effect.runSyncWith(self.cachedContext)(effect)
372 },
373 runPromiseExit<A, E>(effect: Effect.Effect<A, E, R>, options?: Effect.RunOptions): Promise<Exit.Exit<A, E | ER>> {
374 return self.cachedContext === undefined ?
375 Effect.runPromiseExit(provide(self, effect), mergeRunOptions(options)) :
376 Effect.runPromiseExitWith(self.cachedContext)(effect, mergeRunOptions(options))
377 },
378 runPromise<A, E>(effect: Effect.Effect<A, E, R>, options?: {
379 readonly signal?: AbortSignal | undefined
380 }): Promise<A> {
381 return self.cachedContext === undefined ?
382 Effect.runPromise(provide(self, effect), mergeRunOptions(options)) :
383 Effect.runPromiseWith(self.cachedContext)(effect, mergeRunOptions(options))
384 }
385 }
386 return self
387}
388
389function provide<R, ER, A, E>(
390 managed: ManagedRuntime<R, ER>,
391 effect: Effect.Effect<A, E, R>
392): Effect.Effect<A, E | ER> {
393 return Effect.flatMap(
394 managed.contextEffect,
395 (context) => Effect.provideContext(effect, context)
396 )
397}