Complete rename of "swift" terminology to "cortex" across the codebase: Code Changes: - Rename directories: lib/swift → lib/cortex, components/swift → components/cortex - Rename API routes: /api/swift/* → /api/cortex/* - Rename page routes: /epd/swift → /epd/cortex - Rename store: swift-store.ts → cortex-store.ts Type Renames: - SwiftIntent → CortexIntent - SwiftStore → CortexStore - useSwiftStore → useCortexStore - SwiftContext → CortexContext - useSwiftVoice → useCortexVoice Documentation Updates (docs/intent/): - Safety Net → Nudge (Layer 3 rename) - SafetySuggestion → NudgeSuggestion - evaluateSafetyNet → evaluateNudge - SWIFT_* feature flags → CORTEX_* - All path references updated to match new structure Files affected: 47 files, ~700 lines changed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
301 lines
7.3 KiB
TypeScript
301 lines
7.3 KiB
TypeScript
/**
|
|
* Error Handler Utility voor Cortex
|
|
*
|
|
* E5.S2: Gecentraliseerde error handling met network detection,
|
|
* gebruiksvriendelijke berichten en retry logic.
|
|
*/
|
|
|
|
export interface ErrorContext {
|
|
operation: string;
|
|
endpoint?: string;
|
|
statusCode?: number;
|
|
retryable?: boolean;
|
|
}
|
|
|
|
export interface ErrorInfo {
|
|
title: string;
|
|
description: string;
|
|
retryable: boolean;
|
|
statusCode?: number;
|
|
}
|
|
|
|
/**
|
|
* Detecteert of de browser offline is
|
|
*/
|
|
export function isOffline(): boolean {
|
|
return typeof navigator !== 'undefined' && !navigator.onLine;
|
|
}
|
|
|
|
/**
|
|
* Controleert of een error een network error is
|
|
*/
|
|
export function isNetworkError(error: unknown): boolean {
|
|
if (error instanceof TypeError) {
|
|
return (
|
|
error.message.includes('fetch') ||
|
|
error.message.includes('network') ||
|
|
error.message.includes('Failed to fetch')
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Controleert of een error een timeout is
|
|
*/
|
|
export function isTimeoutError(error: unknown): boolean {
|
|
if (error instanceof Error) {
|
|
return error.message.toLowerCase().includes('timeout');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Parse HTTP error response en extraheert gebruiksvriendelijke berichten
|
|
*/
|
|
export async function parseErrorResponse(
|
|
response: Response
|
|
): Promise<{ error: string; details?: string }> {
|
|
try {
|
|
const errorText = await response.text();
|
|
|
|
// Check if response is HTML (likely redirect to login)
|
|
if (errorText.trim().startsWith('<!DOCTYPE') || errorText.trim().startsWith('<html')) {
|
|
return {
|
|
error: 'Niet geautoriseerd. Log opnieuw in.',
|
|
};
|
|
}
|
|
|
|
// Try to parse as JSON
|
|
try {
|
|
const errorData = JSON.parse(errorText);
|
|
return {
|
|
error: errorData.error || errorData.message || 'Onbekende fout',
|
|
details: errorData.details,
|
|
};
|
|
} catch {
|
|
// Not JSON, return text (truncated if too long)
|
|
return {
|
|
error: errorText.length > 200 ? errorText.substring(0, 200) + '...' : errorText,
|
|
};
|
|
}
|
|
} catch {
|
|
return {
|
|
error: 'Kon foutmelding niet lezen',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Genereert gebruiksvriendelijke error informatie op basis van error type en context
|
|
*/
|
|
export function getErrorInfo(error: unknown, context?: ErrorContext): ErrorInfo {
|
|
// Offline detection
|
|
if (isOffline()) {
|
|
return {
|
|
title: 'Geen internetverbinding',
|
|
description: 'Controleer je internetverbinding en probeer het opnieuw.',
|
|
retryable: true,
|
|
};
|
|
}
|
|
|
|
// Network errors (fetch failures)
|
|
if (isNetworkError(error)) {
|
|
return {
|
|
title: 'Verbinding verbroken',
|
|
description: 'Kon geen verbinding maken met de server. Probeer het opnieuw.',
|
|
retryable: true,
|
|
};
|
|
}
|
|
|
|
// Timeout errors
|
|
if (isTimeoutError(error)) {
|
|
return {
|
|
title: 'Verbinding timeout',
|
|
description: 'De verbinding duurde te lang. Probeer het opnieuw.',
|
|
retryable: true,
|
|
};
|
|
}
|
|
|
|
// HTTP status codes
|
|
if (context?.statusCode) {
|
|
switch (context.statusCode) {
|
|
case 401:
|
|
return {
|
|
title: 'Niet geautoriseerd',
|
|
description: 'Je sessie is verlopen. Log opnieuw in.',
|
|
retryable: false,
|
|
statusCode: 401,
|
|
};
|
|
case 403:
|
|
return {
|
|
title: 'Geen toegang',
|
|
description: 'Je hebt geen toegang tot deze actie.',
|
|
retryable: false,
|
|
statusCode: 403,
|
|
};
|
|
case 404:
|
|
return {
|
|
title: 'Niet gevonden',
|
|
description: context.operation
|
|
? `${context.operation} niet gevonden.`
|
|
: 'De gevraagde resource bestaat niet.',
|
|
retryable: false,
|
|
statusCode: 404,
|
|
};
|
|
case 400:
|
|
return {
|
|
title: 'Ongeldige aanvraag',
|
|
description: error instanceof Error ? error.message : 'Controleer je invoer en probeer het opnieuw.',
|
|
retryable: false,
|
|
statusCode: 400,
|
|
};
|
|
case 429:
|
|
return {
|
|
title: 'Te veel aanvragen',
|
|
description: 'Je hebt te veel aanvragen gedaan. Wacht even en probeer het later opnieuw.',
|
|
retryable: true,
|
|
statusCode: 429,
|
|
};
|
|
case 500:
|
|
return {
|
|
title: 'Serverfout',
|
|
description: 'Er ging iets mis op de server. Probeer het later opnieuw.',
|
|
retryable: true,
|
|
statusCode: 500,
|
|
};
|
|
case 503:
|
|
return {
|
|
title: 'Service niet beschikbaar',
|
|
description: 'De service is tijdelijk niet beschikbaar. Probeer het later opnieuw.',
|
|
retryable: true,
|
|
statusCode: 503,
|
|
};
|
|
default:
|
|
return {
|
|
title: 'Fout opgetreden',
|
|
description: error instanceof Error ? error.message : `HTTP ${context.statusCode}`,
|
|
retryable: context.statusCode >= 500,
|
|
statusCode: context.statusCode,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Generic error
|
|
if (error instanceof Error) {
|
|
// Check for specific error messages
|
|
if (error.message.includes('Niet geautoriseerd') || error.message.includes('Log opnieuw in')) {
|
|
return {
|
|
title: 'Niet geautoriseerd',
|
|
description: error.message,
|
|
retryable: false,
|
|
};
|
|
}
|
|
|
|
if (error.message.includes('Validatiefout') || error.message.includes('validatie')) {
|
|
return {
|
|
title: 'Validatiefout',
|
|
description: error.message,
|
|
retryable: false,
|
|
};
|
|
}
|
|
|
|
return {
|
|
title: 'Fout opgetreden',
|
|
description: error.message,
|
|
retryable: true,
|
|
};
|
|
}
|
|
|
|
// Unknown error
|
|
return {
|
|
title: 'Onbekende fout',
|
|
description: 'Er ging iets mis. Probeer het opnieuw.',
|
|
retryable: true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Wrapper voor fetch met verbeterde error handling
|
|
*/
|
|
export async function safeFetch(
|
|
url: string,
|
|
options?: RequestInit,
|
|
context?: Omit<ErrorContext, 'statusCode'>
|
|
): Promise<Response> {
|
|
// Check offline first
|
|
if (isOffline()) {
|
|
throw new Error('Geen internetverbinding');
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
// Add timeout (30 seconds)
|
|
signal: AbortSignal.timeout(30000),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await parseErrorResponse(response);
|
|
const error = new Error(errorData.error);
|
|
(error as any).statusCode = response.status;
|
|
(error as any).details = errorData.details;
|
|
throw error;
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
// Re-throw with context if it's already an Error with statusCode
|
|
if (error instanceof Error && (error as any).statusCode) {
|
|
throw error;
|
|
}
|
|
|
|
// Wrap network errors
|
|
if (isNetworkError(error) || isTimeoutError(error)) {
|
|
throw error;
|
|
}
|
|
|
|
// Re-throw as-is
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retry logic voor retryable errors
|
|
*/
|
|
export async function retryFetch<T>(
|
|
fn: () => Promise<T>,
|
|
maxRetries: number = 3,
|
|
delayMs: number = 1000
|
|
): Promise<T> {
|
|
let lastError: unknown;
|
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
lastError = error;
|
|
const errorInfo = getErrorInfo(error);
|
|
|
|
// Don't retry if not retryable
|
|
if (!errorInfo.retryable) {
|
|
throw error;
|
|
}
|
|
|
|
// Don't retry on last attempt
|
|
if (attempt === maxRetries - 1) {
|
|
throw error;
|
|
}
|
|
|
|
// Wait before retrying (exponential backoff)
|
|
await new Promise((resolve) => setTimeout(resolve, delayMs * (attempt + 1)));
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|
|
|
|
|
|
|
|
|