/** * Transaction Resilience Hook * * Combines timeout detection, lock auto-release, and idempotency lifecycle * into a unified hook for moderation transactions. * * Part of Sacred Pipeline Phase 4: Transaction Resilience */ import { useEffect, useCallback, useRef } from 'react'; import { useAuth } from '@/hooks/useAuth'; import { withTimeout, isTimeoutError, getTimeoutErrorMessage, type TimeoutError, } from '@/lib/timeoutDetection'; import { autoReleaseLockOnError, setupAutoReleaseOnUnload, setupInactivityAutoRelease, } from '@/lib/moderation/lockAutoRelease'; import { generateAndRegisterKey, validateAndStartProcessing, markKeyCompleted, markKeyFailed, is409Conflict, getRetryAfter, sleep, } from '@/lib/idempotencyHelpers'; import { toast } from '@/hooks/use-toast'; import { logger } from '@/lib/logger'; interface TransactionResilientOptions { submissionId: string; /** Timeout in milliseconds (default: 30000) */ timeoutMs?: number; /** Enable auto-release on unload (default: true) */ autoReleaseOnUnload?: boolean; /** Enable inactivity auto-release (default: true) */ autoReleaseOnInactivity?: boolean; /** Inactivity timeout in minutes (default: 10) */ inactivityMinutes?: number; } export function useTransactionResilience(options: TransactionResilientOptions) { const { submissionId, timeoutMs = 30000, autoReleaseOnUnload = true, autoReleaseOnInactivity = true, inactivityMinutes = 10 } = options; const { user } = useAuth(); const cleanupFnsRef = useRef void>>([]); // Setup auto-release mechanisms useEffect(() => { if (!user?.id) return; const cleanupFns: Array<() => void> = []; // Setup unload auto-release if (autoReleaseOnUnload) { const cleanup = setupAutoReleaseOnUnload(submissionId, user.id); cleanupFns.push(cleanup); } // Setup inactivity auto-release if (autoReleaseOnInactivity) { const cleanup = setupInactivityAutoRelease(submissionId, user.id, inactivityMinutes); cleanupFns.push(cleanup); } cleanupFnsRef.current = cleanupFns; // Cleanup on unmount return () => { cleanupFns.forEach(fn => fn()); }; }, [submissionId, user?.id, autoReleaseOnUnload, autoReleaseOnInactivity, inactivityMinutes]); /** * Execute a transaction with full resilience (timeout, idempotency, auto-release) */ const executeTransaction = useCallback( async ( action: 'approval' | 'rejection' | 'retry', itemIds: string[], transactionFn: (idempotencyKey: string) => Promise ): Promise => { if (!user?.id) { throw new Error('User not authenticated'); } // Generate and register idempotency key const { key: idempotencyKey } = await generateAndRegisterKey( action, submissionId, itemIds, user.id ); logger.info('[TransactionResilience] Starting transaction', { action, submissionId, itemIds, idempotencyKey, }); try { // Validate key and mark as processing const isValid = await validateAndStartProcessing(idempotencyKey); if (!isValid) { throw new Error('Idempotency key validation failed - possible duplicate request'); } // Execute transaction with timeout const result = await withTimeout( () => transactionFn(idempotencyKey), timeoutMs, 'edge-function' ); // Mark key as completed await markKeyCompleted(idempotencyKey); logger.info('[TransactionResilience] Transaction completed', { action, submissionId, idempotencyKey, }); return result; } catch (error) { // Check for timeout if (isTimeoutError(error)) { const timeoutError = error as TimeoutError; const message = getTimeoutErrorMessage(timeoutError); logger.error('[TransactionResilience] Transaction timed out', { action, submissionId, idempotencyKey, duration: timeoutError.duration, }); // Auto-release lock on timeout await autoReleaseLockOnError(submissionId, user.id, error); // Mark key as failed await markKeyFailed(idempotencyKey, message); toast({ title: 'Transaction Timeout', description: message, variant: 'destructive', }); throw timeoutError; } // Check for 409 Conflict (duplicate request) if (is409Conflict(error)) { const retryAfter = getRetryAfter(error); logger.warn('[TransactionResilience] Duplicate request detected', { action, submissionId, idempotencyKey, retryAfter, }); toast({ title: 'Duplicate Request', description: `This action is already being processed. Please wait ${retryAfter}s.`, }); // Wait and return (don't auto-release, the other request is handling it) await sleep(retryAfter * 1000); throw error; } // Generic error handling const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error('[TransactionResilience] Transaction failed', { action, submissionId, idempotencyKey, error: errorMessage, }); // Auto-release lock on error await autoReleaseLockOnError(submissionId, user.id, error); // Mark key as failed await markKeyFailed(idempotencyKey, errorMessage); throw error; } }, [submissionId, user?.id, timeoutMs] ); return { executeTransaction, }; }