1import type { ServerConfig, ServerConfigStreamEvent } from "@t3tools/contracts";
2import * as Option from "effect/Option";
3
4export interface ServerConfigProjection {
5 readonly config: ServerConfig;
6 readonly latestEvent: ServerConfigStreamEvent;
7 readonly source: "cache" | "live";
8}
9
10/**
11 * Cached config keeps the provider and model catalog available across reconnects.
12 * Published themes and usage-limit sources are current machine state, so a
13 * cache could restore a set the machine no longer reports. Replay sends both
14 * as separate events.
15 */
16export function withoutEnvironmentThemes(config: ServerConfig): ServerConfig {
17 if (config.environmentThemes === undefined && config.usageLimitSources === undefined) {
18 return config;
19 }
20 const { environmentThemes: _themes, usageLimitSources: _sources, ...rest } = config;
21 return rest;
22}
23
24export function applyServerConfigProjection(
25 current: Option.Option<ServerConfigProjection>,
26 event: ServerConfigStreamEvent,
27): Option.Option<ServerConfigProjection> {
28 switch (event.type) {
29 case "snapshot": {
30 // Wire snapshots never contain published themes. Keep the previous set
31 // until a capable server sends its authoritative theme event. A legacy
32 // server cannot send a later removal, so a downgrade must clear the set.
33 const capabilities = event.config.environment.capabilities;
34 const carriedThemes =
35 capabilities.environmentThemes === true && Option.isSome(current)
36 ? current.value.config.environmentThemes
37 : undefined;
38 const carriedSources =
39 capabilities.usageLimitSources === true && Option.isSome(current)
40 ? current.value.config.usageLimitSources
41 : undefined;
42 return Option.some({
43 config: {
44 ...event.config,
45 ...(carriedThemes === undefined ? {} : { environmentThemes: carriedThemes }),
46 ...(carriedSources === undefined ? {} : { usageLimitSources: carriedSources }),
47 },
48 latestEvent: event,
49 source: "live" as const,
50 });
51 }
52 case "keybindingsUpdated":
53 return Option.map(current, (projection) => ({
54 config: {
55 ...projection.config,
56 keybindings: event.payload.keybindings,
57 issues: event.payload.issues,
58 },
59 latestEvent: event,
60 source: "live",
61 }));
62 case "providerStatuses":
63 return Option.map(current, (projection) => ({
64 config: {
65 ...projection.config,
66 providers: event.payload.providers,
67 },
68 latestEvent: event,
69 source: "live",
70 }));
71 case "settingsUpdated":
72 return Option.map(current, (projection) => ({
73 config: {
74 ...projection.config,
75 settings: event.payload.settings,
76 },
77 latestEvent: event,
78 source: "live",
79 }));
80 case "environmentThemesUpdated":
81 return Option.map(current, (projection) => ({
82 config: {
83 ...projection.config,
84 environmentThemes: event.payload.themes.length > 0 ? event.payload.themes : undefined,
85 },
86 latestEvent: event,
87 source: "live",
88 }));
89 case "usageLimitSourcesUpdated":
90 return Option.map(current, (projection) => ({
91 config: {
92 ...projection.config,
93 usageLimitSources: event.payload.sources.length > 0 ? event.payload.sources : undefined,
94 },
95 latestEvent: event,
96 source: "live",
97 }));
98 }
99}