161 lines
5.0 KiB
TypeScript
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,
|
|
}
|