Refresh Studio 2 LoRAs from live Comfy object_info on fresh=1.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+57
-21
@@ -44,6 +44,7 @@ type LoraCache = {
|
||||
let cache: LoraCache | null = null
|
||||
let inflight: Promise<LoraCache> | null = null
|
||||
let inflightFresh = false
|
||||
let loadSeq = 0
|
||||
|
||||
function uniqueNames(values: unknown[]) {
|
||||
const seen = new Set<string>()
|
||||
@@ -80,14 +81,44 @@ function namesFromObjectInfoNode(info: unknown) {
|
||||
return uniqueNames(names)
|
||||
}
|
||||
|
||||
async function fetchJson(path: string, via: 'video' | 'image') {
|
||||
async function fetchJson(path: string, via: 'video' | 'image', timeoutMs = 8000) {
|
||||
const res = via === 'image'
|
||||
? await imageComfyFetch(path, { signal: AbortSignal.timeout(8000) })
|
||||
: await comfyFetch(path, { signal: AbortSignal.timeout(8000) })
|
||||
? await imageComfyFetch(path, { signal: AbortSignal.timeout(timeoutMs) })
|
||||
: await comfyFetch(path, { signal: AbortSignal.timeout(timeoutMs) })
|
||||
if (!res.ok) return null
|
||||
return res.json().catch(() => null)
|
||||
}
|
||||
|
||||
function collectObjectInfoLoras(all: Record<string, unknown>) {
|
||||
const image: string[] = []
|
||||
const video: string[] = []
|
||||
let any = false
|
||||
for (const node of OBJECT_INFO_NODES) {
|
||||
const info = all[node]
|
||||
if (!info || typeof info !== 'object') continue
|
||||
const names = namesFromObjectInfoNode(info)
|
||||
if (!names.length) continue
|
||||
any = true
|
||||
if (node === LORA_LOADER) image.push(...names)
|
||||
else video.push(...names)
|
||||
if (node !== LORA_LOADER && node !== LORA_MODEL_ONLY) {
|
||||
image.push(...names)
|
||||
video.push(...names)
|
||||
}
|
||||
}
|
||||
if (any) return { image: uniqueNames(image), video: uniqueNames(video) }
|
||||
|
||||
const imageAll: string[] = []
|
||||
const videoAll: string[] = []
|
||||
for (const [classType, info] of Object.entries(all)) {
|
||||
if (!/lora/i.test(classType)) continue
|
||||
const names = namesFromObjectInfoNode(info)
|
||||
if (classType === LORA_LOADER) imageAll.push(...names)
|
||||
else videoAll.push(...names)
|
||||
}
|
||||
return { image: uniqueNames(imageAll), video: uniqueNames(videoAll) }
|
||||
}
|
||||
|
||||
async function fetchModelsLoras(via: 'video' | 'image') {
|
||||
const payload = await fetchJson('/models/loras', via)
|
||||
if (Array.isArray(payload)) return uniqueNames(payload)
|
||||
@@ -97,7 +128,14 @@ async function fetchModelsLoras(via: 'video' | 'image') {
|
||||
return [] as string[]
|
||||
}
|
||||
|
||||
async function fetchObjectInfoLoras(via: 'video' | 'image') {
|
||||
async function fetchObjectInfoLoras(via: 'video' | 'image', fresh = false) {
|
||||
// Full /object_info is Comfy's model-list refresh (UI Refresh) and lists LoraLoader / model-only / rgthree / WanVideoLoraSelect.
|
||||
if (fresh) {
|
||||
const all = await fetchJson('/object_info', via, 45_000)
|
||||
if (all && typeof all === 'object') return collectObjectInfoLoras(all as Record<string, unknown>)
|
||||
return { image: [] as string[], video: [] as string[] }
|
||||
}
|
||||
|
||||
const image: string[] = []
|
||||
const video: string[] = []
|
||||
let any = false
|
||||
@@ -120,22 +158,14 @@ async function fetchObjectInfoLoras(via: 'video' | 'image') {
|
||||
return { image: uniqueNames(image), video: uniqueNames(video) }
|
||||
}
|
||||
|
||||
const all = await fetchJson('/object_info', via)
|
||||
const all = await fetchJson('/object_info', via, 45_000)
|
||||
if (!all || typeof all !== 'object') return { image: [] as string[], video: [] as string[] }
|
||||
const imageAll: string[] = []
|
||||
const videoAll: string[] = []
|
||||
for (const [classType, info] of Object.entries(all as Record<string, unknown>)) {
|
||||
if (!/lora/i.test(classType)) continue
|
||||
const names = namesFromObjectInfoNode(info)
|
||||
if (classType === LORA_LOADER) imageAll.push(...names)
|
||||
else videoAll.push(...names)
|
||||
}
|
||||
return { image: uniqueNames(imageAll), video: uniqueNames(videoAll) }
|
||||
return collectObjectInfoLoras(all as Record<string, unknown>)
|
||||
}
|
||||
|
||||
async function discoverFromHost(via: 'video' | 'image') {
|
||||
async function discoverFromHost(via: 'video' | 'image', fresh = false) {
|
||||
const fromInfo = await fetchObjectInfoLoras(via, fresh).catch(() => ({ image: [] as string[], video: [] as string[] }))
|
||||
const models = await fetchModelsLoras(via).catch(() => [] as string[])
|
||||
const fromInfo = await fetchObjectInfoLoras(via).catch(() => ({ image: [] as string[], video: [] as string[] }))
|
||||
const image = uniqueNames([...fromInfo.image, ...models])
|
||||
const video = uniqueNames([...fromInfo.video, ...models])
|
||||
if (!image.length && video.length) return { image: video, video }
|
||||
@@ -147,17 +177,19 @@ async function loadLoraCache(options: { fresh?: boolean } = {}): Promise<LoraCac
|
||||
const fresh = options.fresh === true
|
||||
const now = Date.now()
|
||||
if (!fresh && cache && now - cache.at < CACHE_MS) return cache
|
||||
// Fresh must not reuse a non-fresh inflight (stale list / frozen CACHE_MS window).
|
||||
if (inflight && (!fresh || inflightFresh)) return inflight
|
||||
const seq = ++loadSeq
|
||||
inflightFresh = fresh
|
||||
inflight = (async () => {
|
||||
const videoHost = comfyConfigured() ? getComfyHost() : ''
|
||||
const imageHost = getBeastImageHost()
|
||||
const same = Boolean(videoHost && imageHost && sameImageHost(videoHost, imageHost))
|
||||
const video = videoHost
|
||||
? await discoverFromHost('video').catch(() => ({ image: [] as string[], video: [] as string[] }))
|
||||
? await discoverFromHost('video', fresh).catch(() => ({ image: [] as string[], video: [] as string[] }))
|
||||
: { image: [] as string[], video: [] as string[] }
|
||||
const image = imageHost && !same
|
||||
? await discoverFromHost('image').catch(() => ({ image: [] as string[], video: [] as string[] }))
|
||||
? await discoverFromHost('image', fresh).catch(() => ({ image: [] as string[], video: [] as string[] }))
|
||||
: video
|
||||
const next: LoraCache = {
|
||||
at: Date.now(),
|
||||
@@ -166,15 +198,19 @@ async function loadLoraCache(options: { fresh?: boolean } = {}): Promise<LoraCac
|
||||
}
|
||||
// Comfy asleep / unreachable returns empty. Keep the last good list so the picker
|
||||
// does not vanish — but do NOT refresh `at`, or a blip freezes new folder drops out.
|
||||
// Never merge a shorter fresh list back into the old one: deletes must disappear.
|
||||
// Never merge a shorter live list into the old one: deletes must disappear; adds must appear.
|
||||
if (!next.image.length && !next.video.length && cache && (cache.image.length || cache.video.length)) {
|
||||
return cache
|
||||
}
|
||||
// Superseded non-fresh must not overwrite a newer fresh result.
|
||||
if (seq !== loadSeq) return cache || next
|
||||
cache = next
|
||||
return next
|
||||
})().finally(() => {
|
||||
inflight = null
|
||||
inflightFresh = false
|
||||
if (seq === loadSeq) {
|
||||
inflight = null
|
||||
inflightFresh = false
|
||||
}
|
||||
})
|
||||
return inflight
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user