Fixing task socket issues when adding tasks in task table.

This commit is contained in:
sean-eskerium
2025-08-21 03:01:35 -04:00
parent a549af726f
commit f0db9ac3bf
4 changed files with 74 additions and 75 deletions

View File

@@ -53,15 +53,15 @@ const mapDBStatusToUIStatus = (dbStatus: DatabaseTaskStatus): Task['status'] =>
const mapDatabaseTaskToUITask = (dbTask: any): Task => { const mapDatabaseTaskToUITask = (dbTask: any): Task => {
return { return {
id: dbTask.id, id: dbTask.id,
title: dbTask.title, title: dbTask.title || '',
description: dbTask.description || '', description: dbTask.description || '',
status: mapDBStatusToUIStatus(dbTask.status), status: mapDBStatusToUIStatus(dbTask.status || 'todo'),
assignee: { assignee: {
name: dbTask.assignee || 'User', name: dbTask.assignee || 'User',
avatar: '' avatar: ''
}, },
feature: dbTask.feature || 'General', feature: dbTask.feature || 'General',
featureColor: '#3b82f6', // Default blue color featureColor: dbTask.featureColor || '#3b82f6', // Default blue color
task_order: dbTask.task_order || 0, task_order: dbTask.task_order || 0,
}; };
}; };
@@ -90,7 +90,10 @@ export const TasksTab = ({
const { addPendingUpdate, isPendingUpdate, removePendingUpdate } = useOptimisticUpdates<Task>(); const { addPendingUpdate, isPendingUpdate, removePendingUpdate } = useOptimisticUpdates<Task>();
// Track recently deleted tasks to prevent race conditions // Track recently deleted tasks to prevent race conditions
const [recentlyDeletedIds, setRecentlyDeletedIds] = useState<Set<string>>(new Set()); const recentlyDeletedIdsRef = useRef<Set<string>>(new Set());
// Track recently created tasks to prevent WebSocket echo
const recentlyCreatedIdsRef = useRef<Set<string>>(new Set());
// Track the project ID to detect when we switch projects // Track the project ID to detect when we switch projects
const lastProjectId = useRef(projectId); const lastProjectId = useRef(projectId);
@@ -115,10 +118,13 @@ export const TasksTab = ({
// Optimized socket handlers with conflict resolution // Optimized socket handlers with conflict resolution
const handleTaskUpdated = useCallback((message: any) => { const handleTaskUpdated = useCallback((message: any) => {
const updatedTask = message.data || message; const updatedTask = message.data || message;
console.log('📝 Real-time task updated received:', updatedTask);
const mappedTask = mapDatabaseTaskToUITask(updatedTask); const mappedTask = mapDatabaseTaskToUITask(updatedTask);
console.log('📝 Mapped task:', mappedTask);
// Skip updates for recently deleted tasks (race condition prevention) // Skip updates for recently deleted tasks (race condition prevention)
if (recentlyDeletedIds.has(updatedTask.id)) { if (recentlyDeletedIdsRef.current.has(updatedTask.id)) {
console.log('[Socket] Ignoring update for recently deleted task:', updatedTask.id); console.log('[Socket] Ignoring update for recently deleted task:', updatedTask.id);
return; return;
} }
@@ -140,11 +146,14 @@ export const TasksTab = ({
// Use server timestamp for conflict resolution // Use server timestamp for conflict resolution
const existingTask = prev.find(task => task.id === updatedTask.id); const existingTask = prev.find(task => task.id === updatedTask.id);
// Skip if we already have this task (prevent duplicate additions)
if (!existingTask) { if (!existingTask) {
console.log('[Socket] Task not found locally, adding:', updatedTask.id); console.log('[Socket] Task not found locally, skipping update for:', updatedTask.id);
console.log('[Socket] Current task IDs:', prev.map(t => t.id));
return prev;
} }
console.log('[Socket] Updating task from:', existingTask.status, 'to:', mappedTask.status);
const updated = prev.map(task => const updated = prev.map(task =>
task.id === updatedTask.id task.id === updatedTask.id
? { ...mappedTask } ? { ...mappedTask }
@@ -155,36 +164,36 @@ export const TasksTab = ({
setTimeout(() => onTasksChange(updated), 0); setTimeout(() => onTasksChange(updated), 0);
return updated; return updated;
}); });
}, [onTasksChange, isModalOpen, editingTask?.id, recentlyDeletedIds, isPendingUpdate]); }, [onTasksChange, isModalOpen, editingTask?.id, isPendingUpdate]);
const handleTaskCreated = useCallback((message: any) => { const handleTaskCreated = useCallback((message: any) => {
const newTask = message.data || message; const newTask = message.data || message;
console.log('🆕 Real-time task created:', newTask); console.log('🆕 Real-time task created:', newTask);
// Skip if this is our own recently created task
if (recentlyCreatedIdsRef.current.has(newTask.id)) {
console.log('[Socket] Skipping echo of our own task creation:', newTask.id);
return;
}
const mappedTask = mapDatabaseTaskToUITask(newTask); const mappedTask = mapDatabaseTaskToUITask(newTask);
setTasks(prev => { setTasks(prev => {
// Check if this is replacing a temporary task from optimistic update
const hasTempTask = prev.some(task => task.id.startsWith('temp-') && task.title === mappedTask.title);
if (hasTempTask) {
// Replace temporary task with real task
const updated = prev.map(task =>
task.id.startsWith('temp-') && task.title === mappedTask.title
? mappedTask
: task
);
setTimeout(() => onTasksChange(updated), 0);
console.log('Replaced temporary task with real task:', mappedTask.id);
return updated;
}
// Check if task already exists to prevent duplicates // Check if task already exists to prevent duplicates
if (prev.some(task => task.id === newTask.id)) { if (prev.some(task => task.id === newTask.id)) {
console.log('Task already exists, skipping create'); console.log('Task already exists, skipping create');
return prev; return prev;
} }
const updated = [...prev, mappedTask]; // Remove any temp tasks with same title (in case of race condition)
const filteredPrev = prev.filter(task => {
// Keep non-temp tasks
if (!task.id?.startsWith('temp-')) return true;
// Remove temp tasks with matching title
return task.title !== newTask.title;
});
const updated = [...filteredPrev, mappedTask];
setTimeout(() => onTasksChange(updated), 0); setTimeout(() => onTasksChange(updated), 0);
return updated; return updated;
}); });
@@ -195,11 +204,7 @@ export const TasksTab = ({
console.log('🗑️ Real-time task deleted:', deletedTask); console.log('🗑️ Real-time task deleted:', deletedTask);
// Remove from recently deleted cache when deletion is confirmed // Remove from recently deleted cache when deletion is confirmed
setRecentlyDeletedIds(prev => { recentlyDeletedIdsRef.current.delete(deletedTask.id);
const newSet = new Set(prev);
newSet.delete(deletedTask.id);
return newSet;
});
setTasks(prev => { setTasks(prev => {
const updated = prev.filter(task => task.id !== deletedTask.id); const updated = prev.filter(task => task.id !== deletedTask.id);
@@ -234,7 +239,7 @@ export const TasksTab = ({
const initialWebSocketTasks = message.data || message; const initialWebSocketTasks = message.data || message;
const uiTasks: Task[] = initialWebSocketTasks.map(mapDatabaseTaskToUITask); const uiTasks: Task[] = initialWebSocketTasks.map(mapDatabaseTaskToUITask);
setTasks(uiTasks); setTasks(uiTasks);
onTasksChange(uiTasks); setTimeout(() => onTasksChange(uiTasks), 0);
}, [onTasksChange]); }, [onTasksChange]);
// Simplified socket connection with better lifecycle management // Simplified socket connection with better lifecycle management
@@ -297,7 +302,7 @@ export const TasksTab = ({
t.id === task.id ? task : t t.id === task.id ? task : t
); );
// Notify parent of the change // Notify parent of the change
onTasksChange(updated); setTimeout(() => onTasksChange(updated), 0);
return updated; return updated;
}); });
@@ -355,7 +360,7 @@ export const TasksTab = ({
t.id === task.id ? originalTask : t t.id === task.id ? originalTask : t
); );
// Notify parent of the rollback // Notify parent of the rollback
onTasksChange(updated); setTimeout(() => onTasksChange(updated), 0);
return updated; return updated;
}); });
@@ -372,7 +377,7 @@ export const TasksTab = ({
// Update tasks helper // Update tasks helper
const updateTasks = (newTasks: Task[]) => { const updateTasks = (newTasks: Task[]) => {
setTasks(newTasks); setTasks(newTasks);
onTasksChange(newTasks); setTimeout(() => onTasksChange(newTasks), 0);
}; };
// Helper function to reorder tasks by status to ensure no gaps (1,2,3...) // Helper function to reorder tasks by status to ensure no gaps (1,2,3...)
@@ -598,7 +603,7 @@ export const TasksTab = ({
try { try {
// Add to recently deleted cache to prevent race conditions // Add to recently deleted cache to prevent race conditions
setRecentlyDeletedIds(prev => new Set(prev).add(taskToDelete.id)); recentlyDeletedIdsRef.current.add(taskToDelete.id);
// OPTIMISTIC UPDATE: Remove task from UI immediately // OPTIMISTIC UPDATE: Remove task from UI immediately
setTasks(prev => { setTasks(prev => {
@@ -614,22 +619,14 @@ export const TasksTab = ({
// Clear from recently deleted cache after a delay (to catch any lingering socket events) // Clear from recently deleted cache after a delay (to catch any lingering socket events)
setTimeout(() => { setTimeout(() => {
setRecentlyDeletedIds(prev => { recentlyDeletedIdsRef.current.delete(taskToDelete.id);
const newSet = new Set(prev);
newSet.delete(taskToDelete.id);
return newSet;
});
}, 3000); // 3 second window to ignore stale socket events }, 3000); // 3 second window to ignore stale socket events
} catch (error) { } catch (error) {
console.error('Failed to delete task:', error); console.error('Failed to delete task:', error);
// Remove from recently deleted cache on error // Remove from recently deleted cache on error
setRecentlyDeletedIds(prev => { recentlyDeletedIdsRef.current.delete(taskToDelete.id);
const newSet = new Set(prev);
newSet.delete(taskToDelete.id);
return newSet;
});
// ROLLBACK on error - restore the task // ROLLBACK on error - restore the task
setTasks(prev => { setTasks(prev => {
@@ -647,7 +644,7 @@ export const TasksTab = ({
} }
}; };
// Inline task creation function // Inline task creation function with optimistic update
const createTaskInline = async (newTask: Omit<Task, 'id'>) => { const createTaskInline = async (newTask: Omit<Task, 'id'>) => {
// Create temporary task with a temp ID for optimistic update // Create temporary task with a temp ID for optimistic update
const tempId = `temp-${Date.now()}`; const tempId = `temp-${Date.now()}`;
@@ -663,10 +660,11 @@ export const TasksTab = ({
}; };
// OPTIMISTIC UPDATE: Add to UI immediately // OPTIMISTIC UPDATE: Add to UI immediately
setTasks(prev => [...prev, tempTask]); setTasks(prev => {
const updated = [...prev, tempTask];
// Notify parent component of the change setTimeout(() => onTasksChange(updated), 0);
onTasksChange([...tasks, tempTask]); return updated;
});
const createData: CreateTaskRequest = { const createData: CreateTaskRequest = {
project_id: projectId, project_id: projectId,
@@ -680,14 +678,21 @@ export const TasksTab = ({
}; };
const createdTask = await projectService.createTask(createData); const createdTask = await projectService.createTask(createData);
const mappedCreatedTask = mapDatabaseTaskToUITask(createdTask);
// Add to recently created to prevent WebSocket echo from duplicating
recentlyCreatedIdsRef.current.add(createdTask.id);
setTimeout(() => {
recentlyCreatedIdsRef.current.delete(createdTask.id);
}, 5000);
// Replace temp task with real one // Replace temp task with real one
setTasks(prev => { setTasks(prev => {
// Find and replace the temp task
const updated = prev.map(t => const updated = prev.map(t =>
t.id === tempId ? mapDatabaseTaskToUITask(createdTask) : t t.id === tempId ? mappedCreatedTask : t
); );
// Notify parent of the update setTimeout(() => onTasksChange(updated), 0);
onTasksChange(updated);
return updated; return updated;
}); });
@@ -697,11 +702,7 @@ export const TasksTab = ({
console.error('Failed to create task:', error); console.error('Failed to create task:', error);
// Rollback: Remove temp task on error // Rollback: Remove temp task on error
setTasks(prev => { setTasks(prev => prev.filter(t => t.id !== tempId));
const updated = prev.filter(t => t.id !== tempId);
onTasksChange(updated);
return updated;
});
throw error; throw error;
} }
@@ -831,7 +832,7 @@ export const TasksTab = ({
<div className="relative h-[calc(100vh-220px)] overflow-auto"> <div className="relative h-[calc(100vh-220px)] overflow-auto">
{viewMode === 'table' ? ( {viewMode === 'table' ? (
<TaskTableView <TaskTableView
tasks={tasks} tasks={tasks.filter(t => t && t.id && t.title !== undefined)}
onTaskView={openEditModal} onTaskView={openEditModal}
onTaskComplete={completeTask} onTaskComplete={completeTask}
onTaskDelete={deleteTask} onTaskDelete={deleteTask}
@@ -841,7 +842,7 @@ export const TasksTab = ({
/> />
) : ( ) : (
<TaskBoardView <TaskBoardView
tasks={tasks} tasks={tasks.filter(t => t && t.id && t.title !== undefined)}
onTaskView={openEditModal} onTaskView={openEditModal}
onTaskComplete={completeTask} onTaskComplete={completeTask}
onTaskDelete={deleteTask} onTaskDelete={deleteTask}

View File

@@ -98,7 +98,7 @@ export function useTaskSocket(options: UseTaskSocketOptions) {
return () => { return () => {
clearInterval(interval); clearInterval(interval);
}; };
}, [onConnectionStateChange, memoizedHandlers]); }, []); // No dependencies - only run once on mount
// Initialize connection once and register handlers // Initialize connection once and register handlers
useEffect(() => { useEffect(() => {
@@ -132,15 +132,7 @@ export function useTaskSocket(options: UseTaskSocketOptions) {
initializeConnection(); initializeConnection();
}, [projectId, memoizedHandlers]); }, [projectId]); // Only depend on projectId
// Update handlers when they change (without reconnecting)
useEffect(() => {
if (isInitializedRef.current && currentProjectIdRef.current === projectId) {
console.log(`[USE_TASK_SOCKET] Updating handlers for component: ${componentIdRef.current}`);
taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers());
}
}, [memoizedHandlers, projectId]);
// Handle project change (different project) // Handle project change (different project)
useEffect(() => { useEffect(() => {
@@ -176,7 +168,7 @@ export function useTaskSocket(options: UseTaskSocketOptions) {
switchProject(); switchProject();
} }
}, [projectId, memoizedHandlers]); }, [projectId]); // Only depend on projectId
// Cleanup on unmount // Cleanup on unmount
useEffect(() => { useEffect(() => {

View File

@@ -367,12 +367,12 @@ export function ProjectPage({
const tasksData = await projectService.getTasksByProject(projectId); const tasksData = await projectService.getTasksByProject(projectId);
// Convert backend tasks to UI format // Convert backend tasks to UI format with proper defaults
const uiTasks: Task[] = tasksData.map(task => ({ const uiTasks: Task[] = tasksData.map(task => ({
id: task.id, id: task.id,
title: task.title, title: task.title || '',
description: task.description, description: task.description || '',
status: (task.uiStatus || 'backlog') as Task['status'], status: (task.uiStatus || task.status || 'backlog') as Task['status'],
assignee: { assignee: {
name: (task.assignee || 'User') as 'User' | 'Archon' | 'AI IDE Agent', name: (task.assignee || 'User') as 'User' | 'Archon' | 'AI IDE Agent',
avatar: '' avatar: ''

View File

@@ -195,7 +195,13 @@ export const statusMappings = {
export function dbTaskToUITask(dbTask: Task): Task { export function dbTaskToUITask(dbTask: Task): Task {
return { return {
...dbTask, ...dbTask,
uiStatus: statusMappings.dbToUI[dbTask.status] uiStatus: statusMappings.dbToUI[dbTask.status || 'todo'],
// Ensure all required fields have defaults
title: dbTask.title || '',
description: dbTask.description || '',
assignee: dbTask.assignee || 'User',
feature: dbTask.feature || 'General',
task_order: dbTask.task_order || 0
}; };
} }