refine config provider and full replacement flow

This commit is contained in:
Shantur Rathore
2025-11-19 14:43:47 +00:00
parent 7aa94e7a88
commit 7e95005d8c
11 changed files with 296 additions and 266 deletions

View File

@@ -1,6 +1,5 @@
import type {
AppConfig,
AppConfigUpdateRequest,
BinaryCreateRequest,
BinaryListResponse,
BinaryUpdateRequest,
@@ -115,12 +114,6 @@ export const cliApi = {
body: JSON.stringify(payload),
})
},
patchConfig(payload: AppConfigUpdateRequest): Promise<AppConfig> {
return request<AppConfig>("/api/config/app", {
method: "PATCH",
body: JSON.stringify(payload),
})
},
listBinaries(): Promise<BinaryListResponse> {
return request<BinaryListResponse>("/api/config/binaries")
},

View File

@@ -4,21 +4,58 @@ import { cliEvents } from "./cli-events"
export type ConfigData = AppConfig
function isDeepEqual(a: unknown, b: unknown): boolean {
if (a === b) {
return true
}
if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
try {
return JSON.stringify(a) === JSON.stringify(b)
} catch (error) {
console.warn("Failed to compare config objects", error)
}
}
return false
}
export class ServerStorage {
private configChangeListeners: Set<() => void> = new Set()
private configChangeListeners: Set<(config: ConfigData) => void> = new Set()
private configCache: ConfigData | null = null
private loadPromise: Promise<ConfigData> | null = null
constructor() {
cliEvents.on("config.appChanged", () => this.notifyConfigChanged())
cliEvents.on("config.appChanged", (event) => {
if (event.type !== "config.appChanged") return
this.setConfigCache(event.config)
})
}
async loadConfig(): Promise<ConfigData> {
const config = await cliApi.fetchConfig()
return config
if (this.configCache) {
return this.configCache
}
if (!this.loadPromise) {
this.loadPromise = cliApi
.fetchConfig()
.then((config) => {
this.setConfigCache(config)
return config
})
.finally(() => {
this.loadPromise = null
})
}
return this.loadPromise
}
async saveConfig(config: ConfigData): Promise<void> {
await cliApi.updateConfig(config)
async updateConfig(next: ConfigData): Promise<ConfigData> {
const nextConfig = await cliApi.updateConfig(next)
this.setConfigCache(nextConfig)
return nextConfig
}
async loadInstanceData(instanceId: string): Promise<InstanceData> {
@@ -33,14 +70,26 @@ export class ServerStorage {
await cliApi.deleteInstanceData(instanceId)
}
onConfigChanged(listener: () => void): () => void {
onConfigChanged(listener: (config: ConfigData) => void): () => void {
this.configChangeListeners.add(listener)
if (this.configCache) {
listener(this.configCache)
}
return () => this.configChangeListeners.delete(listener)
}
private notifyConfigChanged() {
private setConfigCache(config: ConfigData) {
if (this.configCache && isDeepEqual(this.configCache, config)) {
this.configCache = config
return
}
this.configCache = config
this.notifyConfigChanged(config)
}
private notifyConfigChanged(config: ConfigData) {
for (const listener of this.configChangeListeners) {
listener()
listener(config)
}
}
}

View File

@@ -1,5 +1,5 @@
import { createContext, createSignal, useContext, onMount, createEffect, type JSX } from "solid-js"
import { storage, type ConfigData } from "./storage"
import { createContext, createEffect, createSignal, onMount, useContext, type JSX } from "solid-js"
import { useConfig } from "../stores/preferences"
interface ThemeContextValue {
isDark: () => boolean
@@ -20,64 +20,30 @@ function applyTheme(dark: boolean) {
export function ThemeProvider(props: { children: JSX.Element }) {
const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)")
const [isDark, setIsDarkSignal] = createSignal(true) //systemPrefersDark.matches)
let themePreference: "system" | "dark" | "light" = "dark"
const { themePreference, setThemePreference } = useConfig()
const [isDark, setIsDarkSignal] = createSignal(false)
applyTheme(true) //systemPrefersDark.matches)
async function loadTheme() {
try {
const config = await storage.loadConfig()
const savedTheme = config.theme
let themeDark: boolean
if (savedTheme === "system") {
themePreference = "system"
themeDark = systemPrefersDark.matches
} else if (savedTheme === "dark") {
themePreference = "dark"
themeDark = true
} else if (savedTheme === "light") {
themePreference = "light"
themeDark = false
} else {
themePreference = "dark"
themeDark = true
}
setIsDarkSignal(themeDark)
applyTheme(themeDark)
} catch (error) {
console.warn("Failed to load theme from config:", error)
themePreference = "dark"
const themeDark = true
setIsDarkSignal(themeDark)
applyTheme(themeDark)
const resolveDarkTheme = () => {
const preference = themePreference()
if (preference === "system") {
return systemPrefersDark.matches
}
return preference === "dark"
}
async function saveTheme(dark: boolean) {
try {
const config = await storage.loadConfig()
const nextPreference = dark ? "dark" : "light"
config.theme = nextPreference
themePreference = nextPreference
await storage.saveConfig(config)
} catch (error) {
console.warn("Failed to save theme to config:", error)
}
const applyResolvedTheme = () => {
const dark = resolveDarkTheme()
setIsDarkSignal(dark)
applyTheme(dark)
}
createEffect(() => {
applyResolvedTheme()
})
onMount(() => {
loadTheme()
const unsubscribe = storage.onConfigChanged(() => {
loadTheme()
})
// Listen for system theme changes
const handleSystemThemeChange = (event: MediaQueryListEvent) => {
if (themePreference === "system") {
if (themePreference() === "system") {
setIsDarkSignal(event.matches)
applyTheme(event.matches)
}
@@ -86,19 +52,12 @@ export function ThemeProvider(props: { children: JSX.Element }) {
systemPrefersDark.addEventListener("change", handleSystemThemeChange)
return () => {
unsubscribe()
systemPrefersDark.removeEventListener("change", handleSystemThemeChange)
}
})
createEffect(() => {
applyTheme(isDark())
})
const setTheme = (dark: boolean) => {
setIsDarkSignal(dark)
applyTheme(dark)
saveTheme(dark)
setThemePreference(dark ? "dark" : "light")
}
const toggleTheme = () => {