Skip to content
Source
Sign inSign up
Open History to see the last commit

presentation.ts

329 lines12.3 KB

1import {
2 resolveEnvironmentMachineKind,
3 type EnvironmentMachineKind,
4 type EnvironmentId,
5 type ServerConfig,
6 type ServerProvider,
7} from "@t3tools/contracts";
8import * as Option from "effect/Option";
9import { AsyncResult, Atom } from "effect/reactivity";
10​
11import { AVAILABLE_CONNECTION_STATE, type SupervisorConnectionState } from "../connection/model.ts";
12import {
13 connectionCatalogDisplayUrl,
14 presentConnectionState,
15 type EnvironmentConnectionPhase,
16 type EnvironmentConnectionPresentation,
17 type EnvironmentPresentation,
18 type EnvironmentServerConfig,
19} from "../connection/presentation.ts";
20import { type EnvironmentCatalogState, enabledEnvironmentIds } from "./connections.ts";
21import { hasRelayRoute } from "../connection/routes.ts";
22​
23function mapsEqual<K, V>(left: ReadonlyMap<K, V>, right: ReadonlyMap<K, V>): boolean {
24 if (left.size !== right.size) {
25 return false;
26 }
27 for (const [key, value] of left) {
28 if (right.get(key) !== value) {
29 return false;
30 }
31 }
32 return true;
34​
35function sameConnection(
36 left: EnvironmentConnectionPresentation,
37 right: EnvironmentConnectionPresentation,
38): boolean {
39 return left.phase === right.phase && left.error === right.error && left.traceId === right.traceId;
41​
42function withoutProviders(
43 config: ServerConfig | null,
44 previous: EnvironmentServerConfig | null,
45): EnvironmentServerConfig | null {
46 if (config === null) {
47 return null;
48 }
49 const { providers: _providers, ...next } = config;
50 if (previous === null) {
51 return next;
52 }
53 const keys = Object.keys(next) as Array<keyof EnvironmentServerConfig>;
54 return keys.length === Object.keys(previous).length &&
55 keys.every((key) => key in previous && previous[key] === next[key])
56 ? previous
57 : next;
59​
60export interface EnvironmentPresentationWithProviders extends Omit<
61 EnvironmentPresentation,
62 "serverConfig"
63> {
64 readonly serverConfig: ServerConfig | null;
66​
67export function createEnvironmentPresentationAtoms<E>(input: {
68 readonly catalogValueAtom: Atom.Atom<EnvironmentCatalogState>;
69 readonly stateAtom: (
70 environmentId: EnvironmentId,
71 ) => Atom.Atom<AsyncResult.AsyncResult<SupervisorConnectionState, E>>;
72 readonly serverConfigValueAtom: (environmentId: EnvironmentId) => Atom.Atom<ServerConfig | null>;
73}) {
74 const presentationAtom = Atom.family((environmentId: EnvironmentId) =>
75 Atom.make((get) => {
76 const entry = get(input.catalogValueAtom).entries.get(environmentId);
77 if (entry === undefined) {
78 return null;
79 }
80 const state = AsyncResult.getOrElse(
81 get(input.stateAtom(environmentId)),
82 () => AVAILABLE_CONNECTION_STATE,
83 );
84 const previous = Option.getOrNull(get.self<EnvironmentPresentation | null>());
85 // The supervisor never publishes this placeholder; an enabled one starts out connecting.
86 const connection: EnvironmentConnectionPresentation =
87 entry.unsupportedReason !== undefined
88 ? { phase: "unsupported", error: entry.unsupportedReason, traceId: null }
89 : state === AVAILABLE_CONNECTION_STATE && entry.enabled
90 ? { phase: "connecting", error: null, traceId: null }
91 : presentConnectionState(state);
92 const next = {
93 entry,
94 connection:
95 previous !== null && sameConnection(previous.connection, connection)
96 ? previous.connection
97 : connection,
98 serverConfig: withoutProviders(
99 get(input.serverConfigValueAtom(environmentId)),
100 previous?.serverConfig ?? null,
101 ),
102 } satisfies EnvironmentPresentation;
103 return previous !== null &&
104 previous.entry === next.entry &&
105 previous.connection === next.connection &&
106 previous.serverConfig === next.serverConfig
107 ? previous
108 : next;
109 }).pipe(Atom.withLabel(`environment-presentation:${environmentId}`)),
110 );
111​
112 let previous: ReadonlyMap<EnvironmentId, EnvironmentPresentation> = new Map();
113 const presentationsAtom = Atom.make((get) => {
114 const next = new Map<EnvironmentId, EnvironmentPresentation>();
115 for (const environmentId of get(input.catalogValueAtom).entries.keys()) {
116 const presentation = get(presentationAtom(environmentId));
117 if (presentation !== null) {
118 next.set(environmentId, presentation);
119 }
120 }
121 if (mapsEqual(previous, next)) {
122 return previous;
123 }
124 previous = next;
125 return previous;
126 }).pipe(Atom.withLabel("environment-presentations"));
127​
128 // Enabled environments only, like the full server-configs map, but without providers, so
129 // capability checks on every sidebar row and markdown link skip provider status pushes.
130 const serverConfigsAtom = Atom.make((get) => {
131 const next = new Map<EnvironmentId, EnvironmentServerConfig>();
132 for (const environmentId of enabledEnvironmentIds(get(input.catalogValueAtom))) {
133 const serverConfig = get(presentationAtom(environmentId))?.serverConfig;
134 if (serverConfig != null) {
135 next.set(environmentId, serverConfig);
136 }
137 }
138 const previous = Option.getOrNull(
139 get.self<ReadonlyMap<EnvironmentId, EnvironmentServerConfig>>(),
140 );
141 return previous !== null && mapsEqual(previous, next) ? previous : next;
142 }).pipe(Atom.withLabel("environment-server-configs-without-providers"));
143​
144 const providersByEnvironmentAtom = Atom.make((get) => {
145 const next = new Map<EnvironmentId, ReadonlyArray<ServerProvider>>();
146 for (const environmentId of get(input.catalogValueAtom).entries.keys()) {
147 const providers = get(input.serverConfigValueAtom(environmentId))?.providers;
148 if (providers !== undefined) {
149 next.set(environmentId, providers);
150 }
151 }
152 const previous = Option.getOrNull(
153 get.self<ReadonlyMap<EnvironmentId, ReadonlyArray<ServerProvider>>>(),
154 );
155 return previous !== null && mapsEqual(previous, next) ? previous : next;
156 }).pipe(Atom.withLabel("environment-providers"));
157​
158 // Enabled environments only, like serverConfigsAtom: a disabled connection keeps its cached
159 // config, and its accounts would otherwise count in usage limits. Usage limits ride on provider
160 // statuses, so these surfaces rebuild on every push by design.
161 const presentationsWithProvidersAtom = Atom.make((get) => {
162 const previous = Option.getOrNull(
163 get.self<ReadonlyMap<EnvironmentId, EnvironmentPresentationWithProviders>>(),
164 );
165 const next = new Map<EnvironmentId, EnvironmentPresentationWithProviders>();
166 for (const environmentId of enabledEnvironmentIds(get(input.catalogValueAtom))) {
167 const presentation = get(presentationAtom(environmentId));
168 if (presentation === null) continue;
169 const serverConfig = get(input.serverConfigValueAtom(environmentId));
170 const prior = previous?.get(environmentId);
171 next.set(
172 environmentId,
173 prior !== undefined &&
174 prior.entry === presentation.entry &&
175 prior.connection === presentation.connection &&
176 prior.serverConfig === serverConfig
177 ? prior
178 : { ...presentation, serverConfig },
179 );
180 }
181 return previous !== null && mapsEqual(previous, next) ? previous : next;
182 }).pipe(Atom.withLabel("environment-presentations-with-providers"));
183​
184 return {
185 presentationAtom,
186 presentationsAtom,
187 serverConfigsAtom,
188 providersByEnvironmentAtom,
189 presentationsWithProvidersAtom,
190 };
192​
193export interface EnvironmentConnectionSummary {
194 readonly environmentId: EnvironmentId;
195 readonly environmentLabel: string;
196 readonly displayUrl: string;
197 readonly isRelayManaged: boolean;
198 readonly isEnabled: boolean;
199 readonly connectionState: EnvironmentConnectionPhase;
200 readonly connectionError: string | null;
201 readonly connectionErrorTraceId: string | null;
203​
204export function projectEnvironmentConnectionSummary(
205 environmentId: EnvironmentId,
206 environment: EnvironmentPresentation,
207): EnvironmentConnectionSummary {
208 return {
209 environmentId,
210 environmentLabel: environment.entry.target.label,
211 displayUrl: connectionCatalogDisplayUrl(environment.entry) ?? "",
212 isRelayManaged: hasRelayRoute(environment.entry),
213 isEnabled: environment.entry.enabled,
214 connectionState: environment.connection.phase,
215 connectionError: environment.connection.error,
216 connectionErrorTraceId: environment.connection.traceId,
217 };
219​
220export function createEnvironmentSummaryAtoms(input: {
221 readonly catalogValueAtom: Atom.Atom<EnvironmentCatalogState>;
222 readonly presentationAtom: (
223 environmentId: EnvironmentId,
224 ) => Atom.Atom<EnvironmentPresentation | null>;
225}) {
226 const environmentIdsAtom = Atom.make((get) => {
227 const next = [...get(input.catalogValueAtom).entries.keys()];
228 const previous = Option.getOrNull(get.self<ReadonlyArray<EnvironmentId>>());
229 return previous !== null &&
230 previous.length === next.length &&
231 next.every((id, index) => id === previous[index])
232 ? previous
233 : next;
234 });
235 const identitiesAtom = Atom.make((get) => {
236 const next = [...get(input.catalogValueAtom).entries].map(([environmentId, entry]) => ({
237 environmentId,
238 label: entry.target.label,
239 }));
240 const previous = Option.getOrNull(
241 get.self<ReadonlyArray<{ readonly environmentId: EnvironmentId; readonly label: string }>>(),
242 );
243 return previous !== null &&
244 previous.length === next.length &&
245 next.every(
246 (value, index) =>
247 value.environmentId === previous[index]?.environmentId &&
248 value.label === previous[index]?.label,
249 )
250 ? previous
251 : next;
252 });
253 const environmentAtom = Atom.family((environmentId: EnvironmentId) =>
254 Atom.make((get) => {
255 const presentation = get(input.presentationAtom(environmentId));
256 if (presentation === null) return null;
257 const next = projectEnvironmentConnectionSummary(environmentId, presentation);
258 const previous = Option.getOrNull(get.self<EnvironmentConnectionSummary | null>());
259 if (
260 previous !== null &&
261 previous.environmentId === next.environmentId &&
262 previous.environmentLabel === next.environmentLabel &&
263 previous.displayUrl === next.displayUrl &&
264 previous.isRelayManaged === next.isRelayManaged &&
265 previous.isEnabled === next.isEnabled &&
266 previous.connectionState === next.connectionState &&
267 previous.connectionError === next.connectionError &&
268 previous.connectionErrorTraceId === next.connectionErrorTraceId
269 )
270 return previous;
271 return next;
272 }),
273 );
274 const environmentsAtom = Atom.make((get) => {
275 const next: Array<EnvironmentConnectionSummary> = [];
276 for (const environmentId of get(input.catalogValueAtom).entries.keys()) {
277 const environment = get(environmentAtom(environmentId));
278 if (environment !== null) next.push(environment);
279 }
280 const previous = Option.getOrNull(get.self<ReadonlyArray<EnvironmentConnectionSummary>>());
281 return previous !== null &&
282 previous.length === next.length &&
283 next.every((value, index) => value === previous[index])
284 ? previous
285 : next;
286 }).pipe(Atom.withLabel("environment-connection-summaries"));
287 const connectedEnvironmentIdsAtom = Atom.make((get) => {
288 const next = get(environmentIdsAtom).filter(
289 (id) => get(environmentAtom(id))?.connectionState === "connected",
290 );
291 const previous = Option.getOrNull(get.self<ReadonlyArray<EnvironmentId>>());
292 return previous !== null &&
293 previous.length === next.length &&
294 next.every((id, index) => id === previous[index])
295 ? previous
296 : next;
297 });
298 const machineByIdAtom = Atom.make((get) => {
299 const next = new Map(
300 get(environmentIdsAtom).map(
301 (environmentId) =>
302 [
303 environmentId,
304 resolveEnvironmentMachineKind(
305 get(input.presentationAtom(environmentId))?.serverConfig ?? null,
306 ),
307 ] as const,
308 ),
309 );
310 const previous = Option.getOrNull(
311 get.self<ReadonlyMap<EnvironmentId, EnvironmentMachineKind>>(),
312 );
313 return previous !== null && mapsEqual(previous, next) ? previous : next;
314 });
315 const pullRequestsSupportedAtom = Atom.make((get) =>
316 get(environmentIdsAtom).some(
317 (environmentId) =>
318 (get(input.presentationAtom(environmentId))?.serverConfig ?? null) !== null,
319 ),
320 );
321 return {
322 environmentIdsAtom,
323 connectedEnvironmentIdsAtom,
324 identitiesAtom,
325 environmentsAtom,
326 machineByIdAtom,
327 pullRequestsSupportedAtom,
328 };