Integrate transaction resilience hook

Integrate the `useTransactionResilience` hook into `SubmissionReviewManager.tsx` to add timeout detection, auto-release functionality, and idempotency key management to moderation actions. The `handleApprove` and `handleReject` functions have been updated to use the `executeTransaction` wrapper for these operations.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-07 15:36:53 +00:00
parent 34dbe2e262
commit fc8631ff0b

View File

@@ -6,6 +6,7 @@ import { handleError, getErrorMessage } from '@/lib/errorHandler';
import { invokeWithTracking } from '@/lib/edgeFunctionTracking';
import { moderationReducer, canApprove, canReject, hasActiveLock } from '@/lib/moderationStateMachine';
import { useLockMonitor } from '@/lib/moderation/lockMonitor';
import { useTransactionResilience } from '@/hooks/useTransactionResilience';
import {
fetchSubmissionItems,
buildDependencyTree,
@@ -92,6 +93,15 @@ export function SubmissionReviewManager({
// Lock monitoring integration
const { extendLock } = useLockMonitor(state, dispatch, submissionId);
// Transaction resilience (timeout detection & auto-release)
const { executeTransaction } = useTransactionResilience({
submissionId,
timeoutMs: 30000, // 30s timeout
autoReleaseOnUnload: true,
autoReleaseOnInactivity: true,
inactivityMinutes: 10,
});
// Moderation actions
const { escalateSubmission } = useModerationActions({
user,
@@ -230,6 +240,7 @@ export function SubmissionReviewManager({
}
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
const selectedIds = Array.from(selectedItemIds);
// Transition: reviewing → approving
dispatch({ type: 'START_APPROVAL' });
@@ -258,6 +269,7 @@ export function SubmissionReviewManager({
id: item.id
}))
);
setValidationResults(validationResultsMap);
@@ -324,64 +336,73 @@ export function SubmissionReviewManager({
return; // Ask for confirmation
}
// Proceed with approval
const { supabase } = await import('@/integrations/supabase/client');
// Call the edge function for backend processing
const { data, error, requestId } = await invokeWithTracking(
'process-selective-approval',
{
itemIds: Array.from(selectedItemIds),
submissionId
},
user?.id
// Proceed with approval - wrapped with transaction resilience
await executeTransaction(
'approval',
selectedIds,
async (idempotencyKey) => {
const { supabase } = await import('@/integrations/supabase/client');
// Call the edge function for backend processing
const { data, error, requestId } = await invokeWithTracking(
'process-selective-approval',
{
itemIds: selectedIds,
submissionId,
idempotencyKey, // Pass idempotency key to edge function
},
user?.id
);
if (error) {
throw new Error(error.message || 'Failed to process approval');
}
if (!data?.success) {
throw new Error(data?.error || 'Approval processing failed');
}
// Transition: approving → complete
dispatch({ type: 'COMPLETE', payload: { result: 'approved' } });
toast({
title: 'Items Approved',
description: `Successfully approved ${selectedIds.length} item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ''}`,
});
interface ApprovalResult { success: boolean; item_id: string; error?: string }
const successCount = data.results.filter((r: ApprovalResult) => r.success).length;
const failCount = data.results.filter((r: ApprovalResult) => !r.success).length;
const allFailed = failCount > 0 && successCount === 0;
const someFailed = failCount > 0 && successCount > 0;
toast({
title: allFailed ? 'Approval Failed' : someFailed ? 'Partial Approval' : 'Approval Complete',
description: failCount > 0
? `Approved ${successCount} item(s), ${failCount} failed`
: `Successfully approved ${successCount} item(s)`,
variant: allFailed ? 'destructive' : someFailed ? 'default' : 'default',
});
// Reset warning confirmation state after approval
setUserConfirmedWarnings(false);
// If ALL items failed, don't close dialog - show errors
if (allFailed) {
dispatch({ type: 'ERROR', payload: { error: 'All items failed' } });
return data;
}
// Reset warning confirmation state after approval
setUserConfirmedWarnings(false);
onComplete();
onOpenChange(false);
return data;
}
);
if (error) {
throw new Error(error.message || 'Failed to process approval');
}
if (!data?.success) {
throw new Error(data?.error || 'Approval processing failed');
}
// Transition: approving → complete
dispatch({ type: 'COMPLETE', payload: { result: 'approved' } });
toast({
title: 'Items Approved',
description: `Successfully approved ${selectedItemIds.size} item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ''}`,
});
interface ApprovalResult { success: boolean; item_id: string; error?: string }
const successCount = data.results.filter((r: ApprovalResult) => r.success).length;
const failCount = data.results.filter((r: ApprovalResult) => !r.success).length;
const allFailed = failCount > 0 && successCount === 0;
const someFailed = failCount > 0 && successCount > 0;
toast({
title: allFailed ? 'Approval Failed' : someFailed ? 'Partial Approval' : 'Approval Complete',
description: failCount > 0
? `Approved ${successCount} item(s), ${failCount} failed`
: `Successfully approved ${successCount} item(s)`,
variant: allFailed ? 'destructive' : someFailed ? 'default' : 'default',
});
// Reset warning confirmation state after approval
setUserConfirmedWarnings(false);
// If ALL items failed, don't close dialog - show errors
if (allFailed) {
dispatch({ type: 'ERROR', payload: { error: 'All items failed' } });
return;
}
// Reset warning confirmation state after approval
setUserConfirmedWarnings(false);
onComplete();
onOpenChange(false);
} catch (error: unknown) {
dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } });
handleError(error, {
@@ -438,23 +459,34 @@ export function SubmissionReviewManager({
if (!user?.id) return;
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
const selectedIds = selectedItems.map(item => item.id);
// Transition: reviewing → rejecting
dispatch({ type: 'START_REJECTION' });
try {
const selectedItems = items.filter(item => selectedItemIds.has(item.id));
await rejectSubmissionItems(selectedItems, reason, user.id, cascade);
// Transition: rejecting → complete
dispatch({ type: 'COMPLETE', payload: { result: 'rejected' } });
toast({
title: 'Items Rejected',
description: `Successfully rejected ${selectedItems.length} item${selectedItems.length !== 1 ? 's' : ''}`,
});
// Wrap rejection with transaction resilience
await executeTransaction(
'rejection',
selectedIds,
async (idempotencyKey) => {
await rejectSubmissionItems(selectedItems, reason, user.id, cascade);
// Transition: rejecting → complete
dispatch({ type: 'COMPLETE', payload: { result: 'rejected' } });
toast({
title: 'Items Rejected',
description: `Successfully rejected ${selectedItems.length} item${selectedItems.length !== 1 ? 's' : ''}`,
});
onComplete();
onOpenChange(false);
onComplete();
onOpenChange(false);
return { success: true };
}
);
} catch (error: unknown) {
dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } });
handleError(error, {