/** * Idempotency Key Utilities * * Provides helper functions for generating and managing idempotency keys * for moderation operations to prevent duplicate requests. * * Integrated with idempotencyLifecycle.ts for full lifecycle tracking. */ import { registerIdempotencyKey, updateIdempotencyStatus, getIdempotencyRecord, isIdempotencyKeyValid, type IdempotencyRecord, } from './idempotencyLifecycle'; /** * 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 { return new Promise(resolve => setTimeout(resolve, ms)); } /** * Generate and register a new idempotency key with lifecycle tracking * * @param action - The moderation action type * @param submissionId - The submission ID * @param itemIds - Array of item IDs being processed * @param userId - The moderator's user ID * @returns Idempotency key and record */ export async function generateAndRegisterKey( action: 'approval' | 'rejection' | 'retry', submissionId: string, itemIds: string[], userId: string ): Promise<{ key: string; record: IdempotencyRecord }> { const key = generateIdempotencyKey(action, submissionId, itemIds, userId); const record = await registerIdempotencyKey(key, action, submissionId, itemIds, userId); return { key, record }; } /** * Validate and mark idempotency key as processing * * @param key - Idempotency key to validate * @returns True if valid and marked as processing */ export async function validateAndStartProcessing(key: string): Promise { const isValid = await isIdempotencyKeyValid(key); if (!isValid) { return false; } const record = await getIdempotencyRecord(key); // Only allow transition from pending to processing if (record?.status !== 'pending') { return false; } await updateIdempotencyStatus(key, 'processing'); return true; } /** * Mark idempotency key as completed */ export async function markKeyCompleted(key: string): Promise { await updateIdempotencyStatus(key, 'completed'); } /** * Mark idempotency key as failed */ export async function markKeyFailed(key: string, error: string): Promise { await updateIdempotencyStatus(key, 'failed', error); }