1import type { EnvironmentId as EnvironmentIdType } from "@t3tools/contracts";
2import * as Effect from "effect/Effect";
3import * as Option from "effect/Option";
4import * as Stream from "effect/Stream";
5import * as SubscriptionRef from "effect/SubscriptionRef";
6import { AsyncResult, Atom } from "effect/reactivity";
7
8import * as EnvironmentRegistry from "../connection/registry.ts";
9import type { ConnectionCatalogEntry } from "../connection/catalog.ts";
10import { AVAILABLE_CONNECTION_STATE } from "../connection/model.ts";
11import * as EnvironmentSupervisor from "../connection/supervisor.ts";
12import {
13 GitHubRoutingPermissions,
14 type GitHubRoutingPermission,
15 type StoredGitHubRoutingPermission,
16} from "../connection/githubRoutingPermissions.ts";
17import {
18 createAtomCommandScheduler,
19 createRuntimeCommand,
20 followStreamInEnvironment,
21} from "./runtime.ts";
22
23export interface EnvironmentCatalogState {
24 readonly isReady: boolean;
25 readonly entries: ReadonlyMap<EnvironmentIdType, ConnectionCatalogEntry>;
26}
27
28/**
29 * Environments that take part in the workspace: projects, threads, and shell
30 * summaries only come from these. Disabled environments stay in `entries` so
31 * Settings can list them and switch them back on.
32 */
33export function* enabledEnvironmentIds(
34 catalog: EnvironmentCatalogState,
35): Generator<EnvironmentIdType> {
36 for (const [environmentId, entry] of catalog.entries) {
37 if (entry.enabled) {
38 yield environmentId;
39 }
40 }
41}
42
43const EMPTY_ENVIRONMENT_CATALOG_STATE: EnvironmentCatalogState = Object.freeze({
44 isReady: false,
45 entries: new Map(),
46});
47
48export function createEnvironmentCatalogAtoms<R, E>(
49 runtime: Atom.AtomRuntime<EnvironmentRegistry.EnvironmentRegistry | R, E>,
50) {
51 const commandScheduler = createAtomCommandScheduler();
52 const serial = { mode: "serial" as const, key: () => "environment-catalog" };
53 const catalogAtom = runtime.atom(
54 Stream.unwrap(
55 EnvironmentRegistry.EnvironmentRegistry.pipe(
56 Effect.map((registry) =>
57 SubscriptionRef.changes(registry.entries).pipe(
58 Stream.map((entries) => ({
59 isReady: true,
60 entries,
61 })),
62 ),
63 ),
64 ),
65 ),
66 { initialValue: EMPTY_ENVIRONMENT_CATALOG_STATE },
67 );
68
69 const catalogValueAtom = Atom.make((get) =>
70 Option.getOrElse(AsyncResult.value(get(catalogAtom)), () => EMPTY_ENVIRONMENT_CATALOG_STATE),
71 ).pipe(Atom.withLabel("environment-catalog-value"));
72
73 const githubRoutingPermissionsAtom = runtime.atom(
74 Stream.unwrap(GitHubRoutingPermissions.pipe(Effect.map((permissions) => permissions.changes))),
75 { initialValue: [] as ReadonlyArray<StoredGitHubRoutingPermission> },
76 );
77 const githubRoutingPermissionsValueAtom = Atom.make((get) =>
78 Option.getOrElse(AsyncResult.value(get(githubRoutingPermissionsAtom)), () => []),
79 ).pipe(Atom.withLabel("environment-github-routing-permissions"));
80 const setGitHubRoutingPermission = createRuntimeCommand(runtime, {
81 label: "environment-catalog:github-routing-permission",
82 scheduler: commandScheduler,
83 concurrency: serial,
84 execute: Effect.fn(function* (input: {
85 readonly environmentId: EnvironmentIdType;
86 readonly permission: GitHubRoutingPermission;
87 }) {
88 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
89 const entry = (yield* SubscriptionRef.get(registry.entries)).get(input.environmentId);
90 if (entry === undefined)
91 return yield* new EnvironmentRegistry.EnvironmentNotRegisteredError({
92 environmentId: input.environmentId,
93 });
94 const permissions = yield* GitHubRoutingPermissions;
95 yield* permissions.set(entry, input.permission);
96 }),
97 });
98
99 const networkStatusAtom = runtime.atom(
100 Stream.unwrap(
101 EnvironmentRegistry.EnvironmentRegistry.pipe(
102 Effect.map((registry) => SubscriptionRef.changes(registry.networkStatus)),
103 ),
104 ),
105 { initialValue: "unknown" as const },
106 );
107
108 const networkStatusValueAtom = Atom.make((get) =>
109 Option.getOrElse(AsyncResult.value(get(networkStatusAtom)), () => "unknown" as const),
110 ).pipe(Atom.withLabel("environment-network-status-value"));
111
112 const stateAtom = Atom.family((environmentId: EnvironmentIdType) =>
113 runtime.atom(
114 followStreamInEnvironment(
115 environmentId,
116 Stream.unwrap(
117 EnvironmentSupervisor.EnvironmentSupervisor.pipe(
118 Effect.map((supervisor) => SubscriptionRef.changes(supervisor.state)),
119 ),
120 ),
121 ),
122 { initialValue: AVAILABLE_CONNECTION_STATE },
123 ),
124 );
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 githubRoutingPermissionsValueAtom,
199 setGitHubRoutingPermission,
200 networkStatusAtom,
201 networkStatusValueAtom,
202 stateAtom,
203 register,
204 remove,
205 removeRoute,
206 reorderRoutes,
207 removeRelayEnvironments,
208 retryNow,
209 setEnabled,
210 };
211}