1import {
2 DEFAULT_SERVER_SETTINGS,
3 type EnvironmentId,
4 type EnvironmentTheme,
5 type ServerConfig,
6 type ServerConfigStreamEvent,
7 type ServerLifecycleLegacyThreadMigrationPayload,
8 type ServerLifecycleWelcomePayload,
9 type ServerProvider,
10 type ServerSettings,
11} from "@t3tools/contracts";
12import {
13 AVAILABLE_CONNECTION_STATE,
14 presentConnectionState,
15 type EnvironmentPresentation,
16} from "@t3tools/client-runtime/connection";
17import { createServerEnvironmentAtoms } from "@t3tools/client-runtime/state/server";
18import { createOutdatedServerUpdateCommand } from "@t3tools/client-runtime/state/outdatedServerUpdate";
19import { createEnvironmentServerConfigsAtom } from "@t3tools/client-runtime/state/shell";
20import { mergeWithDefaultKeybindings } from "@t3tools/shared/keybindings";
21import * as Option from "effect/Option";
22import { AsyncResult, Atom } from "effect/reactivity";
23
24import { isConnectedWithConfig } from "../components/settings/scopedSettings";
25import { environmentCatalog } from "../connection/catalog";
26import { connectionAtomRuntime } from "../connection/runtime";
27import { environmentAtomList } from "./environmentAtomList";
28import { primaryEnvironmentIdAtom, themeEnvironmentIdAtom } from "./primaryEnvironment";
29import { environmentSession } from "./session";
30import { reuseProvidersIgnoringCheckedAt } from "./serverProviders.logic";
31
32// Subscribe to every environment's themes; primary can change without reconnecting or updating the subscription.
33export const serverEnvironment = createServerEnvironmentAtoms(connectionAtomRuntime, {
34 initialConfigValueAtom: environmentSession.initialConfigValueAtom,
35 environmentThemes: true,
36 usageLimitSources: true,
37 usageLimitsCommand: true,
38});
39export const environmentUpdateStatesAtom = environmentAtomList(
40 "web-environment-update-states",
41 serverEnvironment.updateStateAtom,
42);
43export const updateOutdatedServer = createOutdatedServerUpdateCommand(connectionAtomRuntime);
44export const environmentServerConfigsAtom = createEnvironmentServerConfigsAtom({
45 catalogValueAtom: environmentCatalog.catalogValueAtom,
46 serverConfigValueAtom: serverEnvironment.configValueAtom,
47});
48
49interface PrimaryServerState {
50 readonly config: ServerConfig | null;
51 readonly latestEvent: ServerConfigStreamEvent | null;
52 readonly welcome: ServerLifecycleWelcomePayload | null;
53}
54
55export const EMPTY_SERVER_PROVIDERS: ReadonlyArray<ServerProvider> = [];
56const EMPTY_PRIMARY_SERVER_STATE: PrimaryServerState = {
57 config: null,
58 latestEvent: null,
59 welcome: null,
60};
61
62const primaryServerStateAtom = Atom.make((get): PrimaryServerState => {
63 const environmentId = get(primaryEnvironmentIdAtom);
64 if (environmentId === null) {
65 return EMPTY_PRIMARY_SERVER_STATE;
66 }
67
68 const target = { environmentId, input: {} };
69 const configProjection = AsyncResult.getOrElse(
70 get(serverEnvironment.configProjection(target)),
71 () => null,
72 );
73 const welcome = AsyncResult.getOrElse(get(serverEnvironment.welcome(target)), () => null);
74
75 return {
76 config: get(serverEnvironment.configValueAtom(environmentId)),
77 latestEvent: configProjection?.latestEvent ?? null,
78 welcome,
79 };
80}).pipe(Atom.withLabel("web-primary-server-state"));
81
82export const primaryServerConfigAtom = Atom.make(
83 (get): ServerConfig | null => get(primaryServerStateAtom).config,
84).pipe(Atom.withLabel("web-primary-server-config"));
85
86export const primaryServerConfigEventAtom = Atom.make(
87 (get): ServerConfigStreamEvent | null => get(primaryServerStateAtom).latestEvent,
88).pipe(Atom.withLabel("web-primary-server-config-event"));
89
90export const primaryServerWelcomeAtom = Atom.make(
91 (get): ServerLifecycleWelcomePayload | null => get(primaryServerStateAtom).welcome,
92).pipe(Atom.withLabel("web-primary-server-welcome"));
93
94export const primaryServerLegacyThreadMigrationAtom = Atom.make(
95 (get): ServerLifecycleLegacyThreadMigrationPayload | null => {
96 const environmentId = get(primaryEnvironmentIdAtom);
97 if (environmentId === null) {
98 return null;
99 }
100 return AsyncResult.getOrElse(
101 get(serverEnvironment.legacyThreadMigration({ environmentId, input: {} })),
102 () => null,
103 );
104 },
105).pipe(Atom.withLabel("web-primary-server-legacy-thread-migration"));
106
107export const primaryServerSettingsAtom = Atom.make(
108 (get): ServerSettings => get(primaryServerConfigAtom)?.settings ?? DEFAULT_SERVER_SETTINGS,
109).pipe(Atom.withLabel("web-primary-server-settings"));
110
111export const primaryServerProvidersAtom = Atom.make(
112 (get): ReadonlyArray<ServerProvider> =>
113 get(primaryServerConfigAtom)?.providers ?? EMPTY_SERVER_PROVIDERS,
114).pipe(Atom.withLabel("web-primary-server-providers"));
115
116/** Without a primary, global shortcuts follow unscoped Settings. */
117export function createShortcutKeybindingsAtom(input: {
118 readonly primaryEnvironmentIdAtom: Atom.Atom<EnvironmentId | null>;
119 readonly primaryConfigAtom: Atom.Atom<ServerConfig | null>;
120 readonly environmentsAtom: Atom.Atom<
121 ReadonlyMap<EnvironmentId, Pick<EnvironmentPresentation, "connection" | "serverConfig">>
122 >;
123}) {
124 // Selected before merging, so unrelated config updates keep the merged array.
125 const customAtom = Atom.make((get) => {
126 if (get(input.primaryEnvironmentIdAtom) !== null) {
127 // A primary stays authoritative while its config loads.
128 return get(input.primaryConfigAtom)?.keybindings ?? null;
129 }
130 const environments = [...get(input.environmentsAtom).values()];
131 return environments.find(isConnectedWithConfig)?.serverConfig?.keybindings ?? null;
132 });
133 return Atom.make((get): ServerConfig["keybindings"] =>
134 mergeWithDefaultKeybindings(get(customAtom) ?? []),
135 );
136}
137
138// Settings' presentations import this module; mirror their catalog order and
139// connection presentation here to avoid a cycle.
140const environmentConnectionsAtom = Atom.make((get) => {
141 const environments = new Map<
142 EnvironmentId,
143 Pick<EnvironmentPresentation, "connection" | "serverConfig">
144 >();
145 for (const [environmentId, entry] of get(environmentCatalog.catalogValueAtom).entries) {
146 const state = AsyncResult.getOrElse(
147 get(environmentCatalog.stateAtom(environmentId)),
148 () => AVAILABLE_CONNECTION_STATE,
149 );
150 environments.set(environmentId, {
151 connection:
152 entry.unsupportedReason === undefined
153 ? presentConnectionState(state)
154 : { phase: "unsupported", error: entry.unsupportedReason, traceId: null },
155 serverConfig: get(serverEnvironment.configValueAtom(environmentId)),
156 });
157 }
158 return environments;
159});
160
161export const primaryServerKeybindingsAtom = createShortcutKeybindingsAtom({
162 primaryEnvironmentIdAtom,
163 primaryConfigAtom: primaryServerConfigAtom,
164 environmentsAtom: environmentConnectionsAtom,
165}).pipe(Atom.withLabel("web-primary-server-keybindings"));
166
167const EMPTY_ENVIRONMENT_THEMES: ReadonlyArray<EnvironmentTheme> = [];
168
169const themeServerConfigAtom = Atom.make((get): ServerConfig | null => {
170 const environmentId = get(themeEnvironmentIdAtom);
171 return environmentId === null ? null : get(serverEnvironment.configValueAtom(environmentId));
172}).pipe(Atom.withLabel("web-theme-server-config"));
173
174export const themeServerEnvironmentThemesAtom = Atom.make(
175 (get): ReadonlyArray<EnvironmentTheme> =>
176 get(themeServerConfigAtom)?.environmentThemes ?? EMPTY_ENVIRONMENT_THEMES,
177).pipe(Atom.withLabel("web-theme-server-environment-themes"));
178
179export const themeServerSettingsAtom = Atom.make(
180 (get): ServerSettings => get(themeServerConfigAtom)?.settings ?? DEFAULT_SERVER_SETTINGS,
181).pipe(Atom.withLabel("web-theme-server-settings"));
182
183export const providersIgnoringCheckedAtAtom = Atom.family((environmentId: EnvironmentId) =>
184 Atom.make((get): ReadonlyArray<ServerProvider> =>
185 reuseProvidersIgnoringCheckedAt(
186 Option.getOrNull(get.self<ReadonlyArray<ServerProvider>>()),
187 get(serverEnvironment.providersValueAtom(environmentId)) ?? EMPTY_SERVER_PROVIDERS,
188 ),
189 ).pipe(Atom.withLabel(`web-providers-ignoring-checked-at:${environmentId}`)),
190);