mirror of
https://github.com/root-fr/jmap-webmail.git
synced 2026-09-23 00:01:13 +00:00
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
51 lines
1.5 KiB
TypeScript
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');
|
|
}
|