/// /** * 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) ) }