// Classify an error thrown by a JMAP call as a *transient* connectivity // failure (server unreachable, offline, timed out, rate limited) versus a // *permanent* one (the server received the request and rejected it, e.g. a // method error or a 4xx). The offline outbox uses this to decide whether to // keep an operation queued for a later retry or to give up on it. // // Classification is by error class first. A bare `TypeError` used to count as // transient because RN's fetch throws `TypeError("Network request failed")`, // but the JMAP client now wraps transport failures in `NetworkError`; a // TypeError reaching here is far more likely a programming error (e.g. an // unchecked method-error response) and must not wedge the queue forever. // `AuthenticationError` is terminal: the credentials are gone, retrying won't // help until the user signs in again. import { useNetworkStore } from '../stores/network-store'; const TRANSIENT_NAMES = new Set([ 'NetworkError', // jmap-client transport wrapper 'RequestTimeoutError', // no response headers within the deadline 'RateLimitError', // 429 - retry after the window 'AbortError', // request aborted (e.g. app backgrounded) ]); const TERMINAL_NAMES = new Set([ 'AuthenticationError', 'TotpRequiredError', 'JMAPMethodError', ]); const TRANSIENT_MESSAGE_HINTS = [ 'network request failed', 'network error', 'failed to fetch', 'not connected', 'timeout', 'timed out', 'connection', ]; export function isTransientNetworkError(err: unknown): boolean { if (err instanceof Error && TERMINAL_NAMES.has(err.name)) return false; // If the device itself reports offline, treat any other failure as transient. if (!useNetworkStore.getState().online) return true; if (!(err instanceof Error)) return false; if (TRANSIENT_NAMES.has(err.name)) return true; // A raw fetch TypeError only counts when its message says "network". const msg = err.message?.toLowerCase() ?? ''; if (err.name === 'TypeError') { return msg.includes('network') || msg.includes('failed to fetch') || msg.includes('securefetch failed'); } return TRANSIENT_MESSAGE_HINTS.some((hint) => msg.includes(hint)); } /** True when the failure means the session/credentials are unusable. */ export function isAuthError(err: unknown): boolean { return err instanceof Error && (err.name === 'AuthenticationError' || err.name === 'TotpRequiredError'); }