Files
Matthieu MALVACHE ede603563e feat: v1.2.0 — sandboxed email rendering, mobile UX, sidebar polish, API retry
New features:
- Sandboxed iframe rendering for rich HTML emails (CSS isolation)
- Mobile bottom action bar (Reply, Archive, Delete, More)
- Long-press context menu with haptic feedback on touch devices
- Tag counts sidebar section with batch JMAP queries
- Empty folder for Junk/Trash with batch delete progress
- Extra-compact density option (28px desktop, 44px touch)
- SPF/DKIM/DMARC security tooltips with plain-language explanations
- Resizable sidebars with drag, touch, and keyboard support
- Expandable sender info panel in email viewer
- API retry with exponential backoff for transient JMAP failures
- OAuth-only mode (OAUTH_ONLY env var)

Improvements:
- CSS-first responsive layout (no blink on orientation change)
- Touch-friendly context menu submenus (tap-to-expand)
- Click-to-toggle more-actions dropdown (was hover-only)
- Wide HTML emails horizontally scrollable in iframe
- All 8 locales updated with new translation keys
2026-03-16 16:29:47 +01:00

51 lines
1.5 KiB
TypeScript

const RETRYABLE_STATUS_CODES = new Set([429, 502, 503, 504]);
interface RetryOptions {
maxRetries?: number;
baseDelay?: number;
signal?: AbortSignal;
}
export async function retryWithBackoff(
fn: () => Promise<Response>,
options: RetryOptions = {}
): Promise<Response> {
const { maxRetries = 3, baseDelay = 500, signal } = options;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
if (signal?.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
try {
const response = await fn();
if (attempt < maxRetries && RETRYABLE_STATUS_CODES.has(response.status)) {
let delay: number;
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After');
const retrySeconds = retryAfter ? parseInt(retryAfter, 10) : NaN;
delay = !isNaN(retrySeconds) ? retrySeconds * 1000 : baseDelay * Math.pow(2, attempt);
} else {
delay = baseDelay * Math.pow(2, attempt);
}
const jitter = delay * (0.8 + Math.random() * 0.4);
await new Promise(resolve => setTimeout(resolve, jitter));
continue;
}
return response;
} catch (error) {
if (error instanceof TypeError && attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt);
const jitter = delay * (0.8 + Math.random() * 0.4);
await new Promise(resolve => setTimeout(resolve, jitter));
continue;
}
throw error;
}
}
throw new Error('Retry loop exited unexpectedly');
}