| 1 | import { |
| 2 | ProviderInstanceId, |
| 3 | ProviderSetupError, |
| 4 | type OrchestrationV2ProviderCapabilities, |
| 5 | type ProviderDriverKind, |
| 6 | type ProviderInstanceConfig, |
| 7 | type ProviderInstanceConfigMap, |
| 8 | } from "@t3tools/contracts"; |
| 9 | import * as Context from "effect/Context"; |
| 10 | import * as Effect from "effect/Effect"; |
| 11 | import * as Exit from "effect/Exit"; |
| 12 | import * as Layer from "effect/Layer"; |
| 13 | import * as Schema from "effect/Schema"; |
| 14 | import * as Scope from "effect/Scope"; |
| 15 | |
| 16 | import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts"; |
| 17 | import { |
| 18 | ProviderAdapterDriverCreateError, |
| 19 | type AnyProviderAdapterDriver, |
| 20 | } from "@t3tools/provider-core/server/adapterDriver"; |
| 21 | import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter"; |
| 22 | |
| 23 | const isProviderSetupError = Schema.is(ProviderSetupError); |
| 24 | |
| 25 | export 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 | |
| 36 | export class ProviderAdapterRegistryMetadataError extends Schema.TaggedError<ProviderAdapterRegistryMetadataError>()( |
| 37 | "ProviderAdapterRegistryMetadataError", |
| 38 | { instanceId: ProviderInstanceId, cause: Schema.Defect() }, |
| 39 | ) {} |
| 40 | |
| 41 | export const ProviderAdapterRegistryV2Error = Schema.Union([ |
| 42 | ProviderAdapterRegistryLookupError, |
| 43 | ProviderAdapterRegistryMetadataError, |
| 44 | ProviderAdapterDriverCreateError, |
| 45 | ]); |
| 46 | export type ProviderAdapterRegistryV2Error = typeof ProviderAdapterRegistryV2Error.Type; |
| 47 | |
| 48 | export 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 | |
| 64 | export 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 | */ |
| 74 | export 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 | |
| 168 | export const ProviderAdapterRegistryBuildError = Schema.Union([ProviderAdapterDriverCreateError]); |
| 169 | export type ProviderAdapterRegistryBuildError = typeof ProviderAdapterRegistryBuildError.Type; |
| 170 | |
| 171 | function 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 | |
| 187 | export 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 | |
| 196 | export 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 | |
| 205 | export function layerSingle( |
| 206 | adapter: ProviderAdapter.ProviderAdapterV2["Service"], |
| 207 | ): Layer.Layer<ProviderAdapterRegistryV2> { |
| 208 | return layerFromAdapters([adapter]); |
| 209 | } |
| 210 | |
| 211 | const 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 | |
| 219 | interface LiveAdapterEntry { |
| 220 | readonly adapter: ProviderAdapter.ProviderAdapterV2["Service"]; |
| 221 | readonly scope: Scope.Closeable; |
| 222 | readonly entry: ProviderInstanceConfig; |
| 223 | } |
| 224 | |
| 225 | function 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 | |
| 233 | const 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 | |
| 290 | const 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 | |
| 326 | export 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 | |
| 341 | export 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 | |
| 353 | const 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 | ); |