1import {
2 ProviderInstanceId,
3 ProviderSetupError,
4 type OrchestrationV2ProviderCapabilities,
5 type ProviderDriverKind,
6 type ProviderInstanceConfig,
7 type ProviderInstanceConfigMap,
8} from "@t3tools/contracts";
9import * as Context from "effect/Context";
10import * as Effect from "effect/Effect";
11import * as Exit from "effect/Exit";
12import * as Layer from "effect/Layer";
13import * as Schema from "effect/Schema";
14import * as Scope from "effect/Scope";
15
16import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts";
17import {
18 ProviderAdapterDriverCreateError,
19 type AnyProviderAdapterDriver,
20} from "@t3tools/provider-core/server/adapterDriver";
21import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
22
23const isProviderSetupError = Schema.is(ProviderSetupError);
24
25export class ProviderAdapterRegistryLookupError extends Schema.TaggedError<ProviderAdapterRegistryLookupError>()(
26 "ProviderAdapterRegistryLookupError",
27 {
28 instanceId: ProviderInstanceId,
29 },
30) {
31 override get message(): string {
32 return `No orchestration provider adapter is registered for ${this.instanceId}.`;
33 }
34}
35
36export class ProviderAdapterRegistryMetadataError extends Schema.TaggedError<ProviderAdapterRegistryMetadataError>()(
37 "ProviderAdapterRegistryMetadataError",
38 { instanceId: ProviderInstanceId, cause: Schema.Defect() },
39) {}
40
41export const ProviderAdapterRegistryV2Error = Schema.Union([
42 ProviderAdapterRegistryLookupError,
43 ProviderAdapterRegistryMetadataError,
44 ProviderAdapterDriverCreateError,
45]);
46export type ProviderAdapterRegistryV2Error = typeof ProviderAdapterRegistryV2Error.Type;
47
48export interface ProviderAdapterRegistryV2Shape {
49 readonly get: (
50 instanceId: ProviderInstanceId,
51 ) => Effect.Effect<ProviderAdapter.ProviderAdapterV2["Service"], ProviderAdapterRegistryV2Error>;
52 readonly list: () => Effect.Effect<ReadonlyArray<ProviderInstanceId>>;
53 readonly getMetadata?: (instanceId: ProviderInstanceId) => Effect.Effect<
54 {
55 readonly driver: ProviderAdapter.ProviderAdapterV2["Service"]["driver"];
56 readonly continuationKey: string;
57 readonly enabled: boolean;
58 readonly capabilities: OrchestrationV2ProviderCapabilities;
59 },
60 ProviderAdapterRegistryV2Error
61 >;
62}
63
64export class ProviderAdapterRegistryV2 extends Context.Service<
65 ProviderAdapterRegistryV2,
66 ProviderAdapterRegistryV2Shape
67>()("t3/orchestration-v2/ProviderAdapterRegistry/ProviderAdapterRegistryV2") {}
68
69/**
70 * Production facade over the canonical provider-instance registry. Adapter
71 * lookup stays dynamic so instance hot reloads and removals are visible
72 * without maintaining a second settings watcher or instance map.
73 */
74export const layerFromProviderInstanceRegistry: Layer.Layer<
75 ProviderAdapterRegistryV2,
76 never,
77 ProviderInstanceRegistry.ProviderInstanceRegistry
78> = Layer.effect(
79 ProviderAdapterRegistryV2,
80 Effect.gen(function* () {
81 const instances = yield* ProviderInstanceRegistry.ProviderInstanceRegistry;
82 return ProviderAdapterRegistryV2.of({
83 get: (instanceId) =>
84 instances.getInstance(instanceId).pipe(
85 Effect.flatMap((instance) => {
86 if (instance === undefined)
87 return new ProviderAdapterRegistryLookupError({ instanceId });
88 const adapter = instance.orchestrationAdapter;
89 const auth = instance.auth;
90 if (!auth) return Effect.succeed(adapter);
91 return Effect.succeed({
92 ...adapter,
93 openSession: (input) => {
94 const open = Effect.gen(function* () {
95 const binding = auth.credentialBinding;
96 const related = binding
97 ? (yield* instances.listInstances).filter(
98 (instance) =>
99 instance.auth?.credentialBinding?.key === binding.key &&
100 instance.auth.credentialBinding.owner === binding.owner,
101 )
102 : [instance];
103 for (const instance of related) {
104 if (
105 instance.auth?.isChangingCredentials &&
106 (yield* instance.auth.isChangingCredentials)
107 )
108 return yield* new ProviderSetupError({
109 instanceId,
110 operation: "session",
111 detail: "This provider's sign-in is changing. Try again after it finishes.",
112 });
113 }
114 let admitted: Effect.Effect<
115 ProviderAdapter.ProviderAdapterV2SessionRuntime,
116 ProviderAdapter.ProviderAdapterV2Error | ProviderSetupError,
117 Scope.Scope
118 > = adapter.openSession(input);
119 // Shared credential changes must interrupt a peer's startup too.
120 for (const peer of related) {
121 if (peer.auth?.withAccess) admitted = peer.auth.withAccess(admitted);
122 }
123 return yield* admitted;
124 });
125 return open.pipe(
126 Effect.mapError((cause) =>
127 isProviderSetupError(cause)
128 ? new ProviderAdapter.ProviderAdapterOpenSessionError({
129 driver: adapter.driver,
130 providerSessionId: input.providerSessionId,
131 cause,
132 })
133 : cause,
134 ),
135 );
136 },
137 } satisfies ProviderAdapter.ProviderAdapterV2["Service"]);
138 }),
139 ),
140 list: () =>
141 instances.listInstances.pipe(
142 Effect.map((available) => available.map((instance) => instance.instanceId)),
143 ),
144 getMetadata: (instanceId) =>
145 Effect.gen(function* () {
146 const instance = yield* instances.getInstance(instanceId);
147 if (instance === undefined) {
148 return yield* new ProviderAdapterRegistryLookupError({ instanceId });
149 }
150 const capabilities = yield* instance.orchestrationAdapter
151 .getCapabilities()
152 .pipe(
153 Effect.mapError(
154 (cause) => new ProviderAdapterRegistryMetadataError({ instanceId, cause }),
155 ),
156 );
157 return {
158 driver: instance.driverKind,
159 continuationKey: instance.continuationIdentity.continuationKey,
160 enabled: instance.enabled,
161 capabilities,
162 };
163 }),
164 });
165 }),
166);
167
168export const ProviderAdapterRegistryBuildError = Schema.Union([ProviderAdapterDriverCreateError]);
169export type ProviderAdapterRegistryBuildError = typeof ProviderAdapterRegistryBuildError.Type;
170
171function makeRegistry(
172 adapters: ReadonlyArray<ProviderAdapter.ProviderAdapterV2["Service"]>,
173): ProviderAdapterRegistryV2Shape {
174 return {
175 get: (instanceId) =>
176 Effect.gen(function* () {
177 const adapter = adapters.find((candidate) => candidate.instanceId === instanceId);
178 if (!adapter) {
179 return yield* new ProviderAdapterRegistryLookupError({ instanceId });
180 }
181 return adapter;
182 }),
183 list: () => Effect.succeed(adapters.map((adapter) => adapter.instanceId)),
184 };
185}
186
187export function layerFromAdapters(
188 adapters: ReadonlyArray<ProviderAdapter.ProviderAdapterV2["Service"]>,
189): Layer.Layer<ProviderAdapterRegistryV2> {
190 return Layer.succeed(
191 ProviderAdapterRegistryV2,
192 ProviderAdapterRegistryV2.of(makeRegistry(adapters)),
193 );
194}
195
196export function layerFromAdaptersEffect<R, E>(
197 adapters: Effect.Effect<ReadonlyArray<ProviderAdapter.ProviderAdapterV2["Service"]>, E, R>,
198): Layer.Layer<ProviderAdapterRegistryV2, E, R> {
199 return Layer.effect(
200 ProviderAdapterRegistryV2,
201 adapters.pipe(Effect.map((entries) => ProviderAdapterRegistryV2.of(makeRegistry(entries)))),
202 );
203}
204
205export function layerSingle(
206 adapter: ProviderAdapter.ProviderAdapterV2["Service"],
207): Layer.Layer<ProviderAdapterRegistryV2> {
208 return layerFromAdapters([adapter]);
209}
210
211const decodedConfigEnabled = (config: unknown): boolean | undefined => {
212 if (!config || typeof config !== "object" || globalThis.Array.isArray(config)) {
213 return undefined;
214 }
215 const enabled = (config as { readonly enabled?: unknown }).enabled;
216 return typeof enabled === "boolean" ? enabled : undefined;
217};
218
219interface LiveAdapterEntry {
220 readonly adapter: ProviderAdapter.ProviderAdapterV2["Service"];
221 readonly scope: Scope.Closeable;
222 readonly entry: ProviderInstanceConfig;
223}
224
225function makeDriversById<R>(
226 drivers: ReadonlyArray<AnyProviderAdapterDriver<R>>,
227): ReadonlyMap<ProviderDriverKind, AnyProviderAdapterDriver<R>> {
228 return new Map<ProviderDriverKind, AnyProviderAdapterDriver<R>>(
229 drivers.map((driver) => [driver.driverKind, driver]),
230 );
231}
232
233const createAdapterEntryFromConfigEntry = Effect.fn(
234 "ProviderAdapterRegistry.createAdapterEntryFromConfigEntry",
235)(function* <R>(input: {
236 readonly driversById: ReadonlyMap<ProviderDriverKind, AnyProviderAdapterDriver<R>>;
237 readonly parentScope: Scope.Scope;
238 readonly instanceId: ProviderInstanceId;
239 readonly entry: ProviderInstanceConfig;
240}): Effect.fn.Return<LiveAdapterEntry, ProviderAdapterDriverCreateError, R> {
241 const driver = input.driversById.get(input.entry.driver);
242 if (driver === undefined) {
243 return yield* new ProviderAdapterDriverCreateError({
244 driver: input.entry.driver,
245 instanceId: input.instanceId,
246 detail: "Unknown provider driver.",
247 });
248 }
249
250 const decodeConfig = Schema.decodeUnknownEffect(driver.configSchema);
251 const typedConfig = yield* decodeConfig(input.entry.config ?? driver.defaultConfig()).pipe(
252 Effect.mapError(
253 (cause) =>
254 new ProviderAdapterDriverCreateError({
255 driver: input.entry.driver,
256 instanceId: input.instanceId,
257 detail: "Invalid provider instance config.",
258 cause,
259 }),
260 ),
261 );
262
263 const childScope = yield* Scope.make();
264 yield* Scope.addFinalizer(
265 input.parentScope,
266 Scope.close(childScope, Exit.void).pipe(Effect.ignore),
267 );
268
269 const adapter = yield* driver
270 .create({
271 instanceId: input.instanceId,
272 displayName: input.entry.displayName,
273 accentColor: input.entry.accentColor,
274 environment: input.entry.environment ?? [],
275 enabled: input.entry.enabled ?? decodedConfigEnabled(typedConfig) ?? true,
276 config: typedConfig,
277 })
278 .pipe(
279 Effect.provideService(Scope.Scope, childScope),
280 Effect.onError((cause) => Scope.close(childScope, Exit.failCause(cause))),
281 );
282
283 return {
284 adapter,
285 scope: childScope,
286 entry: input.entry,
287 };
288});
289
290const buildAdaptersFromConfigMap = Effect.fn("ProviderAdapterRegistry.buildAdaptersFromConfigMap")(
291 function* <R>(input: {
292 readonly drivers: ReadonlyArray<AnyProviderAdapterDriver<R>>;
293 readonly configMap: ProviderInstanceConfigMap;
294 readonly parentScope: Scope.Scope;
295 }): Effect.fn.Return<
296 ReadonlyMap<ProviderInstanceId, LiveAdapterEntry>,
297 ProviderAdapterRegistryBuildError,
298 R
299 > {
300 const driversById = makeDriversById(input.drivers);
301 const adapters = new Map<ProviderInstanceId, LiveAdapterEntry>();
302
303 for (const [rawInstanceId, entry] of Object.entries(input.configMap)) {
304 const instanceId = ProviderInstanceId.make(rawInstanceId);
305 if (!driversById.has(entry.driver)) {
306 yield* Effect.logWarning("Skipping orchestration-v2 provider adapter with unknown driver", {
307 instanceId,
308 driver: entry.driver,
309 });
310 continue;
311 }
312
313 const adapter = yield* createAdapterEntryFromConfigEntry({
314 driversById,
315 parentScope: input.parentScope,
316 instanceId,
317 entry,
318 });
319 adapters.set(instanceId, adapter);
320 }
321
322 return adapters;
323 },
324);
325
326export function makeRegistryFromConfigMap<R>(input: {
327 readonly drivers: ReadonlyArray<AnyProviderAdapterDriver<R>>;
328 readonly configMap: ProviderInstanceConfigMap;
329}): Effect.Effect<
330 ProviderAdapterRegistryV2Shape,
331 ProviderAdapterRegistryBuildError,
332 R | Scope.Scope
333> {
334 return Effect.gen(function* () {
335 const parentScope = yield* Effect.scope;
336 const entries = yield* buildAdaptersFromConfigMap({ ...input, parentScope });
337 return makeRegistry(Array.from(entries.values()).map((entry) => entry.adapter));
338 });
339}
340
341export function layerFromDrivers<R>(input: {
342 readonly drivers: ReadonlyArray<AnyProviderAdapterDriver<R>>;
343 readonly configMap: ProviderInstanceConfigMap;
344}): Layer.Layer<ProviderAdapterRegistryV2, ProviderAdapterRegistryBuildError, R> {
345 return Layer.effect(
346 ProviderAdapterRegistryV2,
347 makeRegistryFromConfigMap(input).pipe(
348 Effect.map((registry) => ProviderAdapterRegistryV2.of(registry)),
349 ),
350 ) as Layer.Layer<ProviderAdapterRegistryV2, ProviderAdapterRegistryBuildError, R>;
351}
352
353const layerFromProviderAdapter: Layer.Layer<
354 ProviderAdapterRegistryV2,
355 never,
356 ProviderAdapter.ProviderAdapterV2
357> = Layer.effect(
358 ProviderAdapterRegistryV2,
359 Effect.gen(function* () {
360 const adapter = yield* ProviderAdapter.ProviderAdapterV2;
361 return ProviderAdapterRegistryV2.of({
362 get: (instanceId) =>
363 adapter.instanceId === instanceId
364 ? Effect.succeed(adapter)
365 : Effect.fail(new ProviderAdapterRegistryLookupError({ instanceId })),
366 list: () => Effect.succeed([adapter.instanceId]),
367 } satisfies ProviderAdapterRegistryV2Shape);
368 }),
369);