Files
thrilltrack-explorer/src-old/hooks/useTransactionResilience.ts

206 lines
5.7 KiB
TypeScript

/**
* 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<Array<() => 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 <T,>(
action: 'approval' | 'rejection' | 'retry',
itemIds: string[],
transactionFn: (idempotencyKey: string) => Promise<T>
): Promise<T> => {
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,
};
}