mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 12:33:51 +08:00
- Added QueueSchedulerService to manage task queue lifecycle, including state machine, dependency resolution, and session management. - Implemented HTTP API endpoints for queue scheduling: - POST /api/queue/execute: Submit items to the scheduler. - GET /api/queue/scheduler/state: Retrieve full scheduler state. - POST /api/queue/scheduler/start: Start scheduling loop with items. - POST /api/queue/scheduler/pause: Pause scheduling. - POST /api/queue/scheduler/stop: Graceful stop of the scheduler. - POST /api/queue/scheduler/config: Update scheduler configuration. - Introduced types for queue items, scheduler state, and WebSocket messages to ensure type safety and compatibility with the backend. - Added static model lists for LiteLLM as a fallback for available models.
352 lines
12 KiB
TypeScript
352 lines
12 KiB
TypeScript
// ========================================
|
|
// Queue Scheduler Store
|
|
// ========================================
|
|
// Zustand store for queue scheduler state management.
|
|
// Handles WebSocket message dispatch, API actions, and provides
|
|
// granular selectors for efficient React re-renders.
|
|
|
|
import { create } from 'zustand';
|
|
import { devtools } from 'zustand/middleware';
|
|
import type {
|
|
QueueSchedulerStatus,
|
|
QueueSchedulerConfig,
|
|
QueueItem,
|
|
QueueSchedulerState,
|
|
QueueWSMessage,
|
|
SessionBinding,
|
|
} from '../types/queue-frontend-types';
|
|
|
|
// ========== Default Config ==========
|
|
|
|
const DEFAULT_CONFIG: QueueSchedulerConfig = {
|
|
maxConcurrentSessions: 2,
|
|
sessionIdleTimeoutMs: 60_000,
|
|
resumeKeySessionBindingTimeoutMs: 300_000,
|
|
};
|
|
|
|
// ========== Store State Interface ==========
|
|
|
|
/**
|
|
* Store state extends the wire format QueueSchedulerState with
|
|
* nullable fields for "not yet loaded" state.
|
|
*/
|
|
interface QueueSchedulerStoreState {
|
|
status: QueueSchedulerStatus;
|
|
items: QueueItem[];
|
|
sessionPool: Record<string, SessionBinding>;
|
|
config: QueueSchedulerConfig;
|
|
currentConcurrency: number;
|
|
lastActivityAt: string | null;
|
|
error: string | null;
|
|
}
|
|
|
|
// ========== Actions Interface ==========
|
|
|
|
interface QueueSchedulerActions {
|
|
/** Dispatch a WebSocket message to update store state */
|
|
handleSchedulerMessage: (msg: QueueWSMessage) => void;
|
|
/** Fetch initial state from GET /api/queue/scheduler/state */
|
|
loadInitialState: () => Promise<void>;
|
|
/** Submit items to the queue via POST /api/queue/execute (auto-starts if idle) */
|
|
submitItems: (items: QueueItem[]) => Promise<void>;
|
|
/** Start the queue scheduler via POST /api/queue/scheduler/start */
|
|
startQueue: (items?: QueueItem[]) => Promise<void>;
|
|
/** Pause the queue scheduler via POST /api/queue/scheduler/pause */
|
|
pauseQueue: () => Promise<void>;
|
|
/** Stop the queue scheduler via POST /api/queue/scheduler/stop */
|
|
stopQueue: () => Promise<void>;
|
|
/** Update scheduler config via POST /api/queue/scheduler/config */
|
|
updateConfig: (config: Partial<QueueSchedulerConfig>) => Promise<void>;
|
|
}
|
|
|
|
export type QueueSchedulerStore = QueueSchedulerStoreState & QueueSchedulerActions;
|
|
|
|
// ========== Initial State ==========
|
|
|
|
const initialState: QueueSchedulerStoreState = {
|
|
status: 'idle',
|
|
items: [],
|
|
sessionPool: {},
|
|
config: DEFAULT_CONFIG,
|
|
currentConcurrency: 0,
|
|
lastActivityAt: null,
|
|
error: null,
|
|
};
|
|
|
|
// ========== Store ==========
|
|
|
|
export const useQueueSchedulerStore = create<QueueSchedulerStore>()(
|
|
devtools(
|
|
(set) => ({
|
|
...initialState,
|
|
|
|
// ========== WebSocket Message Handler ==========
|
|
|
|
handleSchedulerMessage: (msg: QueueWSMessage) => {
|
|
switch (msg.type) {
|
|
case 'QUEUE_SCHEDULER_STATE_UPDATE':
|
|
// Backend sends full state snapshot
|
|
set(
|
|
{
|
|
status: msg.state.status,
|
|
items: msg.state.items,
|
|
sessionPool: msg.state.sessionPool,
|
|
config: msg.state.config,
|
|
currentConcurrency: msg.state.currentConcurrency,
|
|
lastActivityAt: msg.state.lastActivityAt,
|
|
error: msg.state.error ?? null,
|
|
},
|
|
false,
|
|
'handleSchedulerMessage/QUEUE_SCHEDULER_STATE_UPDATE'
|
|
);
|
|
break;
|
|
|
|
case 'QUEUE_ITEM_ADDED':
|
|
set(
|
|
(state) => ({
|
|
items: [...state.items, msg.item],
|
|
}),
|
|
false,
|
|
'handleSchedulerMessage/QUEUE_ITEM_ADDED'
|
|
);
|
|
break;
|
|
|
|
case 'QUEUE_ITEM_UPDATED':
|
|
set(
|
|
(state) => ({
|
|
items: state.items.map((item) =>
|
|
item.item_id === msg.item.item_id ? msg.item : item
|
|
),
|
|
}),
|
|
false,
|
|
'handleSchedulerMessage/QUEUE_ITEM_UPDATED'
|
|
);
|
|
break;
|
|
|
|
case 'QUEUE_ITEM_REMOVED':
|
|
set(
|
|
(state) => ({
|
|
items: state.items.filter((item) => item.item_id !== msg.item_id),
|
|
}),
|
|
false,
|
|
'handleSchedulerMessage/QUEUE_ITEM_REMOVED'
|
|
);
|
|
break;
|
|
|
|
case 'QUEUE_SCHEDULER_CONFIG_UPDATED':
|
|
set(
|
|
{
|
|
config: msg.config,
|
|
},
|
|
false,
|
|
'handleSchedulerMessage/QUEUE_SCHEDULER_CONFIG_UPDATED'
|
|
);
|
|
break;
|
|
|
|
// No default - all 5 message types are handled exhaustively
|
|
}
|
|
},
|
|
|
|
// ========== API Actions ==========
|
|
|
|
submitItems: async (items: QueueItem[]) => {
|
|
try {
|
|
const response = await fetch('/api/queue/execute', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ items }),
|
|
});
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({}));
|
|
throw new Error(body.error || body.message || response.statusText);
|
|
}
|
|
// State will be updated via WebSocket broadcast from backend
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
console.error('[QueueScheduler] submitItems error:', message);
|
|
set({ error: message }, false, 'submitItems/error');
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
loadInitialState: async () => {
|
|
try {
|
|
const response = await fetch('/api/queue/scheduler/state', {
|
|
credentials: 'same-origin',
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load scheduler state: ${response.statusText}`);
|
|
}
|
|
const data: QueueSchedulerState = await response.json();
|
|
set(
|
|
{
|
|
status: data.status,
|
|
items: data.items,
|
|
sessionPool: data.sessionPool,
|
|
config: data.config,
|
|
currentConcurrency: data.currentConcurrency,
|
|
lastActivityAt: data.lastActivityAt,
|
|
error: data.error ?? null,
|
|
},
|
|
false,
|
|
'loadInitialState'
|
|
);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
console.error('[QueueScheduler] loadInitialState error:', message);
|
|
set({ error: message }, false, 'loadInitialState/error');
|
|
}
|
|
},
|
|
|
|
startQueue: async (items?: QueueItem[]) => {
|
|
try {
|
|
const body = items ? { items } : {};
|
|
const response = await fetch('/api/queue/scheduler/start', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({}));
|
|
throw new Error(body.error || body.message || response.statusText);
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
console.error('[QueueScheduler] startQueue error:', message);
|
|
set({ error: message }, false, 'startQueue/error');
|
|
}
|
|
},
|
|
|
|
pauseQueue: async () => {
|
|
try {
|
|
const response = await fetch('/api/queue/scheduler/pause', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({}));
|
|
throw new Error(body.error || body.message || response.statusText);
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
console.error('[QueueScheduler] pauseQueue error:', message);
|
|
set({ error: message }, false, 'pauseQueue/error');
|
|
}
|
|
},
|
|
|
|
stopQueue: async () => {
|
|
try {
|
|
const response = await fetch('/api/queue/scheduler/stop', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({}));
|
|
throw new Error(body.error || body.message || response.statusText);
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
console.error('[QueueScheduler] stopQueue error:', message);
|
|
set({ error: message }, false, 'stopQueue/error');
|
|
}
|
|
},
|
|
|
|
updateConfig: async (config: Partial<QueueSchedulerConfig>) => {
|
|
try {
|
|
const response = await fetch('/api/queue/scheduler/config', {
|
|
method: 'POST',
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config),
|
|
});
|
|
if (!response.ok) {
|
|
const body = await response.json().catch(() => ({}));
|
|
throw new Error(body.error || body.message || response.statusText);
|
|
}
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
console.error('[QueueScheduler] updateConfig error:', message);
|
|
set({ error: message }, false, 'updateConfig/error');
|
|
}
|
|
},
|
|
}),
|
|
{ name: 'QueueSchedulerStore' }
|
|
)
|
|
);
|
|
|
|
// ========== Selectors ==========
|
|
|
|
/** Stable empty array to avoid new references on each call */
|
|
const EMPTY_ITEMS: QueueItem[] = [];
|
|
|
|
/** Select current scheduler status */
|
|
export const selectQueueSchedulerStatus = (state: QueueSchedulerStore): QueueSchedulerStatus =>
|
|
state.status;
|
|
|
|
/** Select all queue items */
|
|
export const selectQueueItems = (state: QueueSchedulerStore): QueueItem[] =>
|
|
state.items;
|
|
|
|
/**
|
|
* Select items that are ready to execute (status 'queued' or 'pending').
|
|
* WARNING: Returns new array each call - use with useMemo in components.
|
|
*/
|
|
export const selectReadyItems = (state: QueueSchedulerStore): QueueItem[] => {
|
|
const ready = state.items.filter(
|
|
(item) => item.status === 'queued' || item.status === 'pending'
|
|
);
|
|
return ready.length === 0 ? EMPTY_ITEMS : ready;
|
|
};
|
|
|
|
/**
|
|
* Select items that are blocked (status 'blocked').
|
|
* WARNING: Returns new array each call - use with useMemo in components.
|
|
*/
|
|
export const selectBlockedItems = (state: QueueSchedulerStore): QueueItem[] => {
|
|
const blocked = state.items.filter((item) => item.status === 'blocked');
|
|
return blocked.length === 0 ? EMPTY_ITEMS : blocked;
|
|
};
|
|
|
|
/**
|
|
* Select items currently executing (status 'executing').
|
|
* WARNING: Returns new array each call - use with useMemo in components.
|
|
*/
|
|
export const selectExecutingItems = (state: QueueSchedulerStore): QueueItem[] => {
|
|
const executing = state.items.filter((item) => item.status === 'executing');
|
|
return executing.length === 0 ? EMPTY_ITEMS : executing;
|
|
};
|
|
|
|
/**
|
|
* Calculate overall scheduler progress as a percentage (0-100).
|
|
* Progress = (completed + failed) / total * 100.
|
|
* Returns 0 when there are no items.
|
|
*/
|
|
export const selectSchedulerProgress = (state: QueueSchedulerStore): number => {
|
|
const total = state.items.length;
|
|
if (total === 0) return 0;
|
|
const terminal = state.items.filter(
|
|
(item) => item.status === 'completed' || item.status === 'failed'
|
|
).length;
|
|
return Math.round((terminal / total) * 100);
|
|
};
|
|
|
|
/** Select scheduler config */
|
|
export const selectSchedulerConfig = (state: QueueSchedulerStore): QueueSchedulerConfig =>
|
|
state.config;
|
|
|
|
/** Select session pool */
|
|
export const selectSessionPool = (state: QueueSchedulerStore): Record<string, SessionBinding> =>
|
|
state.sessionPool;
|
|
|
|
/** Select current concurrency */
|
|
export const selectCurrentConcurrency = (state: QueueSchedulerStore): number =>
|
|
state.currentConcurrency;
|
|
|
|
/** Select scheduler error */
|
|
export const selectSchedulerError = (state: QueueSchedulerStore): string | null =>
|
|
state.error;
|