Implement Phase 4: Transaction Resilience

This commit implements Phase 4 of the Sacred Pipeline, focusing on transaction resilience. It introduces:

- **Timeout Detection & Recovery**: New utilities in `src/lib/timeoutDetection.ts` to detect, categorize (minor, moderate, critical), and provide recovery strategies for timeouts across various sources (fetch, Supabase, edge functions, database). Includes a `withTimeout` wrapper.
- **Lock Auto-Release**: Implemented in `src/lib/moderation/lockAutoRelease.ts` to automatically release submission locks on error, timeout, abandonment, or inactivity. Includes mechanisms for unload events and inactivity monitoring.
- **Idempotency Key Lifecycle Management**: A new module `src/lib/idempotencyLifecycle.ts` to track idempotency keys through their states (pending, processing, completed, failed, expired) using IndexedDB. Includes automatic cleanup of expired keys.
- **Enhanced Idempotency Helpers**: Updated `src/lib/idempotencyHelpers.ts` to integrate with the new lifecycle management, providing functions to generate, register, validate, and update the status of idempotency keys.
- **Transaction Resilience Hook**: A new hook `src/hooks/useTransactionResilience.ts` that combines timeout handling, lock auto-release, and idempotency key management for robust transaction execution.
- **Submission Queue Integration**: Updated `src/hooks/useSubmissionQueue.ts` to leverage the new submission queue and idempotency lifecycle functionalities.
- **Documentation**: Added `PHASE4_TRANSACTION_RESILIENCE.md` detailing the implemented features and their usage.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-07 15:03:12 +00:00
parent 095278dafd
commit 34dbe2e262
7 changed files with 1397 additions and 12 deletions

216
src/lib/timeoutDetection.ts Normal file
View File

@@ -0,0 +1,216 @@
/**
* Timeout Detection & Recovery
*
* Detects timeout errors from various sources (fetch, Supabase, edge functions)
* and provides recovery strategies.
*
* Part of Sacred Pipeline Phase 4: Transaction Resilience
*/
import { logger } from './logger';
export interface TimeoutError extends Error {
isTimeout: true;
source: 'fetch' | 'supabase' | 'edge-function' | 'database' | 'unknown';
originalError?: unknown;
duration?: number;
}
/**
* Check if an error is a timeout error
*/
export function isTimeoutError(error: unknown): boolean {
if (!error) return false;
// Check for AbortController timeout
if (error instanceof DOMException && error.name === 'AbortError') {
return true;
}
// Check for fetch timeout
if (error instanceof TypeError && error.message.includes('aborted')) {
return true;
}
// Check error message for timeout keywords
if (error instanceof Error) {
const message = error.message.toLowerCase();
return (
message.includes('timeout') ||
message.includes('timed out') ||
message.includes('deadline exceeded') ||
message.includes('request aborted') ||
message.includes('etimedout')
);
}
// Check Supabase/HTTP timeout status codes
if (error && typeof error === 'object') {
const errorObj = error as { status?: number; code?: string; message?: string };
// HTTP 408 Request Timeout
if (errorObj.status === 408) return true;
// HTTP 504 Gateway Timeout
if (errorObj.status === 504) return true;
// Supabase timeout codes
if (errorObj.code === 'PGRST301') return true; // Connection timeout
if (errorObj.code === '57014') return true; // PostgreSQL query cancelled
// Check message
if (errorObj.message?.toLowerCase().includes('timeout')) return true;
}
return false;
}
/**
* Wrap an error as a TimeoutError with source information
*/
export function wrapAsTimeoutError(
error: unknown,
source: TimeoutError['source'],
duration?: number
): TimeoutError {
const message = error instanceof Error ? error.message : 'Operation timed out';
const timeoutError = new Error(message) as TimeoutError;
timeoutError.name = 'TimeoutError';
timeoutError.isTimeout = true;
timeoutError.source = source;
timeoutError.originalError = error;
timeoutError.duration = duration;
return timeoutError;
}
/**
* Execute a function with a timeout wrapper
*
* @param fn - Function to execute
* @param timeoutMs - Timeout in milliseconds
* @param source - Source identifier for error tracking
* @returns Promise that resolves or rejects with timeout
*/
export async function withTimeout<T>(
fn: () => Promise<T>,
timeoutMs: number,
source: TimeoutError['source'] = 'unknown'
): Promise<T> {
const startTime = Date.now();
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort();
}, timeoutMs);
try {
// Execute the function with abort signal if supported
const result = await fn();
clearTimeout(timeoutId);
return result;
} catch (error) {
clearTimeout(timeoutId);
const duration = Date.now() - startTime;
// Check if error is timeout-related
if (isTimeoutError(error) || controller.signal.aborted) {
const timeoutError = wrapAsTimeoutError(error, source, duration);
logger.error('Operation timed out', {
source,
duration,
timeoutMs,
originalError: error instanceof Error ? error.message : String(error)
});
throw timeoutError;
}
// Re-throw non-timeout errors
throw error;
}
}
/**
* Categorize timeout severity for recovery strategy
*/
export function getTimeoutSeverity(error: TimeoutError): 'minor' | 'moderate' | 'critical' {
const { duration, source } = error;
// No duration means immediate abort - likely user action or critical failure
if (!duration) return 'critical';
// Database/edge function timeouts are more critical
if (source === 'database' || source === 'edge-function') {
if (duration > 30000) return 'critical'; // >30s
if (duration > 10000) return 'moderate'; // >10s
return 'minor';
}
// Fetch timeouts
if (source === 'fetch') {
if (duration > 60000) return 'critical'; // >60s
if (duration > 20000) return 'moderate'; // >20s
return 'minor';
}
return 'moderate';
}
/**
* Get recommended retry strategy based on timeout error
*/
export function getTimeoutRetryStrategy(error: TimeoutError): {
shouldRetry: boolean;
delayMs: number;
maxAttempts: number;
increaseTimeout: boolean;
} {
const severity = getTimeoutSeverity(error);
switch (severity) {
case 'minor':
return {
shouldRetry: true,
delayMs: 1000,
maxAttempts: 3,
increaseTimeout: false,
};
case 'moderate':
return {
shouldRetry: true,
delayMs: 3000,
maxAttempts: 2,
increaseTimeout: true, // Increase timeout by 50%
};
case 'critical':
return {
shouldRetry: false, // Don't auto-retry critical timeouts
delayMs: 5000,
maxAttempts: 1,
increaseTimeout: true,
};
}
}
/**
* User-friendly timeout error message
*/
export function getTimeoutErrorMessage(error: TimeoutError): string {
const severity = getTimeoutSeverity(error);
switch (severity) {
case 'minor':
return 'The request took longer than expected. Retrying...';
case 'moderate':
return 'The server is taking longer than usual to respond. Please wait while we retry.';
case 'critical':
return 'The operation timed out. Please check your connection and try again.';
}
}