mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 17:31:13 -05:00
Add rate limit aware retries
Enhance retry logic to detect 429 rate limits, parse Retry-After headers, and apply smart backoff across all entity submissions. Adds rate-limit-aware backoff, preserves user feedback via UI events, and ensures retries respect server-provided guidance.
This commit is contained in:
@@ -9,7 +9,7 @@ import { logger } from './logger';
|
||||
import { handleError } from './errorHandler';
|
||||
import type { TimelineEventFormData, EntityType } from '@/types/timeline';
|
||||
import { breadcrumb } from './errorBreadcrumbs';
|
||||
import { isRetryableError } from './retryHelpers';
|
||||
import { isRetryableError, isRateLimitError, extractRetryAfter } from './retryHelpers';
|
||||
import {
|
||||
validateParkCreateFields,
|
||||
validateRideCreateFields,
|
||||
@@ -886,11 +886,28 @@ export async function submitParkCreation(
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
logger.warn('Retrying park submission', { attempt, delay, error: error instanceof Error ? error.message : String(error) });
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
// Emit event for UI indicator
|
||||
logger.warn('Retrying park submission', {
|
||||
attempt,
|
||||
delay,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
|
||||
// Emit event for UI indicator with rate limit info
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { id: retryId, attempt, maxAttempts: 3, delay, type: 'park' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'park',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -1125,16 +1142,29 @@ export async function submitParkUpdate(
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying park update submission', {
|
||||
attempt,
|
||||
delay,
|
||||
parkId,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
|
||||
// Emit event for UI retry indicator
|
||||
// Emit event for UI retry indicator with rate limit info
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { id: retryId, attempt, maxAttempts: 3, delay, type: 'park update' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'park update',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -1529,15 +1559,28 @@ export async function submitRideCreation(
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying ride submission', {
|
||||
attempt,
|
||||
delay,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
|
||||
// Emit event for UI indicator
|
||||
// Emit event for UI indicator with rate limit info
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { id: retryId, attempt, maxAttempts: 3, delay, type: 'ride' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'ride',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -1747,16 +1790,29 @@ export async function submitRideUpdate(
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying ride update submission', {
|
||||
attempt,
|
||||
delay,
|
||||
rideId,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
|
||||
// Emit event for UI retry indicator
|
||||
// Emit event for UI retry indicator with rate limit info
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { id: retryId, attempt, maxAttempts: 3, delay, type: 'ride update' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'ride update',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -1966,13 +2022,26 @@ export async function submitRideModelCreation(
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying ride model submission', {
|
||||
attempt,
|
||||
delay,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { id: retryId, attempt, maxAttempts: 3, delay, type: 'ride_model' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'ride_model',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -2163,13 +2232,26 @@ export async function submitRideModelUpdate(
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying ride model update', {
|
||||
attempt,
|
||||
delay,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { id: retryId, attempt, maxAttempts: 3, delay, type: 'ride_model_update' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'ride_model_update',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -2310,13 +2392,26 @@ export async function submitManufacturerCreation(
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying manufacturer submission', {
|
||||
attempt,
|
||||
delay,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { id: retryId, attempt, maxAttempts: 3, delay, type: 'manufacturer' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'manufacturer',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -2409,6 +2504,8 @@ export async function submitManufacturerUpdate(
|
||||
|
||||
// Submit with retry logic
|
||||
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||
const retryId = crypto.randomUUID();
|
||||
|
||||
const result = await withRetry(
|
||||
async () => {
|
||||
const { data: submissionData, error: submissionError } = await supabase
|
||||
@@ -2446,10 +2543,28 @@ export async function submitManufacturerUpdate(
|
||||
},
|
||||
{
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
logger.warn('Retrying manufacturer update', { attempt, delay });
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying manufacturer update', {
|
||||
attempt,
|
||||
delay,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { attempt, maxAttempts: 3, delay, type: 'manufacturer_update' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'manufacturer_update',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -2520,6 +2635,8 @@ export async function submitDesignerCreation(
|
||||
|
||||
// Submit with retry logic
|
||||
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||
const retryId = crypto.randomUUID();
|
||||
|
||||
const result = await withRetry(
|
||||
async () => {
|
||||
const { data: submissionData, error: submissionError } = await supabase
|
||||
@@ -2559,10 +2676,28 @@ export async function submitDesignerCreation(
|
||||
},
|
||||
{
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
logger.warn('Retrying designer submission', { attempt, delay });
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying designer submission', {
|
||||
attempt,
|
||||
delay,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { attempt, maxAttempts: 3, delay, type: 'designer' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'designer',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -2633,6 +2768,8 @@ export async function submitDesignerUpdate(
|
||||
|
||||
// Submit with retry logic
|
||||
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||
const retryId = crypto.randomUUID();
|
||||
|
||||
const result = await withRetry(
|
||||
async () => {
|
||||
const { data: submissionData, error: submissionError } = await supabase
|
||||
@@ -2670,10 +2807,28 @@ export async function submitDesignerUpdate(
|
||||
},
|
||||
{
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
logger.warn('Retrying designer update', { attempt, delay });
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying designer update', {
|
||||
attempt,
|
||||
delay,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { attempt, maxAttempts: 3, delay, type: 'designer_update' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'designer_update',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -2925,10 +3080,28 @@ export async function submitOperatorUpdate(
|
||||
},
|
||||
{
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
logger.warn('Retrying operator update', { attempt, delay });
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying operator update', {
|
||||
attempt,
|
||||
delay,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { attempt, maxAttempts: 3, delay, type: 'operator_update' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'operator_update',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
@@ -3141,6 +3314,8 @@ export async function submitPropertyOwnerUpdate(
|
||||
|
||||
// Submit with retry logic
|
||||
breadcrumb.apiCall('content_submissions', 'INSERT');
|
||||
const retryId = crypto.randomUUID();
|
||||
|
||||
const result = await withRetry(
|
||||
async () => {
|
||||
const { data: submissionData, error: submissionError } = await supabase
|
||||
@@ -3178,10 +3353,28 @@ export async function submitPropertyOwnerUpdate(
|
||||
},
|
||||
{
|
||||
maxAttempts: 3,
|
||||
baseDelay: 1000,
|
||||
onRetry: (attempt, error, delay) => {
|
||||
logger.warn('Retrying property owner update', { attempt, delay });
|
||||
const isRateLimit = isRateLimitError(error);
|
||||
const retryAfter = isRateLimit ? extractRetryAfter(error) : null;
|
||||
|
||||
logger.warn('Retrying property owner update', {
|
||||
attempt,
|
||||
delay,
|
||||
isRateLimit,
|
||||
retryAfter,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('submission-retry', {
|
||||
detail: { attempt, maxAttempts: 3, delay, type: 'property_owner_update' }
|
||||
detail: {
|
||||
id: retryId,
|
||||
attempt,
|
||||
maxAttempts: 3,
|
||||
delay,
|
||||
type: 'property_owner_update',
|
||||
isRateLimit,
|
||||
retryAfter
|
||||
}
|
||||
}));
|
||||
},
|
||||
shouldRetry: (error) => {
|
||||
|
||||
Reference in New Issue
Block a user