| 1 | /** |
| 2 | * ProviderInstanceRegistryHydration — derive a `ProviderInstanceConfigMap` |
| 3 | * from `ServerSettings` and keep `ProviderInstanceRegistry` in sync with it. |
| 4 | * |
| 5 | * `settings.providerInstances` is the source of truth. Every built-in driver |
| 6 | * with a default instance also runs at `defaultInstanceIdForDriver(kind)` |
| 7 | * when that slot has no entry, using the driver's default config, so a fresh |
| 8 | * install shows its built-in providers without writing settings first. |
| 9 | * |
| 10 | * Hot-reload |
| 11 | * ---------- |
| 12 | * On layer build we: |
| 13 | * 1. Read the current `ServerSettings` once and use it to seed the |
| 14 | * registry's initial state via `ProviderInstanceRegistry.layer`. |
| 15 | * 2. Fork a daemon fiber (lifetime tied to the layer's scope) that |
| 16 | * acquires `ServerSettingsService.subscribeChanges` and calls |
| 17 | * `ProviderInstanceRegistryMutator.reconcile` on every emission. |
| 18 | * |
| 19 | * Failures inside the watcher are logged and swallowed so a single bad |
| 20 | * settings emission cannot kill the registry. Unknown drivers and invalid |
| 21 | * configs already round-trip through the registry's own "unavailable" |
| 22 | * shadow bucket. |
| 23 | * |
| 24 | * @module provider/ProviderInstanceRegistryHydration |
| 25 | */ |
| 26 | import { |
| 27 | defaultInstanceIdForDriver, |
| 28 | providerInstanceConfigEnabledFlag, |
| 29 | resolveProviderInstanceEnabled, |
| 30 | type ProviderInstanceConfig, |
| 31 | type ProviderInstanceConfigMap, |
| 32 | type ProviderInstanceId, |
| 33 | ServerSettings, |
| 34 | } from "@t3tools/contracts"; |
| 35 | import * as Effect from "effect/Effect"; |
| 36 | import * as Layer from "effect/Layer"; |
| 37 | import * as Stream from "effect/Stream"; |
| 38 | |
| 39 | import * as Settings from "../serverSettings.ts"; |
| 40 | import { BUILT_IN_DRIVERS, type BuiltInDriversEnv } from "./builtInDrivers.ts"; |
| 41 | import * as ProviderInstanceRegistry from "./ProviderInstanceRegistry.ts"; |
| 42 | import * as ProviderInstanceRegistryMutator from "./ProviderInstanceRegistryMutator.ts"; |
| 43 | import * as ProviderOrchestrationAdapterInfrastructure from "./ProviderOrchestrationAdapterInfrastructure.ts"; |
| 44 | import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport"; |
| 45 | import * as ProviderHostLive from "./ProviderHostLive.ts"; |
| 46 | import type * as ProviderHost from "@t3tools/provider-core/server/ProviderHost"; |
| 47 | import type * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; |
| 48 | import type * as ServerConfig from "../config.ts"; |
| 49 | |
| 50 | type ProviderInstanceRegistryHydrationEnv = |
| 51 | | Exclude< |
| 52 | BuiltInDriversEnv, |
| 53 | | ProviderOrchestrationAdapterInfrastructure.ProviderOrchestrationAdapterInfrastructure |
| 54 | | AcpRegistrySupport.AcpRegistryCatalog |
| 55 | | ProviderHost.ProviderHost |
| 56 | > |
| 57 | | Settings.ServerSettingsService |
| 58 | // Requirements of the `ProviderHost.ProviderHost` the drivers receive. |
| 59 | | BackgroundPolicy.BackgroundPolicy |
| 60 | | ServerConfig.ServerConfig; |
| 61 | |
| 62 | /** |
| 63 | * Explicit `providerInstances` entries plus an implicit default instance for |
| 64 | * each built-in driver whose default slot is empty. Pure so the hydration |
| 65 | * rule can be tested without layers. |
| 66 | */ |
| 67 | export const deriveProviderInstanceConfigMap = ( |
| 68 | settings: ServerSettings, |
| 69 | ): ProviderInstanceConfigMap => { |
| 70 | const merged: Record<string, ProviderInstanceConfig> = { ...settings.providerInstances }; |
| 71 | |
| 72 | for (const driver of BUILT_IN_DRIVERS) { |
| 73 | if (driver.metadata.hasDefaultInstance === false) continue; |
| 74 | const instanceId = defaultInstanceIdForDriver(driver.driverKind); |
| 75 | if (instanceId in merged) continue; |
| 76 | merged[instanceId] = { driver: driver.driverKind }; |
| 77 | } |
| 78 | |
| 79 | // A project that turns an instance on needs it alive and probed even when |
| 80 | // the machine itself has it off, or a thread in that project would route |
| 81 | // to an instance nobody ever checked in on. Fold every project's |
| 82 | // `providerInstances[id].enabled` overrides in: an instance already enabled |
| 83 | // is untouched, and one absent from settings entirely (deleted) stays out. |
| 84 | for (const overrides of Object.values(settings.projectSettingsOverrides)) { |
| 85 | for (const [rawInstanceId, projectOverride] of Object.entries( |
| 86 | overrides?.providerInstances ?? {}, |
| 87 | )) { |
| 88 | if (projectOverride.enabled !== true) continue; |
| 89 | const instanceId = rawInstanceId as ProviderInstanceId; |
| 90 | const existing = merged[instanceId]; |
| 91 | if (existing === undefined || resolveProviderInstanceEnabled(existing)) continue; |
| 92 | merged[instanceId] = { |
| 93 | ...existing, |
| 94 | enabled: true, |
| 95 | ...(providerInstanceConfigEnabledFlag(existing.config) === false |
| 96 | ? { config: { ...(existing.config as Record<string, unknown>), enabled: true } } |
| 97 | : {}), |
| 98 | }; |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | return merged as ProviderInstanceConfigMap; |
| 103 | }; |
| 104 | |
| 105 | /** |
| 106 | * Layer that consumes `ProviderInstanceRegistryMutator` and forks a |
| 107 | * settings-watcher fiber. The fiber's lifetime is tied to the enclosing |
| 108 | * layer scope (process lifetime in production), so it is interrupted on |
| 109 | * shutdown without leaking. |
| 110 | * |
| 111 | * Errors inside the watcher are logged and swallowed — the registry's own |
| 112 | * "unavailable" bucket already absorbs unknown drivers and invalid |
| 113 | * configs, so the only way the watcher could fail is a settings stream |
| 114 | * tear-down, which logs and exits cleanly. |
| 115 | */ |
| 116 | const layerSettingsWatcher = Layer.effectDiscard( |
| 117 | Effect.gen(function* () { |
| 118 | const mutator = yield* ProviderInstanceRegistryMutator.ProviderInstanceRegistryMutator; |
| 119 | const serverSettings = yield* Settings.ServerSettingsService; |
| 120 | const settingsChanges = yield* serverSettings.subscribeChanges; |
| 121 | yield* settingsChanges.pipe( |
| 122 | Stream.runForEach((next) => |
| 123 | mutator |
| 124 | .reconcile(deriveProviderInstanceConfigMap(next)) |
| 125 | .pipe( |
| 126 | Effect.catchCause((cause) => |
| 127 | Effect.logError("ProviderInstanceRegistry reconcile failed", cause), |
| 128 | ), |
| 129 | ), |
| 130 | ), |
| 131 | Effect.forkScoped, |
| 132 | ); |
| 133 | }), |
| 134 | ); |
| 135 | |
| 136 | /** |
| 137 | * Hydrate `ProviderInstanceRegistry` from `ServerSettings` and keep it in |
| 138 | * sync with subsequent `streamChanges` emissions. |
| 139 | * |
| 140 | * The Layer's two halves: |
| 141 | * - `ProviderInstanceRegistry.layer` produces the registry + |
| 142 | * mutator from the initial config map. Its scope owns every |
| 143 | * per-instance child scope created during reconcile. |
| 144 | * - `SettingsWatcherLive` consumes the mutator, acquires its settings |
| 145 | * subscription before forking, and runs a daemon fiber in the same scope. |
| 146 | * |
| 147 | * Composing via `Layer.provideMerge` makes the watcher's deps available |
| 148 | * from the mutable layer while still surfacing the registry as an output. |
| 149 | * The mutator tag is technically also exposed; only this module imports |
| 150 | * it, so the visibility leak is harmless in practice. |
| 151 | */ |
| 152 | export const layer: Layer.Layer< |
| 153 | ProviderInstanceRegistry.ProviderInstanceRegistry, |
| 154 | never, |
| 155 | ProviderInstanceRegistryHydrationEnv |
| 156 | > = Layer.unwrap( |
| 157 | Effect.gen(function* () { |
| 158 | const serverSettings = yield* Settings.ServerSettingsService; |
| 159 | const initialSettings: ServerSettings | undefined = yield* serverSettings.getSettings.pipe( |
| 160 | Effect.orElseSucceed(() => undefined), |
| 161 | ); |
| 162 | const initialConfigMap = |
| 163 | initialSettings === undefined |
| 164 | ? ({} as ProviderInstanceConfigMap) |
| 165 | : deriveProviderInstanceConfigMap(initialSettings); |
| 166 | |
| 167 | const layerMutable = ProviderInstanceRegistry.layer({ |
| 168 | drivers: BUILT_IN_DRIVERS, |
| 169 | configMap: initialConfigMap, |
| 170 | }).pipe( |
| 171 | Layer.provide(ProviderOrchestrationAdapterInfrastructure.layer), |
| 172 | Layer.provide(AcpRegistrySupport.layerFromHost), |
| 173 | Layer.provide(ProviderHostLive.layer), |
| 174 | ); |
| 175 | |
| 176 | return layerSettingsWatcher.pipe(Layer.provideMerge(layerMutable)); |
| 177 | }), |
| 178 | ) as Layer.Layer< |
| 179 | ProviderInstanceRegistry.ProviderInstanceRegistry, |
| 180 | never, |
| 181 | ProviderInstanceRegistryHydrationEnv |
| 182 | >; |