1import {
2 ProviderInstanceId,
3 ProviderSetupError,
4 type OrchestrationV2ProviderCapabilities,
5} from "@t3tools/contracts";
6import * as Context from "effect/Context";
7import * as Effect from "effect/Effect";
8import * as Layer from "effect/Layer";
9import * as Schema from "effect/Schema";
10import * as Scope from "effect/Scope";
11
12import * as ProviderInstanceRegistry from "../provider/ProviderInstanceRegistry.ts";
13import { ProviderAdapterDriverCreateError } from "@t3tools/provider-core/server/adapterDriver";
14import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
15
16const isProviderSetupError = Schema.is(ProviderSetupError);
17
18export class ProviderAdapterRegistryLookupError extends Schema.TaggedError<ProviderAdapterRegistryLookupError>()(
19 "ProviderAdapterRegistryLookupError",
20 {
21 instanceId: ProviderInstanceId,
22 },
23) {
24 override get message(): string {
25 return `No orchestration provider adapter is registered for ${this.instanceId}.`;
26 }
27}
28
29export class ProviderAdapterRegistryMetadataError extends Schema.TaggedError<ProviderAdapterRegistryMetadataError>()(
30 "ProviderAdapterRegistryMetadataError",
31 { instanceId: ProviderInstanceId, cause: Schema.Defect() },
32) {}
33
34export const ProviderAdapterRegistryV2Error = Schema.Union([
35 ProviderAdapterRegistryLookupError,
36 ProviderAdapterRegistryMetadataError,
37 ProviderAdapterDriverCreateError,
38]);
39export type ProviderAdapterRegistryV2Error = typeof ProviderAdapterRegistryV2Error.Type;
40
41export interface ProviderAdapterRegistryV2Shape {
42 readonly get: (
43 instanceId: ProviderInstanceId,
44 ) => Effect.Effect<ProviderAdapter.ProviderAdapterV2["Service"], ProviderAdapterRegistryV2Error>;
45 readonly list: () => Effect.Effect<ReadonlyArray<ProviderInstanceId>>;
46 readonly getMetadata?: (instanceId: ProviderInstanceId) => Effect.Effect<
47 {
48 readonly driver: ProviderAdapter.ProviderAdapterV2["Service"]["driver"];
49 readonly continuationKey: string;
50 readonly enabled: boolean;
51 readonly capabilities: OrchestrationV2ProviderCapabilities;
52 },
53 ProviderAdapterRegistryV2Error
54 >;
55}
56
57export class ProviderAdapterRegistryV2 extends Context.Service<
58 ProviderAdapterRegistryV2,
59 ProviderAdapterRegistryV2Shape
60>()("t3/orchestration-v2/ProviderAdapterRegistry/ProviderAdapterRegistryV2") {}
61
62export const layerFromProviderInstanceRegistry: Layer.Layer<
63 ProviderAdapterRegistryV2,
64 never,
65 ProviderInstanceRegistry.ProviderInstanceRegistry
66> = Layer.effect(
67 ProviderAdapterRegistryV2,
68 Effect.gen(function* () {
69 const instances = yield* ProviderInstanceRegistry.ProviderInstanceRegistry;
70 return ProviderAdapterRegistryV2.of({
71 get: Effect.fn("ProviderAdapterRegistryV2.get")(function (instanceId) {
72 return instances.getInstance(instanceId).pipe(
73 Effect.flatMap((instance) => {
74 if (instance === undefined)
75 return new ProviderAdapterRegistryLookupError({ instanceId });
76 const adapter = instance.orchestrationAdapter;
77 const auth = instance.auth;
78 if (!auth) return Effect.succeed(adapter);
79 return Effect.succeed({
80 ...adapter,
81 openSession: (input) => {
82 const open = Effect.gen(function* () {
83 const binding = auth.credentialBinding;
84 const related = binding
85 ? (yield* instances.listInstances).filter(
86 (instance) =>
87 instance.auth?.credentialBinding?.key === binding.key &&
88 instance.auth.credentialBinding.owner === binding.owner,
89 )
90 : [instance];
91 for (const instance of related) {
92 if (
93 instance.auth?.isChangingCredentials &&
94 (yield* instance.auth.isChangingCredentials)
95 )
96 return yield* new ProviderSetupError({
97 instanceId,
98 operation: "session",
99 detail: "This provider's sign-in is changing. Try again after it finishes.",
100 });
101 }
102 let admitted: Effect.Effect<
103 ProviderAdapter.ProviderAdapterV2SessionRuntime,
104 ProviderAdapter.ProviderAdapterV2Error | ProviderSetupError,
105 Scope.Scope
106 > = adapter.openSession(input);
107 // Shared credential changes must interrupt a peer's startup too.
108 for (const peer of related) {
109 if (peer.auth?.withAccess) admitted = peer.auth.withAccess(admitted);
110 }
111 return yield* admitted;
112 });
113 return open.pipe(
114 Effect.mapError((cause) =>
115 isProviderSetupError(cause)
116 ? new ProviderAdapter.ProviderAdapterOpenSessionError({
117 driver: adapter.driver,
118 providerSessionId: input.providerSessionId,
119 cause,
120 })
121 : cause,
122 ),
123 );
124 },
125 } satisfies ProviderAdapter.ProviderAdapterV2["Service"]);
126 }),
127 );
128 }),
129 list: Effect.fn("ProviderAdapterRegistryV2.list")(function () {
130 return instances.listInstances.pipe(
131 Effect.map((available) => available.map((instance) => instance.instanceId)),
132 );
133 }),
134 getMetadata: Effect.fn("ProviderAdapterRegistry.getMetadata")(function* (instanceId) {
135 const instance = yield* instances.getInstance(instanceId);
136 if (instance === undefined) {
137 return yield* new ProviderAdapterRegistryLookupError({ instanceId });
138 }
139 const capabilities = yield* instance.orchestrationAdapter
140 .getCapabilities()
141 .pipe(
142 Effect.mapError(
143 (cause) => new ProviderAdapterRegistryMetadataError({ instanceId, cause }),
144 ),
145 );
146 return {
147 driver: instance.driverKind,
148 continuationKey: instance.continuationIdentity.continuationKey,
149 enabled: instance.enabled,
150 capabilities,
151 };
152 }),
153 });
154 }),
155);
156
157function makeRegistry(
158 adapters: ReadonlyArray<ProviderAdapter.ProviderAdapterV2["Service"]>,
159): ProviderAdapterRegistryV2Shape {
160 return {
161 get: Effect.fn("ProviderAdapterRegistry.get")(function* (instanceId) {
162 const adapter = adapters.find((candidate) => candidate.instanceId === instanceId);
163 if (!adapter) {
164 return yield* new ProviderAdapterRegistryLookupError({ instanceId });
165 }
166 return adapter;
167 }),
168 list: () => Effect.succeed(adapters.map((adapter) => adapter.instanceId)),
169 };
170}
171
172export function layerFromAdapters(
173 adapters: ReadonlyArray<ProviderAdapter.ProviderAdapterV2["Service"]>,
174): Layer.Layer<ProviderAdapterRegistryV2> {
175 return Layer.succeed(
176 ProviderAdapterRegistryV2,
177 ProviderAdapterRegistryV2.of(makeRegistry(adapters)),
178 );
179}
180
181export function layerFromAdaptersEffect<R, E>(
182 adapters: Effect.Effect<ReadonlyArray<ProviderAdapter.ProviderAdapterV2["Service"]>, E, R>,
183): Layer.Layer<ProviderAdapterRegistryV2, E, Exclude<R, Scope.Scope>> {
184 return Layer.effect(
185 ProviderAdapterRegistryV2,
186 adapters.pipe(Effect.map((entries) => ProviderAdapterRegistryV2.of(makeRegistry(entries)))),
187 );
188}
189
190export function layerSingle(
191 adapter: ProviderAdapter.ProviderAdapterV2["Service"],
192): Layer.Layer<ProviderAdapterRegistryV2> {
193 return layerFromAdapters([adapter]);
194}