1import type { EnvironmentId as EnvironmentIdType } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import * as SubscriptionRef from "effect/SubscriptionRef";
5import { AsyncResult, Atom } from "effect/reactivity";
6
7import * as EnvironmentRegistry from "../connection/registry.ts";
8import type { ConnectionCatalogEntry } from "../connection/catalog.ts";
9import { AVAILABLE_CONNECTION_STATE } from "../connection/model.ts";
10import * as EnvironmentSupervisor from "../connection/supervisor.ts";
11import {
12 PullRequestRoutingPermissions,
13 type PullRequestRoutingPermission,
14 type StoredPullRequestRoutingPermission,
15} from "../connection/pullRequestRoutingPermissions.ts";
16import {
17 createAtomCommandScheduler,
18 createRuntimeCommand,
19 environmentSessionGenerationAtom,
20 followStreamInEnvironment,
21} from "./runtime.ts";
22
23export interface EnvironmentCatalogState {
24 readonly isReady: boolean;
25 readonly entries: ReadonlyMap<EnvironmentIdType, ConnectionCatalogEntry>;
26}
27
28export function* enabledEnvironmentIds(
29 catalog: EnvironmentCatalogState,
30): Generator<EnvironmentIdType> {
31 for (const [environmentId, entry] of catalog.entries) {
32 if (entry.enabled && entry.unsupportedReason === undefined) {
33 yield environmentId;
34 }
35 }
36}
37
38const EMPTY_ENVIRONMENT_CATALOG_STATE: EnvironmentCatalogState = Object.freeze({
39 isReady: false,
40 entries: new Map(),
41});
42
43export function createEnvironmentCatalogAtoms<R, E>(
44 runtime: Atom.AtomRuntime<EnvironmentRegistry.EnvironmentRegistry | R, E>,
45) {
46 const commandScheduler = createAtomCommandScheduler();
47 const serial = { mode: "serial" as const, key: () => "environment-catalog" };
48 const catalogAtom = runtime.atom(
49 Stream.unwrap(
50 EnvironmentRegistry.EnvironmentRegistry.pipe(
51 Effect.map((registry) =>
52 SubscriptionRef.changes(registry.entries).pipe(
53 Stream.map((entries) => ({
54 isReady: true,
55 entries,
56 })),
57 ),
58 ),
59 ),
60 ),
61 { initialValue: EMPTY_ENVIRONMENT_CATALOG_STATE },
62 );
63
64 const catalogValueAtom = Atom.make((get) =>
65 AsyncResult.getOrElse(get(catalogAtom), () => EMPTY_ENVIRONMENT_CATALOG_STATE),
66 ).pipe(Atom.withLabel("environment-catalog-value"));
67
68 const pullRequestRoutingPermissionsAtom = runtime.atom(
69 Stream.unwrap(
70 PullRequestRoutingPermissions.pipe(Effect.map((permissions) => permissions.changes)),
71 ),
72 { initialValue: [] as ReadonlyArray<StoredPullRequestRoutingPermission> },
73 );
74 const pullRequestRoutingPermissionsValueAtom = Atom.make((get) =>
75 AsyncResult.getOrElse(get(pullRequestRoutingPermissionsAtom), () => []),
76 ).pipe(Atom.withLabel("environment-pull-request-routing-permissions"));
77 const setPullRequestRoutingPermission = createRuntimeCommand(runtime, {
78 label: "environment-catalog:pull-request-routing-permission",
79 scheduler: commandScheduler,
80 concurrency: serial,
81 execute: Effect.fn(function* (input: {
82 readonly environmentId: EnvironmentIdType;
83 readonly permission: PullRequestRoutingPermission;
84 }) {
85 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
86 const entry = (yield* SubscriptionRef.get(registry.entries)).get(input.environmentId);
87 if (entry === undefined)
88 return yield* new EnvironmentRegistry.EnvironmentNotRegisteredError({
89 environmentId: input.environmentId,
90 });
91 const permissions = yield* PullRequestRoutingPermissions;
92 yield* permissions.set(entry, input.permission);
93 }),
94 });
95
96 const networkStatusAtom = runtime.atom(
97 Stream.unwrap(
98 EnvironmentRegistry.EnvironmentRegistry.pipe(
99 Effect.map((registry) => SubscriptionRef.changes(registry.networkStatus)),
100 ),
101 ),
102 { initialValue: "unknown" as const },
103 );
104
105 const networkStatusValueAtom = Atom.make((get) =>
106 AsyncResult.getOrElse(get(networkStatusAtom), () => "unknown" as const),
107 ).pipe(Atom.withLabel("environment-network-status-value"));
108
109 const stateAtom = Atom.family((environmentId: EnvironmentIdType) =>
110 runtime.atom(
111 followStreamInEnvironment(
112 environmentId,
113 Stream.unwrap(
114 EnvironmentSupervisor.EnvironmentSupervisor.pipe(
115 Effect.map((supervisor) => SubscriptionRef.changes(supervisor.state)),
116 ),
117 ),
118 ),
119 { initialValue: AVAILABLE_CONNECTION_STATE },
120 ),
121 );
122
123 const sessionGenerationAtom = (environmentId: EnvironmentIdType) =>
124 environmentSessionGenerationAtom(runtime, environmentId);
125
126 const register = createRuntimeCommand(runtime, {
127 label: "environment-catalog:register",
128 scheduler: commandScheduler,
129 concurrency: serial,
130 execute: (
131 target: Parameters<EnvironmentRegistry.EnvironmentRegistry["Service"]["register"]>[0],
132 ) =>
133 EnvironmentRegistry.EnvironmentRegistry.pipe(
134 Effect.flatMap((registry) => registry.register(target)),
135 ),
136 });
137 const remove = createRuntimeCommand(runtime, {
138 label: "environment-catalog:remove",
139 scheduler: commandScheduler,
140 concurrency: serial,
141 execute: (environmentId: EnvironmentIdType) =>
142 EnvironmentRegistry.EnvironmentRegistry.pipe(
143 Effect.flatMap((registry) => registry.remove(environmentId)),
144 ),
145 });
146 const removeRelayEnvironments = createRuntimeCommand(runtime, {
147 label: "environment-catalog:remove-relay-environments",
148 scheduler: commandScheduler,
149 concurrency: serial,
150 execute: (_input: void) =>
151 EnvironmentRegistry.EnvironmentRegistry.pipe(
152 Effect.flatMap((registry) => registry.removeRelayEnvironments()),
153 ),
154 });
155 const setEnabled = createRuntimeCommand(runtime, {
156 label: "environment-catalog:set-enabled",
157 scheduler: commandScheduler,
158 concurrency: serial,
159 execute: (input: { readonly environmentId: EnvironmentIdType; readonly enabled: boolean }) =>
160 EnvironmentRegistry.EnvironmentRegistry.pipe(
161 Effect.flatMap((registry) => registry.setEnabled(input.environmentId, input.enabled)),
162 ),
163 });
164 const removeRoute = createRuntimeCommand(runtime, {
165 label: "environment-catalog:remove-route",
166 scheduler: commandScheduler,
167 concurrency: serial,
168 execute: (input: { readonly environmentId: EnvironmentIdType; readonly routeId: string }) =>
169 EnvironmentRegistry.EnvironmentRegistry.pipe(
170 Effect.flatMap((registry) => registry.removeRoute(input.environmentId, input.routeId)),
171 ),
172 });
173 const reorderRoutes = createRuntimeCommand(runtime, {
174 label: "environment-catalog:reorder-routes",
175 scheduler: commandScheduler,
176 concurrency: serial,
177 execute: (input: {
178 readonly environmentId: EnvironmentIdType;
179 readonly routeIds: ReadonlyArray<string>;
180 }) =>
181 EnvironmentRegistry.EnvironmentRegistry.pipe(
182 Effect.flatMap((registry) => registry.reorderRoutes(input.environmentId, input.routeIds)),
183 ),
184 });
185 const retryNow = createRuntimeCommand(runtime, {
186 label: "environment-catalog:retry-now",
187 scheduler: commandScheduler,
188 concurrency: serial,
189 execute: (environmentId: EnvironmentIdType) =>
190 EnvironmentRegistry.EnvironmentRegistry.pipe(
191 Effect.flatMap((registry) => registry.retryNow(environmentId)),
192 ),
193 });
194
195 return {
196 catalogAtom,
197 catalogValueAtom,
198 pullRequestRoutingPermissionsValueAtom,
199 setPullRequestRoutingPermission,
200 networkStatusAtom,
201 networkStatusValueAtom,
202 stateAtom,
203 sessionGenerationAtom,
204 register,
205 remove,
206 removeRoute,
207 reorderRoutes,
208 removeRelayEnvironments,
209 retryNow,
210 setEnabled,
211 };
212}