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 */
26import {
27 defaultInstanceIdForDriver,
28 providerInstanceConfigEnabledFlag,
29 resolveProviderInstanceEnabled,
30 type ProviderInstanceConfig,
31 type ProviderInstanceConfigMap,
32 type ProviderInstanceId,
33 ServerSettings,
34} from "@t3tools/contracts";
35import * as Effect from "effect/Effect";
36import * as Layer from "effect/Layer";
37import * as Stream from "effect/Stream";
38
39import * as Settings from "../serverSettings.ts";
40import { BUILT_IN_DRIVERS, type BuiltInDriversEnv } from "./builtInDrivers.ts";
41import * as ProviderInstanceRegistry from "./ProviderInstanceRegistry.ts";
42import * as ProviderInstanceRegistryMutator from "./ProviderInstanceRegistryMutator.ts";
43import * as ProviderOrchestrationAdapterInfrastructure from "./ProviderOrchestrationAdapterInfrastructure.ts";
44import * as AcpRegistrySupport from "@t3tools/provider-acp-registry/server/AcpRegistrySupport";
45import * as ProviderHostLive from "./ProviderHostLive.ts";
46import type * as ProviderHost from "@t3tools/provider-core/server/ProviderHost";
47import type * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
48import type * as ServerConfig from "../config.ts";
49
50type 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 */
67export 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 */
116const 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 */
152export 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>;