///
/**
* Runs many effects against services built once from a `Layer`.
*
* A `ManagedRuntime` builds the services from a layer, keeps those services
* available for repeated effect runs, and releases acquired resources when it
* is disposed. This module includes the runtime type, a constructor, a guard,
* and runners for connecting Effect programs to JavaScript entry points such as
* promises, callbacks, and synchronous code.
*
* @stability stable
* @since 2.0.0
*/
import * as Context from "./Context.ts"
import * as Deferred from "./Deferred.ts"
import * as Effect from "./Effect.ts"
import * as Exit from "./Exit.ts"
import * as Fiber from "./Fiber.ts"
import { fiberEnterUninterruptibleUnsafe, type FiberImpl, scopeRemoveFinalizerUnsafe } from "./internal/effect.ts"
import * as Layer from "./Layer.ts"
import { hasProperty } from "./Predicate.ts"
import * as Scope from "./Scope.ts"
import type { Mutable } from "./Types.ts"
const TypeId = "~effect/ManagedRuntime"
/**
* Checks whether the provided argument is a `ManagedRuntime`.
*
* **When to use**
*
* Use to narrow an unknown value before treating it as a `ManagedRuntime`.
*
* **Details**
*
* The guard checks the internal `ManagedRuntime` marker property. It does not
* build the layer or inspect the runtime's services.
*
* **Gotchas**
*
* Disposed runtimes still carry the marker, so this guard does not prove the
* runtime is still usable.
*
* @see {@link make} for creating managed runtimes this guard recognizes
*
* @stability stable
* @category guards
* @since 3.9.0
*/
export const isManagedRuntime = (input: unknown): input is ManagedRuntime =>
hasProperty(input, TypeId)
/**
* Type helpers associated with `ManagedRuntime`.
*
* **When to use**
*
* Use to reference type-level helpers for extracting managed runtime services
* and layer errors.
*
* @stability stable
* @since 3.4.0
*/
export declare namespace ManagedRuntime {
/**
* Extracts the services available from a `ManagedRuntime`.
*
* **When to use**
*
* Use to derive the service requirements provided by an existing
* `ManagedRuntime` type.
*
* @category utility types
* @since 3.4.0
*/
export type Services> = [T] extends [ManagedRuntime] ? R
: never
/**
* Extracts the layer construction error type of a `ManagedRuntime`.
*
* **When to use**
*
* Use to derive the layer construction error type from an existing
* `ManagedRuntime` type.
*
* @category utility types
* @since 3.4.0
*/
export type Error> = [T] extends [ManagedRuntime] ? E : never
}
/**
* A runtime built from a layer that can execute effects requiring that layer's
* services.
*
* **When to use**
*
* Use as the reusable runtime value returned by `make` when application entry
* points or integration code need to run many effects against the same
* layer-built services.
*
* **Details**
*
* The runtime builds and caches its service context and owns the scope for
* resources acquired by the layer.
*
* **Gotchas**
*
* Dispose the runtime with `dispose` or `disposeEffect` when it is no longer
* needed.
*
* @see {@link make} for constructing a managed runtime from a layer
* @see {@link Layer.build} for lower-level scoped layer construction
*
* @stability stable
* @category models
* @since 2.0.0
*/
export interface ManagedRuntime {
readonly [TypeId]: typeof TypeId
readonly memoMap: Layer.MemoMap
readonly contextEffect: Effect.Effect, ER>
readonly context: () => Promise>
// internal
readonly scope: Scope.Closeable
// internal
cachedContext: Context.Context | undefined
/**
* Executes the effect using the provided Scheduler or using the global
* Scheduler if not provided
*
* **When to use**
*
* Use to fork an effect against this runtime's services and get the running
* fiber.
*/
readonly runFork: (
self: Effect.Effect,
options?: Effect.RunOptions
) => Fiber.Fiber
/**
* Executes the effect synchronously returning the exit.
*
* **When to use**
*
* Use when invoking this effectful method at the edges of your
* program.
*/
readonly runSyncExit: (effect: Effect.Effect) => Exit.Exit
/**
* Executes the effect synchronously throwing in case of errors or async boundaries.
*
* **When to use**
*
* Use when invoking this effectful method at the edges of your
* program.
*/
readonly runSync: (effect: Effect.Effect) => A
/**
* Executes the effect asynchronously, eventually passing the exit value to
* the specified callback.
*
* **When to use**
*
* Use when invoking this effectful method at the edges of your
* program.
*/
readonly runCallback: (
effect: Effect.Effect,
options?:
| Effect.RunOptions & {
readonly onExit: (exit: Exit.Exit) => void
}
| undefined
) => (interruptor?: number | undefined) => void
/**
* 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.
*
* **When to use**
*
* Use when invoking this effectful method at the edges of your
* program.
*/
readonly runPromise: (effect: Effect.Effect, options?: Effect.RunOptions) => Promise
/**
* Runs the `Effect`, returning a JavaScript `Promise` that will be resolved
* with the `Exit` state of the effect once the effect has been executed.
*
* **When to use**
*
* Use when invoking this effectful method at the edges of your
* program.
*/
readonly runPromiseExit: (
effect: Effect.Effect,
options?: Effect.RunOptions
) => Promise>
/**
* Dispose of the resources associated with the runtime.
*
* **When to use**
*
* Use to release this runtime's layer resources from Promise-based code.
*
* **Details**
*
* Calls made while a disposal is in progress wait for it to finish.
*/
readonly dispose: () => Promise
/**
* Dispose of the resources associated with the runtime.
*
* **When to use**
*
* Use with the `await using` syntax to automatically dispose the runtime
* when it goes out of scope.
*/
readonly [Symbol.asyncDispose]: () => Promise
/**
* Dispose of the resources associated with the runtime.
*
* **When to use**
*
* Use to release this runtime's layer resources from an `Effect` workflow.
*
* **Details**
*
* Disposal runs uninterruptibly. Calls made while a disposal is in progress
* wait for it to finish, and interrupting a waiting call only stops the wait.
* Fibers started by this runtime, their child fibers, and the runtime's
* finalizers do not wait: the disposal interrupts or runs them, so waiting
* would deadlock.
*/
readonly disposeEffect: Effect.Effect
}
// The runtimes whose disposal the current fiber is running. A disposal adds its
// runtime to the set it inherits, so a finalizer that disposes another runtime
// still belongs to the outer disposals. Finalizers and their parallel fibers
// inherit the set from the disposing fiber. It is not provided to the layer
// build, because finalizers of layers shared through a MemoMap run with the
// build context of the runtime that built them, which may not be the runtime
// disposing them.
const DisposingRuntimes = Context.Reference>>(
"effect/ManagedRuntime/DisposingRuntimes",
{
defaultValue: () => new Set()
}
)
/**
* Creates a `ManagedRuntime` from a layer.
*
* **When to use**
*
* Use to create a reusable runtime from a `Layer` for application entry points
* or integration code that runs many effects without rebuilding services.
*
* **Details**
*
* The layer is built lazily on first use and its context is cached for
* subsequent runs. Resources acquired by the layer are owned by the runtime and
* are released when `dispose` or `disposeEffect` is run. `options.memoMap` can
* be used to share layer memoization with other layer builds.
*
* **Gotchas**
*
* Dispose the runtime when it is no longer needed. A runtime cannot be reused
* after disposal.
*
* A failed layer build is also cached: subsequent runs return the same build
* failure without rebuilding the layer. For transient failures, retry inside
* the layer before passing it to `make`, or dispose the runtime and create a
* new one to attempt a fresh build.
*
* **Example** (Creating a managed runtime)
*
* ```ts import.meta.vitest
* import { Context, Effect, Layer, ManagedRuntime } from "effect"
*
* const notifications: Array = []
*
* class Notifications extends Context.Service Effect.Effect
* }>()("Notifications") {
* static readonly layer = Layer.succeed(this)({
* notify: Effect.fn("Notifications.notify")((message) =>
* Effect.sync(() => notifications.push(message))
* )
* })
* }
*
* const runtime = ManagedRuntime.make(Notifications.layer)
*
* const program = Effect.flatMap(
* Notifications,
* (_) => _.notify("Hello, world!")
* ).pipe(Effect.ensuring(runtime.disposeEffect))
*
* await runtime.runPromise(program)
* notifications // => ["Hello, world!"]
* ```
*
* @see {@link ManagedRuntime} for the returned runtime interface
* @see {@link Layer.MemoMap} for shared layer memoization
* @see {@link Layer.build} for lower-level scoped layer construction
*
* @stability stable
* @category constructors
* @since 2.0.0
*/
export const make = (
layer: Layer.Layer,
options?: {
readonly memoMap?: Layer.MemoMap | undefined
} | undefined
): ManagedRuntime => {
const memoMap = options?.memoMap ?? Layer.makeMemoMapUnsafe()
const scope = Scope.makeUnsafe("sequential")
const layerScope = Scope.forkUnsafe(scope, "sequential")
const fiberScope = Scope.forkUnsafe(scope, "parallel")
// Fibers started by the runtime, including the layer build, are interrupted
// and awaited by its disposal, so they and their child fibers must not wait
// for it.
const managedFibers = new WeakSet>()
const disposingFibers = new WeakSet>()
const defaultRunOptions: Effect.RunOptions = {
onFiberStart(fiber) {
managedFibers.add(fiber)
if (disposingFibers.has(fiber)) return
Fiber.runIn(fiber, fiberScope)
}
}
const mergeRunOptions = (options?: O): O =>
options
? {
...options,
onFiberStart: options.onFiberStart ?
(fiber) => {
defaultRunOptions.onFiberStart!(fiber)
options.onFiberStart!(fiber)
} :
defaultRunOptions.onFiberStart
}
: defaultRunOptions as O
let buildFiber: Fiber.Fiber, ER> | undefined
let disposed: Deferred.Deferred | undefined
const contextEffect = Effect.withFiber, ER>((fiber) => {
if (!buildFiber) {
buildFiber = Effect.runFork(
Effect.tap(
Layer.buildWithMemoMap(layer, memoMap, layerScope),
(context) =>
Effect.sync(() => {
self.cachedContext = context
})
),
{
scheduler: fiber.cache.scheduler,
// Unlike requests, the build joins fiberScope even if it disposed the
// runtime, so disposal still interrupts it.
onFiberStart(fiber) {
managedFibers.add(fiber)
Fiber.runIn(fiber, fiberScope)
}
}
)
}
return Effect.flatten(Fiber.await(buildFiber))
})
const self: ManagedRuntime = {
[TypeId]: TypeId,
memoMap,
scope,
contextEffect: contextEffect,
cachedContext: undefined,
context() {
return self.cachedContext === undefined ?
Effect.runPromise(self.contextEffect) :
Promise.resolve(self.cachedContext)
},
dispose(): Promise {
return Effect.runPromise(self.disposeEffect)
},
[Symbol.asyncDispose](): Promise {
return self.dispose()
},
disposeEffect: Effect.withFiber((fiber) => {
if (disposed !== undefined) {
if (fiber.getRef(DisposingRuntimes).has(self)) return Effect.void
for (let current = fiber as FiberImpl | undefined; current; current = current._parent) {
if (managedFibers.has(current)) return Effect.void
}
return Deferred.await(disposed)
}
// Once `disposed` is set the close must run, or later calls would wait forever.
fiberEnterUninterruptibleUnsafe(fiber)
const done = disposed = Deferred.makeUnsafe()
// Closing fiberScope interrupts and awaits every managed fiber. Exempt the
// disposing fiber and its ancestors, which would otherwise await themselves.
for (let current = fiber as FiberImpl | undefined; current; current = current._parent) {
disposingFibers.add(current)
scopeRemoveFinalizerUnsafe(fiberScope, current)
}
;(self as Mutable>).contextEffect = Effect.die("ManagedRuntime disposed")
self.cachedContext = undefined
return Scope.close(self.scope, Exit.void).pipe(
Effect.provideService(DisposingRuntimes, new Set(fiber.getRef(DisposingRuntimes)).add(self)),
Effect.ensuring(Deferred.done(done, Exit.void))
)
}),
runFork(effect: Effect.Effect, options?: Effect.RunOptions): Fiber.Fiber {
return self.cachedContext === undefined ?
Effect.runFork(provide(self, effect), mergeRunOptions(options)) :
Effect.runForkWith(self.cachedContext)(effect, mergeRunOptions(options))
},
runCallback(
effect: Effect.Effect,
options?: Effect.RunOptions & {
readonly onExit: (exit: Exit.Exit) => void
}
): (interruptor?: number | undefined) => void {
return self.cachedContext === undefined ?
Effect.runCallback(provide(self, effect), mergeRunOptions(options)) :
Effect.runCallbackWith(self.cachedContext)(effect, mergeRunOptions(options))
},
runSyncExit(effect: Effect.Effect): Exit.Exit {
return self.cachedContext === undefined ?
Effect.runSyncExit(provide(self, effect)) :
Effect.runSyncExitWith(self.cachedContext)(effect)
},
runSync(effect: Effect.Effect): A {
return self.cachedContext === undefined ?
Effect.runSync(provide(self, effect)) :
Effect.runSyncWith(self.cachedContext)(effect)
},
runPromiseExit(effect: Effect.Effect, options?: Effect.RunOptions): Promise> {
return self.cachedContext === undefined ?
Effect.runPromiseExit(provide(self, effect), mergeRunOptions(options)) :
Effect.runPromiseExitWith(self.cachedContext)(effect, mergeRunOptions(options))
},
runPromise(effect: Effect.Effect, options?: {
readonly signal?: AbortSignal | undefined
}): Promise {
return self.cachedContext === undefined ?
Effect.runPromise(provide(self, effect), mergeRunOptions(options)) :
Effect.runPromiseWith(self.cachedContext)(effect, mergeRunOptions(options))
}
}
return self
}
function provide(
managed: ManagedRuntime,
effect: Effect.Effect
): Effect.Effect {
return Effect.flatMap(
managed.contextEffect,
(context) => Effect.provideContext(effect, context)
)
}