feat: implement FlowExecutor for executing flow definitions with DAG traversal and node execution

This commit is contained in:
catlog22
2026-01-30 16:59:18 +08:00
parent 0a7c1454d9
commit a5c3dff8d3
92 changed files with 23875 additions and 542 deletions

View File

@@ -0,0 +1,373 @@
// ========================================
// useSessions Hook
// ========================================
// TanStack Query hooks for sessions with optimistic updates
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
fetchSessions,
createSession,
updateSession,
archiveSession,
deleteSession,
type SessionsResponse,
type CreateSessionInput,
type UpdateSessionInput,
} from '../lib/api';
import type { SessionMetadata } from '../types/store';
import { dashboardStatsKeys } from './useDashboardStats';
// Query key factory
export const sessionsKeys = {
all: ['sessions'] as const,
lists: () => [...sessionsKeys.all, 'list'] as const,
list: (filters?: SessionsFilter) => [...sessionsKeys.lists(), filters] as const,
details: () => [...sessionsKeys.all, 'detail'] as const,
detail: (id: string) => [...sessionsKeys.details(), id] as const,
};
// Default stale time: 30 seconds
const STALE_TIME = 30 * 1000;
export interface SessionsFilter {
status?: SessionMetadata['status'][];
search?: string;
location?: 'active' | 'archived' | 'all';
}
export interface UseSessionsOptions {
/** Filter options */
filter?: SessionsFilter;
/** Override default stale time (ms) */
staleTime?: number;
/** Enable/disable the query */
enabled?: boolean;
/** Refetch interval (ms), 0 to disable */
refetchInterval?: number;
}
export interface UseSessionsReturn {
/** All sessions data */
sessions: SessionsResponse | undefined;
/** Active sessions */
activeSessions: SessionMetadata[];
/** Archived sessions */
archivedSessions: SessionMetadata[];
/** Filtered sessions based on filter options */
filteredSessions: SessionMetadata[];
/** Loading state for initial fetch */
isLoading: boolean;
/** Fetching state (initial or refetch) */
isFetching: boolean;
/** Error object if query failed */
error: Error | null;
/** Manually refetch data */
refetch: () => Promise<void>;
/** Invalidate and refetch sessions */
invalidate: () => Promise<void>;
}
/**
* Hook for fetching sessions data
*
* @example
* ```tsx
* const { activeSessions, isLoading } = useSessions({
* filter: { location: 'active' }
* });
* ```
*/
export function useSessions(options: UseSessionsOptions = {}): UseSessionsReturn {
const { filter, staleTime = STALE_TIME, enabled = true, refetchInterval = 0 } = options;
const queryClient = useQueryClient();
const query = useQuery({
queryKey: sessionsKeys.list(filter),
queryFn: fetchSessions,
staleTime,
enabled,
refetchInterval: refetchInterval > 0 ? refetchInterval : false,
retry: 2,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),
});
const activeSessions = query.data?.activeSessions ?? [];
const archivedSessions = query.data?.archivedSessions ?? [];
// Apply client-side filtering
const filteredSessions = (() => {
let sessions: SessionMetadata[] = [];
if (!filter?.location || filter.location === 'all') {
sessions = [...activeSessions, ...archivedSessions];
} else if (filter.location === 'active') {
sessions = activeSessions;
} else {
sessions = archivedSessions;
}
// Apply status filter
if (filter?.status && filter.status.length > 0) {
sessions = sessions.filter((s) => filter.status!.includes(s.status));
}
// Apply search filter
if (filter?.search) {
const searchLower = filter.search.toLowerCase();
sessions = sessions.filter(
(s) =>
s.session_id.toLowerCase().includes(searchLower) ||
s.title?.toLowerCase().includes(searchLower) ||
s.description?.toLowerCase().includes(searchLower)
);
}
return sessions;
})();
const refetch = async () => {
await query.refetch();
};
const invalidate = async () => {
await queryClient.invalidateQueries({ queryKey: sessionsKeys.all });
};
return {
sessions: query.data,
activeSessions,
archivedSessions,
filteredSessions,
isLoading: query.isLoading,
isFetching: query.isFetching,
error: query.error,
refetch,
invalidate,
};
}
// ========== Mutations ==========
export interface UseCreateSessionReturn {
createSession: (input: CreateSessionInput) => Promise<SessionMetadata>;
isCreating: boolean;
error: Error | null;
}
/**
* Hook for creating a new session
*/
export function useCreateSession(): UseCreateSessionReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: createSession,
onSuccess: (newSession) => {
// Update sessions cache
queryClient.setQueryData<SessionsResponse>(sessionsKeys.list(), (old) => {
if (!old) return { activeSessions: [newSession], archivedSessions: [] };
return {
...old,
activeSessions: [newSession, ...old.activeSessions],
};
});
// Invalidate dashboard stats
queryClient.invalidateQueries({ queryKey: dashboardStatsKeys.all });
},
});
return {
createSession: mutation.mutateAsync,
isCreating: mutation.isPending,
error: mutation.error,
};
}
export interface UseUpdateSessionReturn {
updateSession: (sessionId: string, input: UpdateSessionInput) => Promise<SessionMetadata>;
isUpdating: boolean;
error: Error | null;
}
/**
* Hook for updating a session
*/
export function useUpdateSession(): UseUpdateSessionReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: ({ sessionId, input }: { sessionId: string; input: UpdateSessionInput }) =>
updateSession(sessionId, input),
onSuccess: (updatedSession) => {
// Update sessions cache
queryClient.setQueryData<SessionsResponse>(sessionsKeys.list(), (old) => {
if (!old) return old;
return {
activeSessions: old.activeSessions.map((s) =>
s.session_id === updatedSession.session_id ? updatedSession : s
),
archivedSessions: old.archivedSessions.map((s) =>
s.session_id === updatedSession.session_id ? updatedSession : s
),
};
});
},
});
return {
updateSession: (sessionId, input) => mutation.mutateAsync({ sessionId, input }),
isUpdating: mutation.isPending,
error: mutation.error,
};
}
export interface UseArchiveSessionReturn {
archiveSession: (sessionId: string) => Promise<SessionMetadata>;
isArchiving: boolean;
error: Error | null;
}
/**
* Hook for archiving a session with optimistic update
*/
export function useArchiveSession(): UseArchiveSessionReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: archiveSession,
onMutate: async (sessionId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: sessionsKeys.all });
// Snapshot previous value
const previousSessions = queryClient.getQueryData<SessionsResponse>(sessionsKeys.list());
// Optimistically update
queryClient.setQueryData<SessionsResponse>(sessionsKeys.list(), (old) => {
if (!old) return old;
const session = old.activeSessions.find((s) => s.session_id === sessionId);
if (!session) return old;
const archivedSession: SessionMetadata = {
...session,
status: 'archived',
location: 'archived',
updated_at: new Date().toISOString(),
};
return {
activeSessions: old.activeSessions.filter((s) => s.session_id !== sessionId),
archivedSessions: [archivedSession, ...old.archivedSessions],
};
});
return { previousSessions };
},
onError: (_error, _sessionId, context) => {
// Rollback on error
if (context?.previousSessions) {
queryClient.setQueryData(sessionsKeys.list(), context.previousSessions);
}
},
onSettled: () => {
// Invalidate to ensure sync with server
queryClient.invalidateQueries({ queryKey: sessionsKeys.all });
queryClient.invalidateQueries({ queryKey: dashboardStatsKeys.all });
},
});
return {
archiveSession: mutation.mutateAsync,
isArchiving: mutation.isPending,
error: mutation.error,
};
}
export interface UseDeleteSessionReturn {
deleteSession: (sessionId: string) => Promise<void>;
isDeleting: boolean;
error: Error | null;
}
/**
* Hook for deleting a session with optimistic update
*/
export function useDeleteSession(): UseDeleteSessionReturn {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: deleteSession,
onMutate: async (sessionId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: sessionsKeys.all });
// Snapshot previous value
const previousSessions = queryClient.getQueryData<SessionsResponse>(sessionsKeys.list());
// Optimistically remove
queryClient.setQueryData<SessionsResponse>(sessionsKeys.list(), (old) => {
if (!old) return old;
return {
activeSessions: old.activeSessions.filter((s) => s.session_id !== sessionId),
archivedSessions: old.archivedSessions.filter((s) => s.session_id !== sessionId),
};
});
return { previousSessions };
},
onError: (_error, _sessionId, context) => {
// Rollback on error
if (context?.previousSessions) {
queryClient.setQueryData(sessionsKeys.list(), context.previousSessions);
}
},
onSettled: () => {
// Invalidate to ensure sync with server
queryClient.invalidateQueries({ queryKey: sessionsKeys.all });
queryClient.invalidateQueries({ queryKey: dashboardStatsKeys.all });
},
});
return {
deleteSession: mutation.mutateAsync,
isDeleting: mutation.isPending,
error: mutation.error,
};
}
/**
* Combined hook for all session mutations
*/
export function useSessionMutations() {
const create = useCreateSession();
const update = useUpdateSession();
const archive = useArchiveSession();
const remove = useDeleteSession();
return {
createSession: create.createSession,
updateSession: update.updateSession,
archiveSession: archive.archiveSession,
deleteSession: remove.deleteSession,
isCreating: create.isCreating,
isUpdating: update.isUpdating,
isArchiving: archive.isArchiving,
isDeleting: remove.isDeleting,
isMutating: create.isCreating || update.isUpdating || archive.isArchiving || remove.isDeleting,
};
}
/**
* Hook to prefetch sessions data
*/
export function usePrefetchSessions() {
const queryClient = useQueryClient();
return (filter?: SessionsFilter) => {
queryClient.prefetchQuery({
queryKey: sessionsKeys.list(filter),
queryFn: fetchSessions,
staleTime: STALE_TIME,
});
};
}