Create standalone browser port of LTX Athena with authenticated native API transport
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import { ApiClient } from './api-client'
|
||||
import { GENERATION_RECOVERY_KEY } from '../hooks/use-generation'
|
||||
|
||||
type ProgressResult = Awaited<ReturnType<typeof ApiClient.getGenerationProgress>>
|
||||
type Listener = (result: ProgressResult) => void
|
||||
|
||||
const POLL_INTERVAL_MS = 3000
|
||||
const MARKER_CHECK_INTERVAL_MS = 3000
|
||||
|
||||
const listeners = new Set<Listener>()
|
||||
let interval: ReturnType<typeof setInterval> | null = null
|
||||
let lastResult: ProgressResult | null = null
|
||||
let pollInFlight = false
|
||||
|
||||
async function poll(): Promise<void> {
|
||||
// getGenerationProgress can be slow under the exact event-loop-starvation this PR exists to
|
||||
// handle — without this guard, a slow response plus this interval's next tick queues a second
|
||||
// overlapping request, and whichever resolves last "wins" regardless of which was actually
|
||||
// more recent, dispatching stale results out of order.
|
||||
if (pollInFlight) return
|
||||
pollInFlight = true
|
||||
try {
|
||||
const result = await ApiClient.getGenerationProgress()
|
||||
lastResult = result
|
||||
listeners.forEach(listener => listener(result))
|
||||
} finally {
|
||||
pollInFlight = false
|
||||
}
|
||||
}
|
||||
|
||||
// The global generation lock and the background recovery watcher both need the same
|
||||
// generation-progress poll while mounted together (any project open); sharing one interval
|
||||
// instead of one per subscriber halves that network chatter.
|
||||
export function subscribeToGenerationProgress(listener: Listener): () => void {
|
||||
listeners.add(listener)
|
||||
if (lastResult) listener(lastResult)
|
||||
if (!interval) {
|
||||
void poll()
|
||||
interval = setInterval(poll, POLL_INTERVAL_MS)
|
||||
}
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
if (listeners.size === 0 && interval) {
|
||||
clearInterval(interval)
|
||||
interval = null
|
||||
// Otherwise a later generation's fresh subscribe (see subscribeWhileGenerationMayBeActive)
|
||||
// immediately replays this now-unrelated snapshot before its own first poll ever completes
|
||||
// - e.g. reporting "running" from a previous, unrelated session for a moment.
|
||||
lastResult = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Every generate-starting call site (see GenSpace.tsx's writeRecoveryContext calls) writes a
|
||||
// recovery marker into localStorage BEFORE it starts, in this same renderer process — so no
|
||||
// marker anywhere is proof nothing here could be running, checkable with a local read instead of
|
||||
// a network call. Gates the shared poll on that: idle app (no generation ever started, or one
|
||||
// that already finished and was consumed) costs zero network calls, not just at startup but for
|
||||
// the whole session, since this re-checks on the same cadence as the poll it gates.
|
||||
export function subscribeWhileGenerationMayBeActive(listener: Listener): () => void {
|
||||
let unsubscribe: (() => void) | null = null
|
||||
let latest: ProgressResult | null = null
|
||||
const trackingListener: Listener = result => {
|
||||
latest = result
|
||||
listener(result)
|
||||
}
|
||||
const sync = () => {
|
||||
const hasMarker = localStorage.getItem(GENERATION_RECOVERY_KEY) != null
|
||||
if (hasMarker) {
|
||||
if (!unsubscribe) unsubscribe = subscribeToGenerationProgress(trackingListener)
|
||||
return
|
||||
}
|
||||
// No marker doesn't mean safe to stop yet: a marker's writer can clear it eagerly, straight
|
||||
// off its own HTTP response (Enhance does — see GenSpace.tsx's runEnhance), without ever
|
||||
// going through a poll cycle first. If we unsubscribed immediately here, a shared poll tick
|
||||
// still mid-flight (or one that simply hasn't fired again yet) never gets to redeliver the
|
||||
// real terminal status, and the listener (e.g. useGlobalGenerationLock's isRunning) is stuck
|
||||
// on whatever "running" snapshot it last saw — forever, since nothing calls it again. Only
|
||||
// stop once a poll has actually caught up and confirmed we're not running.
|
||||
if (unsubscribe && latest && (!latest.ok || latest.data.status !== 'running')) {
|
||||
unsubscribe()
|
||||
unsubscribe = null
|
||||
}
|
||||
}
|
||||
sync()
|
||||
const interval = setInterval(sync, MARKER_CHECK_INTERVAL_MS)
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
unsubscribe?.()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user