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:
gpt-engineer-app[bot]
2025-11-10 19:05:31 +00:00
parent 8ed5edbe24
commit 73e847015d
3 changed files with 381 additions and 35 deletions

View File

@@ -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) => {