mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-28 09:36:47 +00:00
Improve LXC updater selection, error handling and dashboard consistency
This commit is contained in:
@@ -17,6 +17,82 @@ export type LxcAppsBundle = {
|
||||
const dataCache = new Map<number, LxcAppsBundle>()
|
||||
const inFlight = new Map<number, Promise<LxcAppsBundle | null>>()
|
||||
const cacheRevision = new Map<number, number>()
|
||||
type AppWatchState = {
|
||||
id: string
|
||||
state_revision?: number
|
||||
managed_oci_app_id?: string | null
|
||||
checked_at?: string | null
|
||||
[key: string]: any
|
||||
}
|
||||
const observedStates = new Map<number, AppWatchState[]>()
|
||||
const listeners = new Map<number, Set<(bundle: LxcAppsBundle) => void>>()
|
||||
const stateFields = [
|
||||
"installed_version", "latest_version", "latest_published_at",
|
||||
"update_available", "error", "checked_at",
|
||||
] as const
|
||||
|
||||
function withObservedStates(vmid: number, sidecar: any): any {
|
||||
if (!Array.isArray(sidecar?.apps)) return sidecar
|
||||
const observations = new Map((observedStates.get(vmid) || []).map(item => [item.id, item]))
|
||||
let changed = false
|
||||
let revision = sidecar._revision || 0
|
||||
const apps = sidecar.apps.map((app: any) => {
|
||||
const observed = observations.get(app.id)
|
||||
if (!observed || observed.managed_oci_app_id) return app
|
||||
if (observed.state_revision && sidecar._revision) {
|
||||
if (observed.state_revision < sidecar._revision) return app
|
||||
} else if (!observed.checked_at || (app.state?.checked_at && observed.checked_at < app.state.checked_at)) {
|
||||
return app
|
||||
}
|
||||
const state = { ...app.state }
|
||||
let stateChanged = false
|
||||
for (const field of stateFields) {
|
||||
if (field in observed && observed[field] !== state[field]) {
|
||||
state[field] = observed[field]
|
||||
stateChanged = true
|
||||
}
|
||||
}
|
||||
revision = Math.max(revision, observed.state_revision || 0)
|
||||
if (!stateChanged) return app
|
||||
changed = true
|
||||
return { ...app, state }
|
||||
})
|
||||
return changed || revision !== (sidecar._revision || 0)
|
||||
? { ...sidecar, apps, _revision: revision }
|
||||
: sidecar
|
||||
}
|
||||
|
||||
function publish(vmid: number, bundle: LxcAppsBundle): void {
|
||||
dataCache.set(vmid, bundle)
|
||||
listeners.get(vmid)?.forEach(listener => listener(bundle))
|
||||
}
|
||||
|
||||
export function subscribeLxcApps(vmid: number, listener: (bundle: LxcAppsBundle) => void): () => void {
|
||||
const subscribers = listeners.get(vmid) || new Set()
|
||||
subscribers.add(listener)
|
||||
listeners.set(vmid, subscribers)
|
||||
const current = dataCache.get(vmid)
|
||||
if (current) listener(current)
|
||||
return () => {
|
||||
subscribers.delete(listener)
|
||||
if (!subscribers.size) listeners.delete(vmid)
|
||||
}
|
||||
}
|
||||
|
||||
// Reuse the existing VM-list feed; this never starts a detection or an HTTP request.
|
||||
export function syncLxcAppsState(vmid: number, watches: AppWatchState[]): void {
|
||||
const previous = observedStates.get(vmid) || []
|
||||
const previousRevision = Math.max(0, ...previous.map(item => item.state_revision || 0))
|
||||
const nextRevision = Math.max(0, ...watches.map(item => item.state_revision || 0))
|
||||
if (nextRevision && nextRevision < previousRevision) return
|
||||
observedStates.set(vmid, watches)
|
||||
const current = dataCache.get(vmid)
|
||||
if (!current) return
|
||||
const sidecar = withObservedStates(vmid, current.sidecar)
|
||||
if (sidecar === current.sidecar) return
|
||||
cacheRevision.set(vmid, (cacheRevision.get(vmid) || 0) + 1)
|
||||
publish(vmid, { ...current, sidecar })
|
||||
}
|
||||
|
||||
export function getLxcAppsCached(vmid: number): LxcAppsBundle | undefined {
|
||||
return dataCache.get(vmid)
|
||||
@@ -34,12 +110,12 @@ export function setLxcAppsCached(
|
||||
cacheRevision.set(vmid, (cacheRevision.get(vmid) || 0) + 1)
|
||||
const current = dataCache.get(vmid)
|
||||
const bundle: LxcAppsBundle = {
|
||||
sidecar,
|
||||
sidecar: withObservedStates(vmid, sidecar),
|
||||
suggestions: suggestions === undefined
|
||||
? (current?.suggestions ?? null)
|
||||
: suggestions,
|
||||
}
|
||||
dataCache.set(vmid, bundle)
|
||||
publish(vmid, bundle)
|
||||
return bundle
|
||||
}
|
||||
|
||||
@@ -53,13 +129,13 @@ export function fetchLxcApps(vmid: number): Promise<LxcAppsBundle | null> {
|
||||
])
|
||||
.then(([sc, sug]) => {
|
||||
if (!sc) return null
|
||||
const bundle: LxcAppsBundle = { sidecar: sc, suggestions: sug }
|
||||
const bundle: LxcAppsBundle = { sidecar: withObservedStates(vmid, sc), suggestions: sug }
|
||||
// A successful write may have completed while these GETs were in
|
||||
// flight. Never let that older response overwrite the mutation result.
|
||||
if ((cacheRevision.get(vmid) || 0) !== startedRevision) {
|
||||
return dataCache.get(vmid) ?? null
|
||||
}
|
||||
dataCache.set(vmid, bundle)
|
||||
publish(vmid, bundle)
|
||||
return bundle
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -72,6 +148,7 @@ export function fetchLxcApps(vmid: number): Promise<LxcAppsBundle | null> {
|
||||
export function invalidateLxcApps(vmid: number): void {
|
||||
cacheRevision.set(vmid, (cacheRevision.get(vmid) || 0) + 1)
|
||||
dataCache.delete(vmid)
|
||||
observedStates.delete(vmid)
|
||||
}
|
||||
|
||||
// Seed the cache from the bulk modal-cache endpoint. Both registered
|
||||
@@ -85,5 +162,5 @@ export function seedLxcAppsCache(
|
||||
if (!sidecar) return
|
||||
const existing = dataCache.get(vmid)
|
||||
if (existing) return // per-panel fetch already ran, don't overwrite
|
||||
dataCache.set(vmid, { sidecar, suggestions: suggestions ?? null })
|
||||
publish(vmid, { sidecar: withObservedStates(vmid, sidecar), suggestions: suggestions ?? null })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user