import { type Effect, Option, type Schema, type Scope } from 'effect'; import type { ManagedResource } from '../managedResource/index.js'; /** Internal configuration for a single Managed Resource, used by the runtime. */ export type ManagedResourceConfig = { readonly schema: Schema.Schema; readonly resource: ManagedResource; readonly modelToMaybeRequirements: (model: Model) => any; readonly acquire: (params: any) => Effect.Effect; readonly release: (value: any) => Effect.Effect; readonly onAcquired: (value: any) => Message; readonly onReleased: () => Message; readonly onAcquireError: (error: unknown) => Message; }; /** A record of named Managed Resource configurations, keyed by resource name. */ export type ManagedResources = Record> & { readonly __managedResourceServices?: Services; }; type EntryBrand = { readonly __managedResourceEntry: never; }; /** * The requirements value the runtime hands to `acquire`. When the requirements * schema is wrapped in `S.Option`, the runtime unwraps the `Some` before * calling `acquire`, so the parameter is the inner type. */ type AcquireParams = Requirements extends Option.Option ? Params : Requirements; /** * A single Managed Resource entry produced by `ManagedResource.make`, * `ManagedResource.lift`, or `ManagedResource.aggregate`. The brand field is * `never`, so application code cannot manually construct one: it must go * through a constructor. * * The `Value` parameter is the type the resource holds while active: what * `acquire` produces, what `release` and `onAcquired` receive, and what the * tag's `.get` yields to commands. It is inferred from the `resource` tag. * * The `Service` parameter carries the resource tag's identity so `make`, * `lift`, and `aggregate` can union the services a record requires. Read the * union off a finished record with `ManagedResource.ServicesOf`. */ export type Entry = { readonly schema: Schema.Schema; readonly resource: ManagedResource; readonly modelToMaybeRequirements: (model: Model) => Requirements; readonly acquire: (params: AcquireParams) => Effect.Effect; readonly release: (value: Value) => Effect.Effect; readonly onAcquired: (value: Value) => Message; readonly onReleased: () => Message; readonly onAcquireError: (error: unknown) => Message; } & EntryBrand; /** Type-level utility to extract the service union from a Managed Resources record. */ export type ServicesOf = { [Key in keyof Resources]: Resources[Key] extends { readonly resource: ManagedResource; } ? Service : never; }[keyof Resources]; /** * Builds a single Managed Resource entry from a requirements schema and a * config. Reading the schema as a positional argument (rather than a property * on the config literal) lets TypeScript fully resolve the requirements type * before contextually typing `modelToMaybeRequirements` and `acquire`, so * destructuring patterns are inferred correctly even when the schema uses * transforms like `S.Option`. */ export type EntryBuilder = , Value, Service>(schema: RequirementsSchema, config: { readonly resource: ManagedResource; readonly modelToMaybeRequirements: (model: Model) => Schema.Schema.Type; readonly acquire: (params: AcquireParams>) => Effect.Effect; readonly release: (value: Value) => Effect.Effect; readonly onAcquired: (value: Value) => Message; readonly onReleased: () => Message; readonly onAcquireError: (error: unknown) => Message; }) => Entry, Value, Service>; /** * Declares a Managed Resources record. The Model and Message generics are * provided up front; the entries record follows, built from calls to the * `entry` builder passed into the inner function. * * Use this when a resource is expensive or stateful and should only exist while * the model is in a particular state: a camera stream during a video call, a * WebSocket connection while on a chat page, or a Web Worker pool during a * computation. For resources that live for the entire application lifetime, use * the static `resources` config instead. * * Reach for `ManagedResource.aggregate` to combine multiple records, and * `ManagedResource.lift` to translate a child Submodel's record into a parent * context. * * **Lifecycle** — The runtime watches each entry's `modelToMaybeRequirements` * after every model update, structurally comparing the result against the * previous value: * * - `Option.none()` → `Option.some(params)`: calls `acquire(params)`, then * dispatches `onAcquired(value)`. * - `Option.some(paramsA)` → `Option.some(paramsB)` (structurally different): * releases the old resource, then acquires a new one with `paramsB`. * - `Option.some(params)` → `Option.none()`: calls `release(value)`, then * dispatches `onReleased()`. No re-acquisition occurs. * * If `acquire` fails, `onAcquireError` is dispatched and the resource daemon * continues watching for the next requirements change: a failed acquisition * does not crash the application. * * **Config fields:** * * - `resource` — The identity tag created with `ManagedResource.tag`. Appears * in the Effect R channel so commands that call `.get` are type-checked. * - `modelToMaybeRequirements` — Extracts requirements from the model. * `Option.none()` means "release", `Option.some(params)` means * "acquire/re-acquire if params changed". For resources with no * parameters, use `S.Option(S.Null)` and return `Option.some(null)`. * - `acquire` — Creates the resource from the unwrapped params. The returned * Effect should fail when acquisition fails: errors in the error channel * flow to `onAcquireError` as a message instead of crashing the runtime. * `acquire` runs with the resource-lifetime `Scope.Scope` in its context, so * it can build an Effect `Layer` with `Layer.build` or register finalizers * with `Effect.addFinalizer` whose teardown is tied to the resource. Those * finalizers run when the resource is released or re-acquired, after the * explicit `release` callback (scope finalizers run in last-in-first-out * order, and the runtime registers `release` after `acquire` completes). A * `Layer`-built resource therefore needs only `release: () => Effect.void`: * the `Layer` finalizers run automatically when the scope closes. * - `release` — Tears down the resource. Errors thrown here are silently * swallowed: release must not block cleanup. Resources that register their * teardown as scope finalizers in `acquire` leave this as `() => Effect.void`. * - `onAcquired` — Message dispatched when `acquire` succeeds. * - `onAcquireError` — Message dispatched when `acquire` fails. * - `onReleased` — Message dispatched after `release` completes. * * @example * ```ts * const CameraStream = ManagedResource.tag()('CameraStream') * * const managedResources = ManagedResource.make()(entry => ({ * camera: entry(S.Option(S.Struct({ facingMode: S.String })), { * resource: CameraStream, * modelToMaybeRequirements: model => * pipe( * model.callState, * Option.liftPredicate( * (callState): callState is typeof InCall.Type => * callState._tag === 'InCall', * ), * Option.map(callState => ({ facingMode: callState.facingMode })), * ), * acquire: ({ facingMode }) => * Effect.tryPromise(() => * navigator.mediaDevices.getUserMedia({ video: { facingMode } }), * ), * release: stream => * Effect.sync(() => stream.getTracks().forEach(track => track.stop())), * onAcquired: () => AcquiredCamera(), * onAcquireError: error => FailedAcquireCamera({ error: String(error) }), * onReleased: () => ReleasedCamera(), * }), * })) * ``` * * @see {@link ManagedResource.tag} for creating the resource identity. */ export declare const make: () => >>(build: (entry: EntryBuilder) => Entries) => Entries; type ChildModelOf = Resources[keyof Resources] extends Entry ? ChildModel : never; type ChildMessageOf = Resources[keyof Resources] extends Entry ? ChildMessage : never; /** * Lifts a record of child Managed Resources into a parent's Model and Message * context, applying a Model accessor and a Message wrapper uniformly to every * entry. Per-entry requirements schemas and resource services are preserved. * * Unlike `Subscription.lift`, `toChildModel` returns an `Option`: a managed * resource already speaks in `Option` (`modelToMaybeRequirements` returns * `Option.none()` to release), and a child Submodel that owns a managed * resource is itself something that mounts and unmounts. A missing child is * just another `None` and flows through the same acquire/release channel, so * each lifted entry's requirements must be `S.Option`-wrapped. */ export declare const lift: , any, any>>>(resources: Resources) => (config: { readonly toChildModel: (parentModel: ParentModel) => Option.Option>; readonly toParentMessage: (message: ChildMessageOf) => ParentMessage; }) => { readonly [Key in keyof Resources]: Resources[Key] extends Entry ? Entry : never; }; type MergeRecords> = Records extends readonly [infer Head, ...infer Rest] ? Head & (Rest extends ReadonlyArray ? MergeRecords : {}) : {}; /** * Combines multiple Managed Resources records into one. Throws on duplicate * keys so a misconfigured aggregate fails loudly at startup rather than * silently overriding. */ export declare const aggregate: () => >>>(...records: Records) => MergeRecords; export {}; //# sourceMappingURL=managedResource.d.ts.map