Create standalone browser port of LTX Athena with authenticated native API transport
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { subscribeWhileGenerationMayBeActive } from '../lib/generation-progress-poll'
|
||||
import { GENERATION_RECOVERY_KEY, readRecoveryMarkerCanCancel } from './use-generation'
|
||||
|
||||
export interface GlobalGenerationLock {
|
||||
// Fail-closed Generate disable: a recovery marker, a running poll, or an unconfirmed
|
||||
// poll error all mean the single global slot must be treated as busy.
|
||||
isRunning: boolean
|
||||
// Same poll as isRunning, gated on the backend's frozen local-GPU vs API slot.
|
||||
// Never derived from live Settings — switching to local mid-API-job must not reveal Stop.
|
||||
canCancel: boolean
|
||||
// Slot still busy after Stop (`status=running`, `phase=cancelled`) until generate() unwinds.
|
||||
isCancelling: boolean
|
||||
}
|
||||
|
||||
// Only one generation can run at a time across the whole app (single global backend slot), but
|
||||
// each project's GenSpace only tracks its OWN local isGenerating-style state — it has no idea a
|
||||
// different project (or the same one, reconnected via a stale click) is already occupying that
|
||||
// slot. Without this, Generate stays clickable in project B while project A is mid-generation;
|
||||
// the request 409s, but only after writeRecoveryContext already overwrote A's in-flight recovery
|
||||
// marker with B's (now-failed) one. Polling here lets Generate disable proactively instead.
|
||||
// Stop uses the same sources: the recovery marker (immediate, frozen at job start) and
|
||||
// GET /generation/progress.cancellable (authoritative once a poll lands). Hook-local canCancel
|
||||
// dies on UI refresh; these do not. Live Settings must not flip Stop mid-job.
|
||||
// No marker anywhere means nothing CAN be running (see subscribeWhileGenerationMayBeActive), so
|
||||
// idle starts unlocked and costs no network call; once a marker exists and we're actually
|
||||
// polling, an unconfirmed/failed poll is treated as locked rather than silently trusting "not
|
||||
// running" — that unconfirmed-failure gap is exactly what previously let Generate stay clickable
|
||||
// during another project's generation. The initial state has to check the marker too, not just
|
||||
// hardcode false: a page refresh resets this hook's React state from scratch while another
|
||||
// project's marker (and its still-running backend generation) survives in localStorage, and the
|
||||
// first poll takes a network round trip to resolve — that gap is otherwise the same unconfirmed
|
||||
// window all over again, just re-opened on every reload instead of only at first app launch.
|
||||
export function useGlobalGenerationLock(): GlobalGenerationLock {
|
||||
const [lock, setLock] = useState<GlobalGenerationLock>(() => ({
|
||||
isRunning: localStorage.getItem(GENERATION_RECOVERY_KEY) != null,
|
||||
canCancel: readRecoveryMarkerCanCancel(),
|
||||
isCancelling: false,
|
||||
}))
|
||||
|
||||
useEffect(() => subscribeWhileGenerationMayBeActive(result => {
|
||||
if (!result.ok) {
|
||||
// Same fail-closed Generate lock as before. Stop keeps the frozen marker bit rather
|
||||
// than guessing — a starved poll during local GPU work must not hide Stop, and a
|
||||
// failed poll during an API job must not reveal it.
|
||||
setLock({
|
||||
isRunning: true,
|
||||
canCancel: readRecoveryMarkerCanCancel(),
|
||||
isCancelling: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
const isRunning = result.data.status === 'running'
|
||||
setLock({
|
||||
isRunning,
|
||||
canCancel: isRunning && result.data.cancellable,
|
||||
isCancelling: isRunning && result.data.phase === 'cancelled',
|
||||
})
|
||||
}), [])
|
||||
|
||||
return lock
|
||||
}
|
||||
Reference in New Issue
Block a user