69 lines
2.5 KiB
TypeScript
69 lines
2.5 KiB
TypeScript
import { useEffect, useSyncExternalStore } from 'react'
|
|
import type { QueuedCommand } from '../types/textInputTypes.js'
|
|
import {
|
|
getCommandQueueSnapshot,
|
|
subscribeToCommandQueue,
|
|
} from '../utils/messageQueueManager.js'
|
|
import type { QueryGuard } from '../utils/QueryGuard.js'
|
|
import { processQueueIfReady } from '../utils/queueProcessor.js'
|
|
|
|
type UseQueueProcessorParams = {
|
|
executeQueuedInput: (commands: QueuedCommand[]) => Promise<void>
|
|
hasActiveLocalJsxUI: boolean
|
|
queryGuard: QueryGuard
|
|
}
|
|
|
|
/**
|
|
* Hook that processes queued commands when conditions are met.
|
|
*
|
|
* Uses a single unified command queue (module-level store). Priority determines
|
|
* processing order: 'now' > 'next' (user input) > 'later' (task notifications).
|
|
* The dequeue() function handles priority ordering automatically.
|
|
*
|
|
* Processing triggers when:
|
|
* - No query active (queryGuard — reactive via useSyncExternalStore)
|
|
* - Queue has items
|
|
* - No active local JSX UI blocking input
|
|
*/
|
|
export function useQueueProcessor({
|
|
executeQueuedInput,
|
|
hasActiveLocalJsxUI,
|
|
queryGuard,
|
|
}: UseQueueProcessorParams): void {
|
|
// Subscribe to the query guard. Re-renders when a query starts or ends
|
|
// (or when reserve/cancelReservation transitions dispatching state).
|
|
const isQueryActive = useSyncExternalStore(
|
|
queryGuard.subscribe,
|
|
queryGuard.getSnapshot,
|
|
)
|
|
|
|
// Subscribe to the unified command queue via useSyncExternalStore.
|
|
// This guarantees re-render when the store changes, bypassing
|
|
// React context propagation delays that cause missed notifications in Ink.
|
|
const queueSnapshot = useSyncExternalStore(
|
|
subscribeToCommandQueue,
|
|
getCommandQueueSnapshot,
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (isQueryActive) return
|
|
if (hasActiveLocalJsxUI) return
|
|
if (queueSnapshot.length === 0) return
|
|
|
|
// Reservation is now owned by handlePromptSubmit (inside executeUserInput's
|
|
// try block). The sync chain executeQueuedInput → handlePromptSubmit →
|
|
// executeUserInput → queryGuard.reserve() runs before the first real await,
|
|
// so by the time React re-runs this effect (due to the dequeue-triggered
|
|
// snapshot change), isQueryActive is already true (dispatching) and the
|
|
// guard above returns early. handlePromptSubmit's finally releases the
|
|
// reservation via cancelReservation() (no-op if onQuery already ran end()).
|
|
processQueueIfReady({ executeInput: executeQueuedInput })
|
|
}, [
|
|
queueSnapshot,
|
|
isQueryActive,
|
|
executeQueuedInput,
|
|
hasActiveLocalJsxUI,
|
|
queryGuard,
|
|
])
|
|
}
|