import { createContext, createMemo, createSignal, onCleanup, type Accessor, type ParentComponent, useContext } from "solid-js" import type { InstanceData } from "../../../server/src/api-types" import { storage } from "../lib/storage" import { getLogger } from "../lib/logger" const log = getLogger("api") const DEFAULT_INSTANCE_DATA: InstanceData = { messageHistory: [], agentModelSelections: {}, sessionTasks: {}, sessionSkills: {}, sessionMessages: {}, customAgents: [], } const [instanceDataMap, setInstanceDataMap] = createSignal>(new Map()) const loadPromises = new Map>() const instanceSubscriptions = new Map void>() function cloneInstanceData(data?: InstanceData | null): InstanceData { const source = data ?? DEFAULT_INSTANCE_DATA return { ...source, messageHistory: Array.isArray(source.messageHistory) ? [...source.messageHistory] : [], agentModelSelections: { ...(source.agentModelSelections ?? {}) }, sessionTasks: { ...(source.sessionTasks ?? {}) }, sessionSkills: { ...(source.sessionSkills ?? {}) }, sessionMessages: { ...(source.sessionMessages ?? {}) }, customAgents: Array.isArray(source.customAgents) ? [...source.customAgents] : [], } } // Track instance IDs that we are currently saving - ignore SSE echoes const pendingSaveIds = new Set() function attachSubscription(instanceId: string) { if (instanceSubscriptions.has(instanceId)) return const unsubscribe = storage.onInstanceDataChanged(instanceId, (data) => { // Skip SSE echo from our own save if (pendingSaveIds.has(instanceId)) return setInstanceData(instanceId, data) }) instanceSubscriptions.set(instanceId, unsubscribe) } function detachSubscription(instanceId: string) { const unsubscribe = instanceSubscriptions.get(instanceId) if (!unsubscribe) return unsubscribe() instanceSubscriptions.delete(instanceId) } function setInstanceData(instanceId: string, data: InstanceData) { setInstanceDataMap((prev) => { const next = new Map(prev) next.set(instanceId, cloneInstanceData(data)) return next }) } async function ensureInstanceConfig(instanceId: string): Promise { if (!instanceId) return if (instanceDataMap().has(instanceId)) return if (loadPromises.has(instanceId)) { await loadPromises.get(instanceId) return } const promise = storage .loadInstanceData(instanceId) .then((data) => { setInstanceData(instanceId, data) attachSubscription(instanceId) }) .catch((error) => { log.warn("Failed to load instance data", error) setInstanceData(instanceId, DEFAULT_INSTANCE_DATA) attachSubscription(instanceId) }) .finally(() => { loadPromises.delete(instanceId) }) loadPromises.set(instanceId, promise) await promise } async function updateInstanceConfig(instanceId: string, mutator: (draft: InstanceData) => void): Promise { if (!instanceId) return await ensureInstanceConfig(instanceId) const current = instanceDataMap().get(instanceId) ?? DEFAULT_INSTANCE_DATA const draft = cloneInstanceData(current) mutator(draft) setInstanceData(instanceId, draft) pendingSaveIds.add(instanceId) try { await storage.saveInstanceData(instanceId, draft) } catch (error) { log.warn("Failed to persist instance data", error) } finally { setTimeout(() => pendingSaveIds.delete(instanceId), 1000) } } function getInstanceConfig(instanceId: string): InstanceData { return instanceDataMap().get(instanceId) ?? DEFAULT_INSTANCE_DATA } function useInstanceConfig(instanceId: string): Accessor { const context = useContext(InstanceConfigContext) if (!context) { throw new Error("useInstanceConfig must be used within InstanceConfigProvider") } return createMemo(() => instanceDataMap().get(instanceId) ?? DEFAULT_INSTANCE_DATA) } function clearInstanceConfig(instanceId: string): void { setInstanceDataMap((prev) => { if (!prev.has(instanceId)) return prev const next = new Map(prev) next.delete(instanceId) return next }) detachSubscription(instanceId) } interface InstanceConfigContextValue { getInstanceConfig: typeof getInstanceConfig ensureInstanceConfig: typeof ensureInstanceConfig updateInstanceConfig: typeof updateInstanceConfig clearInstanceConfig: typeof clearInstanceConfig } const InstanceConfigContext = createContext() const contextValue: InstanceConfigContextValue = { getInstanceConfig, ensureInstanceConfig, updateInstanceConfig, clearInstanceConfig, } const InstanceConfigProvider: ParentComponent = (props) => { onCleanup(() => { for (const unsubscribe of instanceSubscriptions.values()) { unsubscribe() } instanceSubscriptions.clear() }) return {props.children} } export { InstanceConfigProvider, useInstanceConfig, ensureInstanceConfig as ensureInstanceConfigLoaded, getInstanceConfig, updateInstanceConfig, clearInstanceConfig, }