fix(sockets): delete block case (#703)

This commit is contained in:
Vikhyath Mondreti
2025-07-15 21:21:59 -07:00
committed by GitHub
parent 19ca9c78b4
commit 27c248a70c
3 changed files with 97 additions and 8 deletions
+12 -4
View File
@@ -44,8 +44,14 @@ export function useCollaborativeWorkflow() {
const lastPositionTimestamps = useRef<Map<string, number>>(new Map())
// Operation queue
const { queue, hasOperationError, addToQueue, confirmOperation, failOperation } =
useOperationQueue()
const {
queue,
hasOperationError,
addToQueue,
confirmOperation,
failOperation,
cancelOperationsForBlock,
} = useOperationQueue()
// Clear position timestamps when switching workflows
// Note: Workflow joining is now handled automatically by socket connect event based on URL
@@ -332,7 +338,7 @@ export function useCollaborativeWorkflow() {
const { operationId, error, retryable } = data
logger.warn('Operation failed', { operationId, error, retryable })
failOperation(operationId)
failOperation(operationId, retryable)
}
// Register event handlers
@@ -534,9 +540,11 @@ export function useCollaborativeWorkflow() {
const collaborativeRemoveBlock = useCallback(
(id: string) => {
cancelOperationsForBlock(id)
executeQueuedOperation('remove', 'block', { id }, () => workflowStore.removeBlock(id))
},
[executeQueuedOperation, workflowStore]
[executeQueuedOperation, workflowStore, cancelOperationsForBlock]
)
const collaborativeUpdateBlockPosition = useCallback(
+9 -2
View File
@@ -125,9 +125,16 @@ export function setupSubblocksHandlers(
serverTimestamp: Date.now(),
})
}
}
logger.debug(`Subblock update in workflow ${workflowId}: ${blockId}.${subblockId}`)
logger.debug(`Subblock update in workflow ${workflowId}: ${blockId}.${subblockId}`)
} else if (operationId) {
// Block was deleted - notify client that operation completed (but didn't update anything)
socket.emit('operation-failed', {
operationId,
error: 'Block no longer exists',
retryable: false, // No point retrying for deleted blocks
})
}
} catch (error) {
logger.error('Error handling subblock update:', error)
+76 -2
View File
@@ -24,9 +24,10 @@ interface OperationQueueState {
addToQueue: (operation: Omit<QueuedOperation, 'timestamp' | 'retryCount' | 'status'>) => void
confirmOperation: (operationId: string) => void
failOperation: (operationId: string) => void
failOperation: (operationId: string, retryable?: boolean) => void
handleOperationTimeout: (operationId: string) => void
processNextOperation: () => void
cancelOperationsForBlock: (blockId: string) => void
triggerOfflineMode: () => void
clearError: () => void
@@ -211,7 +212,7 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
get().processNextOperation()
},
failOperation: (operationId: string) => {
failOperation: (operationId: string, retryable = true) => {
const state = get()
const operation = state.operations.find((op) => op.id === operationId)
if (!operation) {
@@ -239,6 +240,18 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
}
}
if (!retryable) {
logger.debug('Operation marked as non-retryable, removing from queue', { operationId })
set((state) => ({
operations: state.operations.filter((op) => op.id !== operationId),
isProcessing: false,
}))
get().processNextOperation()
return
}
if (operation.retryCount < 3) {
const newRetryCount = operation.retryCount + 1
const delay = 2 ** newRetryCount * 1000 // 2s, 4s, 8s
@@ -338,6 +351,66 @@ export const useOperationQueueStore = create<OperationQueueState>((set, get) =>
operationTimeouts.set(nextOperation.id, timeoutId)
},
cancelOperationsForBlock: (blockId: string) => {
logger.debug('Canceling all operations for block', { blockId })
// Cancel all debounce timeouts for this block's subblocks
const keysToDelete: string[] = []
for (const [key, timeout] of subblockDebounceTimeouts.entries()) {
if (key.startsWith(`${blockId}-`)) {
clearTimeout(timeout)
keysToDelete.push(key)
}
}
keysToDelete.forEach((key) => subblockDebounceTimeouts.delete(key))
// Find and cancel operation timeouts for operations related to this block
const state = get()
const operationsToCancel = state.operations.filter(
(op) =>
(op.operation.target === 'block' && op.operation.payload?.id === blockId) ||
(op.operation.target === 'subblock' && op.operation.payload?.blockId === blockId)
)
// Cancel timeouts for these operations
operationsToCancel.forEach((op) => {
const operationTimeout = operationTimeouts.get(op.id)
if (operationTimeout) {
clearTimeout(operationTimeout)
operationTimeouts.delete(op.id)
}
const retryTimeout = retryTimeouts.get(op.id)
if (retryTimeout) {
clearTimeout(retryTimeout)
retryTimeouts.delete(op.id)
}
})
// Remove all operations for this block (both pending and processing)
const newOperations = state.operations.filter(
(op) =>
!(
(op.operation.target === 'block' && op.operation.payload?.id === blockId) ||
(op.operation.target === 'subblock' && op.operation.payload?.blockId === blockId)
)
)
set({
operations: newOperations,
isProcessing: false, // Reset processing state in case we removed the current operation
})
logger.debug('Cancelled operations for block', {
blockId,
cancelledDebounceTimeouts: keysToDelete.length,
cancelledOperations: operationsToCancel.length,
})
// Process next operation if there are any remaining
get().processNextOperation()
},
triggerOfflineMode: () => {
logger.error('Operation failed after retries - triggering offline mode')
@@ -369,6 +442,7 @@ export function useOperationQueue() {
confirmOperation: store.confirmOperation,
failOperation: store.failOperation,
processNextOperation: store.processNextOperation,
cancelOperationsForBlock: store.cancelOperationsForBlock,
triggerOfflineMode: store.triggerOfflineMode,
clearError: store.clearError,
}