Files
NomadArch/packages/ui/src/stores/instance-config.tsx
Gemini AI babce0e0a9
Some checks failed
Release Binaries / release (push) Has been cancelled
chore: user modifications
2025-12-28 01:03:54 +04:00

161 lines
5.0 KiB
TypeScript

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<Map<string, InstanceData>>(new Map())
const loadPromises = new Map<string, Promise<void>>()
const instanceSubscriptions = new Map<string, () => 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<string>()
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<void> {
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<void> {
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<InstanceData> {
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<InstanceConfigContextValue>()
const contextValue: InstanceConfigContextValue = {
getInstanceConfig,
ensureInstanceConfig,
updateInstanceConfig,
clearInstanceConfig,
}
const InstanceConfigProvider: ParentComponent = (props) => {
onCleanup(() => {
for (const unsubscribe of instanceSubscriptions.values()) {
unsubscribe()
}
instanceSubscriptions.clear()
})
return <InstanceConfigContext.Provider value={contextValue}>{props.children}</InstanceConfigContext.Provider>
}
export {
InstanceConfigProvider,
useInstanceConfig,
ensureInstanceConfig as ensureInstanceConfigLoaded,
getInstanceConfig,
updateInstanceConfig,
clearInstanceConfig,
}