feat: Add idempotency to useModerationActions

Implement idempotency integration in the useModerationActions hook as per the detailed plan.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-06 17:43:16 +00:00
parent 85436b5c1e
commit b92a62ebc8
3 changed files with 321 additions and 30 deletions

View File

@@ -0,0 +1,90 @@
/**
* Idempotency Key Utilities
*
* Provides helper functions for generating and managing idempotency keys
* for moderation operations to prevent duplicate requests.
*/
/**
* Generate a deterministic idempotency key for a moderation action
*
* Format: action_submissionId_itemIds_userId_timestamp
* Example: approval_abc123_def456_ghi789_user123_1699564800000
*
* @param action - The moderation action type ('approval', 'rejection', 'retry')
* @param submissionId - The submission ID
* @param itemIds - Array of item IDs being processed
* @param userId - The moderator's user ID
* @returns Deterministic idempotency key
*/
export function generateIdempotencyKey(
action: 'approval' | 'rejection' | 'retry',
submissionId: string,
itemIds: string[],
userId: string
): string {
// Sort itemIds to ensure consistency regardless of order
const sortedItemIds = [...itemIds].sort().join('_');
// Include timestamp to allow same moderator to retry after 24h window
const timestamp = Date.now();
return `${action}_${submissionId}_${sortedItemIds}_${userId}_${timestamp}`;
}
/**
* Check if an error is a 409 Conflict (duplicate request)
*
* @param error - Error object to check
* @returns True if error is 409 Conflict
*/
export function is409Conflict(error: unknown): boolean {
if (!error || typeof error !== 'object') return false;
const errorObj = error as { status?: number; message?: string };
// Check status code
if (errorObj.status === 409) return true;
// Check error message for conflict indicators
const message = errorObj.message?.toLowerCase() || '';
return message.includes('duplicate request') ||
message.includes('already in progress') ||
message.includes('race condition');
}
/**
* Extract retry-after value from error response
*
* @param error - Error object with potential Retry-After header
* @returns Seconds to wait before retry, defaults to 3
*/
export function getRetryAfter(error: unknown): number {
if (!error || typeof error !== 'object') return 3;
const errorObj = error as {
retryAfter?: number;
context?: { headers?: { 'Retry-After'?: string } }
};
// Check structured retryAfter field
if (errorObj.retryAfter) return errorObj.retryAfter;
// Check Retry-After header
const retryAfterHeader = errorObj.context?.headers?.['Retry-After'];
if (retryAfterHeader) {
const seconds = parseInt(retryAfterHeader, 10);
return isNaN(seconds) ? 3 : seconds;
}
return 3; // Default 3 seconds
}
/**
* Sleep for a specified duration
*
* @param ms - Milliseconds to sleep
*/
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}