/** * ProviderInstanceRegistryHydration — derive a `ProviderInstanceConfigMap` * from `ServerSettings` and keep `ProviderInstanceRegistry` in sync with it. * * The server still reads two shapes: * * 1. `settings.providerInstances` — the new driver-agnostic map the * registry expects. Keyed by `ProviderInstanceId`, values are * `ProviderInstanceConfig` envelopes. * 2. `settings.providers.` — the legacy single-instance-per-driver * fields (`providers.codex`, `providers.claudeAgent`, …). These are * the source of truth for every deployment that hasn't been migrated * yet to an explicit `providerInstances` entry. * * This module bridges (2) into (1) and wires the resulting map into a * mutable registry. For every built-in driver whose id is not already * present in `providerInstances` (keyed on * `defaultInstanceIdForDriver(driverKind)` — literally the driver kind as a * routing slug), we synthesize an envelope from the legacy field. The * registry decodes both flavours through the same `configSchema` and ends * up with one uniform `ProviderInstance` per entry. * * Explicit `providerInstances` entries always win — users can already * override the legacy `providers.` blob by authoring a * `providerInstances.codex` entry with a matching driver, and we don't * want the synthesized envelope to silently stomp their config. * * Hot-reload * ---------- * On layer build we: * 1. Read the current `ServerSettings` once and use it to seed the * registry's initial state via `ProviderInstanceRegistryMutableLayer`. * 2. Fork a daemon fiber (lifetime tied to the layer's scope) that * subscribes to `ServerSettingsService.streamChanges` and calls * `ProviderInstanceRegistryMutator.reconcile` on every emission. * * Failures inside the watcher are logged and swallowed so a single bad * settings emission cannot kill the registry. Unknown drivers and invalid * configs already round-trip through the registry's own "unavailable" * shadow bucket. * * @module provider/Layers/ProviderInstanceRegistryHydration */ import { defaultInstanceIdForDriver, type ProviderInstanceConfig, type ProviderInstanceConfigMap, ServerSettings, } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Stream from "effect/Stream"; import { ServerSettingsService } from "../../serverSettings.ts"; import { BUILT_IN_DRIVERS, type BuiltInDriversEnv } from "../builtInDrivers.ts"; import { ProviderInstanceRegistry } from "../Services/ProviderInstanceRegistry.ts"; import { ProviderInstanceRegistryMutator } from "../Services/ProviderInstanceRegistryMutator.ts"; import { ProviderInstanceRegistryMutableLayer } from "./ProviderInstanceRegistryLive.ts"; /** * Synthesize a `ProviderInstanceConfigMap` from a `ServerSettings` snapshot. * * Strategy: * 1. Copy all explicit `settings.providerInstances` entries verbatim. * 2. For each built-in driver whose `defaultInstanceIdForDriver(id)` key * is *not* already in the explicit map, synthesize an entry from the * matching legacy `settings.providers.` blob. * * The returned map is the input the registry consumes; pure & exported * separately so the hydration logic can be exercised by unit tests * without layering. */ export const deriveProviderInstanceConfigMap = ( settings: ServerSettings, ): ProviderInstanceConfigMap => { const merged: Record = { ...settings.providerInstances }; for (const driver of BUILT_IN_DRIVERS) { const instanceId = defaultInstanceIdForDriver(driver.driverKind); if (instanceId in merged) { // Explicit `providerInstances` entry for this slot — user-authored // config always wins over the legacy mirror. continue; } // Only built-in drivers have a legacy mirror; the registry's // `providers` struct is keyed on the same literal slug as // `driverKind`. Access is dynamic (the driver kind is a branded string), // but it's constrained to `keyof settings.providers` by the union of // built-in driver kinds. const legacyKey = driver.driverKind as keyof ServerSettings["providers"]; const legacyConfig = settings.providers[legacyKey]; if (legacyConfig === undefined) { continue; } merged[instanceId] = { driver: driver.driverKind, config: legacyConfig, }; } return merged as ProviderInstanceConfigMap; }; /** * Layer that consumes `ProviderInstanceRegistryMutator` and forks a * settings-watcher fiber. The fiber's lifetime is tied to the enclosing * layer scope (process lifetime in production), so it is interrupted on * shutdown without leaking. * * Errors inside the watcher are logged and swallowed — the registry's own * "unavailable" bucket already absorbs unknown drivers and invalid * configs, so the only way the watcher could fail is a settings stream * tear-down, which logs and exits cleanly. */ const SettingsWatcherLive: Layer.Layer< never, never, ProviderInstanceRegistryMutator | ServerSettingsService > = Layer.effectDiscard( Effect.gen(function* () { const mutator = yield* ProviderInstanceRegistryMutator; const serverSettings = yield* ServerSettingsService; yield* serverSettings.streamChanges.pipe( Stream.runForEach((next) => mutator .reconcile(deriveProviderInstanceConfigMap(next)) .pipe( Effect.catchCause((cause) => Effect.logError("ProviderInstanceRegistry reconcile failed", cause), ), ), ), Effect.forkScoped, ); }), ); /** * Hydrate `ProviderInstanceRegistry` from `ServerSettings` and keep it in * sync with subsequent `streamChanges` emissions. * * The Layer's two halves: * - `ProviderInstanceRegistryMutableLayer` produces the registry + * mutator from the initial config map. Its scope owns every * per-instance child scope created during reconcile. * - `SettingsWatcherLive` consumes the mutator and runs a daemon fiber * in the same scope. * * Composing via `Layer.provideMerge` makes the watcher's deps available * from the mutable layer while still surfacing the registry as an output. * The mutator tag is technically also exposed; only this module imports * it, so the visibility leak is harmless in practice. */ export const ProviderInstanceRegistryHydrationLive: Layer.Layer< ProviderInstanceRegistry, never, BuiltInDriversEnv | ServerSettingsService > = Layer.unwrap( Effect.gen(function* () { const serverSettings = yield* ServerSettingsService; const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe( Effect.orElseSucceed(() => undefined), ); const initialConfigMap = initialSettings === undefined ? ({} as ProviderInstanceConfigMap) : deriveProviderInstanceConfigMap(initialSettings); const mutableLayer = ProviderInstanceRegistryMutableLayer({ drivers: BUILT_IN_DRIVERS, configMap: initialConfigMap, }); return SettingsWatcherLive.pipe(Layer.provideMerge(mutableLayer)); }), ) as Layer.Layer;