@umituz/react-native-design-system 4.23.114 → 4.23.116

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-design-system",
3
- "version": "4.23.114",
3
+ "version": "4.23.116",
4
4
  "description": "Universal design system for React Native apps - Consolidated package with atoms, molecules, organisms, theme, typography, responsive, safe area, exception, infinite scroll, UUID, image, timezone, offline, onboarding, and loading utilities",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { Directory, Paths } from "expo-file-system";
7
7
  import type { DirectoryType } from "../../domain/entities/File";
8
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
8
9
 
9
10
  /**
10
11
  * Create directory
@@ -14,8 +15,17 @@ export async function createDirectory(uri: string): Promise<boolean> {
14
15
  const dir = new Directory(uri);
15
16
  await dir.create({ intermediates: true, idempotent: true });
16
17
  return true;
17
- } catch {
18
- return false;
18
+ } catch (error) {
19
+ const handled = ErrorHandler.handleAndLog(
20
+ error,
21
+ 'createDirectory',
22
+ { uri }
23
+ );
24
+ throw ErrorHandler.create(
25
+ `Failed to create directory: ${handled.message}`,
26
+ ErrorCodes.DIRECTORY_CREATE_ERROR,
27
+ { uri, originalError: handled }
28
+ );
19
29
  }
20
30
  }
21
31
 
@@ -27,8 +37,17 @@ export async function listDirectory(uri: string): Promise<string[]> {
27
37
  const dir = new Directory(uri);
28
38
  const items = await dir.list();
29
39
  return items.map((item) => item.uri);
30
- } catch {
31
- return [];
40
+ } catch (error) {
41
+ const handled = ErrorHandler.handleAndLog(
42
+ error,
43
+ 'listDirectory',
44
+ { uri }
45
+ );
46
+ throw ErrorHandler.create(
47
+ `Failed to list directory contents: ${handled.message}`,
48
+ ErrorCodes.FILE_READ_ERROR,
49
+ { uri, originalError: handled }
50
+ );
32
51
  }
33
52
  }
34
53
 
@@ -43,10 +62,19 @@ export function getDirectoryPath(type: DirectoryType): string {
43
62
  case "cacheDirectory":
44
63
  return Paths.cache.uri;
45
64
  default:
46
- return "";
65
+ throw ErrorHandler.create(
66
+ `Unknown directory type: ${type}`,
67
+ ErrorCodes.INVALID_INPUT,
68
+ { type }
69
+ );
47
70
  }
48
- } catch {
49
- return "";
71
+ } catch (error) {
72
+ const handled = ErrorHandler.handleAndLog(
73
+ error,
74
+ 'getDirectoryPath',
75
+ { type }
76
+ );
77
+ throw handled;
50
78
  }
51
79
  }
52
80
 
@@ -7,6 +7,8 @@ import type { FileOperationResult } from "../../domain/entities/File";
7
7
  import { FileUtils } from "../../domain/entities/File";
8
8
  import { SUPPORTED_DOWNLOAD_EXTENSIONS, DEFAULT_DOWNLOAD_EXTENSION } from "./download.constants";
9
9
  import type { DownloadProgressCallback, DownloadWithProgressResult } from "./download.types";
10
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
11
+ import { retryWithBackoff, isNetworkError } from "../../../utils/async";
10
12
 
11
13
  const hashUrl = (url: string) => {
12
14
  let hash = 0;
@@ -25,9 +27,29 @@ const getCacheUri = (url: string, dir: string) => FileUtils.joinPaths(dir, `cach
25
27
  export async function downloadFile(url: string, dest?: string): Promise<FileOperationResult> {
26
28
  try {
27
29
  const destination = dest ? new File(dest) : new File(Paths.document, FileUtils.generateUniqueFilename("download"));
28
- const res = await File.downloadFileAsync(url, destination, { idempotent: true });
30
+
31
+ // Retry download with exponential backoff
32
+ const res = await retryWithBackoff(
33
+ () => File.downloadFileAsync(url, destination, { idempotent: true }),
34
+ {
35
+ maxRetries: 3,
36
+ baseDelay: 1000,
37
+ shouldRetry: (error) => isNetworkError(error as Error),
38
+ }
39
+ );
40
+
29
41
  return { success: true, uri: res.uri };
30
- } catch (e: unknown) { return { success: false, error: e instanceof Error ? e.message : String(e) }; }
42
+ } catch (error) {
43
+ const handled = ErrorHandler.handleAndLog(
44
+ error,
45
+ 'downloadFile',
46
+ { url, dest }
47
+ );
48
+ return {
49
+ success: false,
50
+ error: handled.getUserMessage(),
51
+ };
52
+ }
31
53
  }
32
54
 
33
55
  export async function downloadFileWithProgress(
@@ -44,7 +66,13 @@ export async function downloadFileWithProgress(
44
66
  if (new File(destUri).exists) return { success: true, uri: destUri, fromCache: true };
45
67
 
46
68
  const response = await fetch(url, { signal });
47
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
69
+ if (!response.ok) {
70
+ throw ErrorHandler.create(
71
+ `HTTP ${response.status}: ${response.statusText}`,
72
+ ErrorCodes.NETWORK_ERROR,
73
+ { url, status: response.status }
74
+ );
75
+ }
48
76
 
49
77
  const totalBytes = parseInt(response.headers.get("content-length") || "0", 10);
50
78
  if (!response.body) return { ...(await downloadFile(url, destUri)), fromCache: false };
@@ -57,7 +85,11 @@ export async function downloadFileWithProgress(
57
85
  while (true) {
58
86
  if (signal?.aborted) {
59
87
  await reader.cancel();
60
- throw new Error("Download aborted");
88
+ throw ErrorHandler.create(
89
+ 'Download aborted by user',
90
+ ErrorCodes.NETWORK_ERROR,
91
+ { url }
92
+ );
61
93
  }
62
94
 
63
95
  const { done, value } = await reader.read();
@@ -76,13 +108,32 @@ export async function downloadFileWithProgress(
76
108
  } finally {
77
109
  reader.releaseLock();
78
110
  }
79
- } catch (e: unknown) { return { success: false, error: e instanceof Error ? e.message : String(e) }; }
111
+ } catch (error) {
112
+ const handled = ErrorHandler.handleAndLog(
113
+ error,
114
+ 'downloadFileWithProgress',
115
+ { url, cacheDir }
116
+ );
117
+ return {
118
+ success: false,
119
+ error: handled.getUserMessage(),
120
+ };
121
+ }
80
122
  }
81
123
 
82
124
  export const isUrlCached = (url: string, dir: string) => new File(getCacheUri(url, dir)).exists;
83
125
  export const getCachedFileUri = (url: string, dir: string) => isUrlCached(url, dir) ? getCacheUri(url, dir) : null;
84
- export const deleteCachedFile = async (url: string, dir: string) => {
85
- const f = new File(getCacheUri(url, dir));
86
- if (f.exists) await f.delete();
87
- return true;
126
+ export const deleteCachedFile = async (url: string, dir: string): Promise<boolean> => {
127
+ try {
128
+ const f = new File(getCacheUri(url, dir));
129
+ if (f.exists) await f.delete();
130
+ return true;
131
+ } catch (error) {
132
+ ErrorHandler.handleAndLog(
133
+ error,
134
+ 'deleteCachedFile',
135
+ { url, dir }
136
+ );
137
+ return false;
138
+ }
88
139
  };
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { File, Directory } from "expo-file-system";
7
7
  import type { FileOperationResult } from "../../domain/entities/File";
8
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
8
9
 
9
10
  /**
10
11
  * Delete file or directory
@@ -18,8 +19,11 @@ export async function deleteFile(uri: string): Promise<boolean> {
18
19
  await file.delete();
19
20
  return true;
20
21
  }
21
- } catch {
22
+ } catch (fileError) {
22
23
  // Not a file, try as directory
24
+ if (__DEV__) {
25
+ console.log('[deleteFile] Not a file, trying as directory:', uri);
26
+ }
23
27
  }
24
28
 
25
29
  // Try as directory
@@ -29,13 +33,30 @@ export async function deleteFile(uri: string): Promise<boolean> {
29
33
  await dir.delete();
30
34
  return true;
31
35
  }
32
- } catch {
36
+ } catch (dirError) {
33
37
  // Not a directory either
38
+ if (__DEV__) {
39
+ console.log('[deleteFile] Not a directory:', uri);
40
+ }
34
41
  }
35
42
 
36
- return false;
37
- } catch {
38
- return false;
43
+ // File/directory doesn't exist
44
+ throw ErrorHandler.create(
45
+ `File or directory not found: ${uri}`,
46
+ ErrorCodes.FILE_NOT_FOUND,
47
+ { uri }
48
+ );
49
+ } catch (error) {
50
+ const handled = ErrorHandler.handleAndLog(
51
+ error,
52
+ 'deleteFile',
53
+ { uri }
54
+ );
55
+ throw ErrorHandler.create(
56
+ `Failed to delete file: ${handled.message}`,
57
+ ErrorCodes.FILE_DELETE_ERROR,
58
+ { uri, originalError: handled }
59
+ );
39
60
  }
40
61
  }
41
62
 
@@ -52,9 +73,14 @@ export async function copyFile(
52
73
  await sourceFile.copy(destination);
53
74
  return { success: true, uri: destinationUri };
54
75
  } catch (error) {
76
+ const handled = ErrorHandler.handleAndLog(
77
+ error,
78
+ 'copyFile',
79
+ { sourceUri, destinationUri }
80
+ );
55
81
  return {
56
82
  success: false,
57
- error: error instanceof Error ? error.message : "Unknown error",
83
+ error: handled.getUserMessage(),
58
84
  };
59
85
  }
60
86
  }
@@ -72,9 +98,14 @@ export async function moveFile(
72
98
  await sourceFile.move(destination);
73
99
  return { success: true, uri: destinationUri };
74
100
  } catch (error) {
101
+ const handled = ErrorHandler.handleAndLog(
102
+ error,
103
+ 'moveFile',
104
+ { sourceUri, destinationUri }
105
+ );
75
106
  return {
76
107
  success: false,
77
- error: error instanceof Error ? error.message : "Unknown error",
108
+ error: handled.getUserMessage(),
78
109
  };
79
110
  }
80
111
  }
@@ -6,6 +6,7 @@
6
6
  import { File } from "expo-file-system";
7
7
  import type { FileEncoding, FileOperationResult } from "../../domain/entities/File";
8
8
  import { getEncodingType, type ExpoEncodingType } from "./encoding.service";
9
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
9
10
 
10
11
  /**
11
12
  * Write string to file
@@ -23,10 +24,14 @@ export async function writeFile(
23
24
  });
24
25
  return { success: true, uri };
25
26
  } catch (error) {
26
- const writeError = error as Error;
27
+ const handled = ErrorHandler.handleAndLog(
28
+ error,
29
+ 'writeFile',
30
+ { uri, encoding, contentLength: content.length }
31
+ );
27
32
  return {
28
33
  success: false,
29
- error: writeError.message || "Unknown error",
34
+ error: handled.getUserMessage(),
30
35
  };
31
36
  }
32
37
  }
@@ -22,6 +22,7 @@ import {
22
22
  } from "../utils/mediaPickerMappers";
23
23
  import { PermissionManager } from "../utils/PermissionManager";
24
24
  import { FileValidator } from "../../domain/utils/FileValidator";
25
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
25
26
 
26
27
  /**
27
28
  * Media picker service for selecting images/videos
@@ -33,7 +34,11 @@ export class MediaPickerService {
33
34
  try {
34
35
  const permission = await PermissionManager.requestCameraPermission();
35
36
  if (!PermissionManager.isPermissionGranted(permission)) {
36
- return { canceled: true };
37
+ return {
38
+ canceled: true,
39
+ error: MediaValidationError.PERMISSION_DENIED,
40
+ errorMessage: "Camera permission was denied",
41
+ };
37
42
  }
38
43
 
39
44
  const result = await ImagePicker.launchCameraAsync({
@@ -45,8 +50,13 @@ export class MediaPickerService {
45
50
  });
46
51
 
47
52
  return mapPickerResult(result);
48
- } catch {
49
- return { canceled: true };
53
+ } catch (error) {
54
+ ErrorHandler.handleAndLog(error, 'launchCamera', { options });
55
+ return {
56
+ canceled: true,
57
+ error: MediaValidationError.PICKER_ERROR,
58
+ errorMessage: "Failed to launch camera",
59
+ };
50
60
  }
51
61
  }
52
62
 
@@ -56,7 +66,11 @@ export class MediaPickerService {
56
66
  try {
57
67
  const permission = await PermissionManager.requestCameraPermission();
58
68
  if (!PermissionManager.isPermissionGranted(permission)) {
59
- return { canceled: true };
69
+ return {
70
+ canceled: true,
71
+ error: MediaValidationError.PERMISSION_DENIED,
72
+ errorMessage: "Camera permission was denied",
73
+ };
60
74
  }
61
75
 
62
76
  const result = await ImagePicker.launchCameraAsync({
@@ -67,8 +81,13 @@ export class MediaPickerService {
67
81
  });
68
82
 
69
83
  return mapPickerResult(result);
70
- } catch {
71
- return { canceled: true };
84
+ } catch (error) {
85
+ ErrorHandler.handleAndLog(error, 'launchCameraForVideo', { options });
86
+ return {
87
+ canceled: true,
88
+ error: MediaValidationError.PICKER_ERROR,
89
+ errorMessage: "Failed to launch camera for video",
90
+ };
72
91
  }
73
92
  }
74
93
 
@@ -114,8 +133,13 @@ export class MediaPickerService {
114
133
  }
115
134
 
116
135
  return mappedResult;
117
- } catch {
118
- return { canceled: true };
136
+ } catch (error) {
137
+ ErrorHandler.handleAndLog(error, 'pickImage', { options });
138
+ return {
139
+ canceled: true,
140
+ error: MediaValidationError.PICKER_ERROR,
141
+ errorMessage: "Failed to pick image from library",
142
+ };
119
143
  }
120
144
  }
121
145
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  import * as FileSystem from "expo-file-system";
7
7
  import { MediaLibraryPermission } from "../../domain/entities/Media";
8
+ import { ErrorHandler, ErrorCodes } from "../../../utils/errors";
8
9
 
9
10
  export interface SaveResult {
10
11
  success: boolean;
@@ -87,10 +88,14 @@ export class MediaSaveService {
87
88
  path: destination,
88
89
  };
89
90
  } catch (error) {
90
- const message = error instanceof Error ? error.message : "Unknown error";
91
+ const handled = ErrorHandler.handleAndLog(
92
+ error,
93
+ 'saveToStorage',
94
+ { uri, mediaType }
95
+ );
91
96
  return {
92
97
  success: false,
93
- error: `Failed to save media: ${message}`,
98
+ error: `Failed to save media: ${handled.getUserMessage()}`,
94
99
  };
95
100
  }
96
101
  }
@@ -20,6 +20,8 @@ import {
20
20
  isSuccessfulResponse,
21
21
  fetchWithTimeout,
22
22
  } from './utils/responseHandler';
23
+ import { retryWithBackoff, isNetworkError, isRetryableHttpStatus } from '../../utils/async';
24
+ import { ErrorHandler } from '../../utils/errors';
23
25
 
24
26
  /**
25
27
  * Applies interceptors to a value
@@ -46,7 +48,7 @@ export class ApiClient {
46
48
  }
47
49
 
48
50
  /**
49
- * Makes an HTTP request
51
+ * Makes an HTTP request with automatic retry for retryable errors
50
52
  */
51
53
  async request<T>(requestConfig: ApiRequestConfig): Promise<ApiResponse<T>> {
52
54
  try {
@@ -61,11 +63,39 @@ export class ApiClient {
61
63
  headers: { ...this.config.headers, ...config.headers },
62
64
  });
63
65
 
64
- const response = await fetchWithTimeout(
65
- fullURL,
66
- fetchOptions,
67
- config.timeout || this.config.timeout || 30000
68
- );
66
+ // Retry only for GET requests and retryable errors
67
+ const shouldRetry = config.method === 'GET';
68
+ const timeout = config.timeout || this.config.timeout || 30000;
69
+
70
+ const response = shouldRetry
71
+ ? await retryWithBackoff(
72
+ () => fetchWithTimeout(fullURL, fetchOptions, timeout),
73
+ {
74
+ maxRetries: 3,
75
+ baseDelay: 1000,
76
+ shouldRetry: (error) => {
77
+ // Retry on network errors
78
+ if (isNetworkError(error as Error)) return true;
79
+
80
+ // Retry on specific HTTP status codes (5xx, 429, 408)
81
+ if ('status' in error && typeof error.status === 'number') {
82
+ return isRetryableHttpStatus(error.status);
83
+ }
84
+
85
+ return false;
86
+ },
87
+ onRetry: (error, attempt, delay) => {
88
+ if (__DEV__) {
89
+ ErrorHandler.log({
90
+ name: 'ApiRetry',
91
+ message: `Retrying API request (attempt ${attempt}) after ${delay}ms`,
92
+ context: { url: fullURL, error: error.message },
93
+ });
94
+ }
95
+ },
96
+ }
97
+ )
98
+ : await fetchWithTimeout(fullURL, fetchOptions, timeout);
69
99
 
70
100
  if (!isSuccessfulResponse(response)) {
71
101
  const error = await handleHttpError(response);
@@ -78,6 +108,7 @@ export class ApiClient {
78
108
  return parsedResponse;
79
109
  } catch (error) {
80
110
  const apiError = handleNetworkError(error);
111
+ ErrorHandler.log(apiError);
81
112
  throw await applyInterceptors(apiError, this.config.errorInterceptors);
82
113
  }
83
114
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Async Utilities
3
+ * Retry and timeout utilities for async operations
4
+ */
5
+
6
+ export {
7
+ retryWithBackoff,
8
+ retryWithTimeout,
9
+ isNetworkError,
10
+ isRetryableHttpStatus,
11
+ type RetryOptions,
12
+ } from './retryWithBackoff';
@@ -0,0 +1,177 @@
1
+ /**
2
+ * retryWithBackoff
3
+ *
4
+ * Retry utility with exponential backoff for async operations.
5
+ * Useful for network requests, file operations, etc.
6
+ */
7
+
8
+ import { ErrorHandler } from '../errors/ErrorHandler';
9
+
10
+ export interface RetryOptions {
11
+ /**
12
+ * Maximum number of retry attempts
13
+ * @default 3
14
+ */
15
+ maxRetries?: number;
16
+
17
+ /**
18
+ * Initial delay in milliseconds before first retry
19
+ * @default 1000
20
+ */
21
+ baseDelay?: number;
22
+
23
+ /**
24
+ * Maximum delay in milliseconds (caps exponential growth)
25
+ * @default 10000
26
+ */
27
+ maxDelay?: number;
28
+
29
+ /**
30
+ * Multiplier for exponential backoff
31
+ * @default 2
32
+ */
33
+ backoffMultiplier?: number;
34
+
35
+ /**
36
+ * Function to determine if error is retryable
37
+ * @default () => true (retry all errors)
38
+ */
39
+ shouldRetry?: (error: Error, attempt: number) => boolean;
40
+
41
+ /**
42
+ * Callback on each retry attempt
43
+ */
44
+ onRetry?: (error: Error, attempt: number, delay: number) => void;
45
+ }
46
+
47
+ /**
48
+ * Retry an async function with exponential backoff
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * const result = await retryWithBackoff(
53
+ * () => fetch('https://api.example.com'),
54
+ * { maxRetries: 3, baseDelay: 1000 }
55
+ * );
56
+ * ```
57
+ */
58
+ export async function retryWithBackoff<T>(
59
+ fn: () => Promise<T>,
60
+ options: RetryOptions = {}
61
+ ): Promise<T> {
62
+ const {
63
+ maxRetries = 3,
64
+ baseDelay = 1000,
65
+ maxDelay = 10000,
66
+ backoffMultiplier = 2,
67
+ shouldRetry = () => true,
68
+ onRetry,
69
+ } = options;
70
+
71
+ let lastError: Error;
72
+
73
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
74
+ try {
75
+ // Attempt the operation
76
+ return await fn();
77
+ } catch (error) {
78
+ lastError = error instanceof Error ? error : new Error(String(error));
79
+
80
+ // Check if we should retry
81
+ const isLastAttempt = attempt === maxRetries;
82
+ if (isLastAttempt || !shouldRetry(lastError, attempt)) {
83
+ throw lastError;
84
+ }
85
+
86
+ // Calculate delay with exponential backoff
87
+ const delay = Math.min(
88
+ baseDelay * Math.pow(backoffMultiplier, attempt),
89
+ maxDelay
90
+ );
91
+
92
+ // Call onRetry callback if provided
93
+ if (onRetry) {
94
+ onRetry(lastError, attempt + 1, delay);
95
+ }
96
+
97
+ // Log retry in development
98
+ if (__DEV__) {
99
+ console.log(
100
+ `[Retry] Attempt ${attempt + 1}/${maxRetries} failed. Retrying in ${delay}ms...`,
101
+ lastError.message
102
+ );
103
+ }
104
+
105
+ // Wait before retrying
106
+ await new Promise((resolve) => setTimeout(resolve, delay));
107
+ }
108
+ }
109
+
110
+ // This should never be reached, but TypeScript needs it
111
+ throw lastError!;
112
+ }
113
+
114
+ /**
115
+ * Retry with timeout
116
+ * Combines retry logic with a timeout
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * const result = await retryWithTimeout(
121
+ * () => fetch('https://api.example.com'),
122
+ * { timeout: 5000, maxRetries: 3 }
123
+ * );
124
+ * ```
125
+ */
126
+ export async function retryWithTimeout<T>(
127
+ fn: () => Promise<T>,
128
+ options: RetryOptions & { timeout?: number } = {}
129
+ ): Promise<T> {
130
+ const { timeout = 30000, ...retryOptions } = options;
131
+
132
+ return retryWithBackoff(
133
+ () => withTimeout(fn(), timeout),
134
+ retryOptions
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Add timeout to a promise
140
+ */
141
+ function withTimeout<T>(
142
+ promise: Promise<T>,
143
+ timeoutMs: number
144
+ ): Promise<T> {
145
+ return Promise.race([
146
+ promise,
147
+ new Promise<T>((_, reject) =>
148
+ setTimeout(
149
+ () => reject(new Error(`Operation timed out after ${timeoutMs}ms`)),
150
+ timeoutMs
151
+ )
152
+ ),
153
+ ]);
154
+ }
155
+
156
+ /**
157
+ * Check if error is a network error
158
+ * Useful for shouldRetry callback
159
+ */
160
+ export function isNetworkError(error: Error): boolean {
161
+ return (
162
+ error.message.includes('network') ||
163
+ error.message.includes('timeout') ||
164
+ error.message.includes('fetch') ||
165
+ error.message.includes('ECONNREFUSED') ||
166
+ error.message.includes('ETIMEDOUT')
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Check if error is retryable HTTP status
172
+ * Useful for shouldRetry callback
173
+ */
174
+ export function isRetryableHttpStatus(status: number): boolean {
175
+ // Retry on 5xx server errors and 429 (rate limit)
176
+ return status >= 500 || status === 429 || status === 408;
177
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * DesignSystemError
3
+ *
4
+ * Unified error class for the design system package.
5
+ * Provides consistent error handling with error codes and context.
6
+ */
7
+
8
+ export class DesignSystemError extends Error {
9
+ /**
10
+ * Error code for categorization
11
+ */
12
+ public readonly code: string;
13
+
14
+ /**
15
+ * Additional context about the error
16
+ */
17
+ public readonly context?: Record<string, any>;
18
+
19
+ /**
20
+ * Timestamp when error was created
21
+ */
22
+ public readonly timestamp: Date;
23
+
24
+ constructor(
25
+ message: string,
26
+ code: string,
27
+ context?: Record<string, any>
28
+ ) {
29
+ super(message);
30
+ this.name = 'DesignSystemError';
31
+ this.code = code;
32
+ this.context = context;
33
+ this.timestamp = new Date();
34
+
35
+ // Maintains proper stack trace for where our error was thrown (only available on V8)
36
+ if (Error.captureStackTrace) {
37
+ Error.captureStackTrace(this, DesignSystemError);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Convert error to JSON for logging/debugging
43
+ */
44
+ toJSON(): Record<string, any> {
45
+ return {
46
+ name: this.name,
47
+ message: this.message,
48
+ code: this.code,
49
+ context: this.context,
50
+ timestamp: this.timestamp.toISOString(),
51
+ stack: this.stack,
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Get user-friendly error message
57
+ */
58
+ getUserMessage(): string {
59
+ // You can customize user-facing messages based on error codes
60
+ switch (this.code) {
61
+ case 'FILE_NOT_FOUND':
62
+ return 'The requested file could not be found.';
63
+ case 'PERMISSION_DENIED':
64
+ return 'Permission denied. Please check app permissions.';
65
+ case 'NETWORK_ERROR':
66
+ return 'Network error. Please check your connection.';
67
+ case 'STORAGE_FULL':
68
+ return 'Storage is full. Please free up some space.';
69
+ case 'INVALID_INPUT':
70
+ return 'Invalid input provided.';
71
+ default:
72
+ return this.message;
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Common error codes used across the design system
79
+ */
80
+ export const ErrorCodes = {
81
+ // File system errors
82
+ FILE_NOT_FOUND: 'FILE_NOT_FOUND',
83
+ FILE_READ_ERROR: 'FILE_READ_ERROR',
84
+ FILE_WRITE_ERROR: 'FILE_WRITE_ERROR',
85
+ FILE_DELETE_ERROR: 'FILE_DELETE_ERROR',
86
+ DIRECTORY_CREATE_ERROR: 'DIRECTORY_CREATE_ERROR',
87
+ PERMISSION_DENIED: 'PERMISSION_DENIED',
88
+ STORAGE_FULL: 'STORAGE_FULL',
89
+
90
+ // Network errors
91
+ NETWORK_ERROR: 'NETWORK_ERROR',
92
+ TIMEOUT_ERROR: 'TIMEOUT_ERROR',
93
+ API_ERROR: 'API_ERROR',
94
+
95
+ // Media errors
96
+ MEDIA_PICKER_ERROR: 'MEDIA_PICKER_ERROR',
97
+ MEDIA_SAVE_ERROR: 'MEDIA_SAVE_ERROR',
98
+ IMAGE_LOAD_ERROR: 'IMAGE_LOAD_ERROR',
99
+
100
+ // Storage errors
101
+ CACHE_ERROR: 'CACHE_ERROR',
102
+ STORAGE_ERROR: 'STORAGE_ERROR',
103
+
104
+ // Validation errors
105
+ INVALID_INPUT: 'INVALID_INPUT',
106
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
107
+
108
+ // Theme errors
109
+ THEME_LOAD_ERROR: 'THEME_LOAD_ERROR',
110
+ THEME_SAVE_ERROR: 'THEME_SAVE_ERROR',
111
+
112
+ // Generic errors
113
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
114
+ INITIALIZATION_ERROR: 'INITIALIZATION_ERROR',
115
+ } as const;
116
+
117
+ export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
@@ -0,0 +1,137 @@
1
+ /**
2
+ * ErrorHandler
3
+ *
4
+ * Centralized error handling utility for the design system.
5
+ * Provides consistent error handling, logging, and reporting.
6
+ */
7
+
8
+ import { DesignSystemError, ErrorCodes } from './DesignSystemError';
9
+
10
+ export class ErrorHandler {
11
+ /**
12
+ * Handle and normalize errors
13
+ * Converts any error type to DesignSystemError
14
+ */
15
+ static handle(
16
+ error: unknown,
17
+ context?: string,
18
+ additionalContext?: Record<string, any>
19
+ ): DesignSystemError {
20
+ // Already a DesignSystemError, return as-is
21
+ if (error instanceof DesignSystemError) {
22
+ return error;
23
+ }
24
+
25
+ // Standard Error object
26
+ if (error instanceof Error) {
27
+ return new DesignSystemError(
28
+ error.message,
29
+ ErrorCodes.UNKNOWN_ERROR,
30
+ {
31
+ context,
32
+ originalError: error.name,
33
+ stack: error.stack,
34
+ ...additionalContext,
35
+ }
36
+ );
37
+ }
38
+
39
+ // String error
40
+ if (typeof error === 'string') {
41
+ return new DesignSystemError(
42
+ error,
43
+ ErrorCodes.UNKNOWN_ERROR,
44
+ { context, ...additionalContext }
45
+ );
46
+ }
47
+
48
+ // Unknown error type
49
+ return new DesignSystemError(
50
+ 'An unknown error occurred',
51
+ ErrorCodes.UNKNOWN_ERROR,
52
+ {
53
+ context,
54
+ originalError: String(error),
55
+ ...additionalContext,
56
+ }
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Log error to console (only in development)
62
+ */
63
+ static log(error: DesignSystemError | Error | unknown): void {
64
+ if (__DEV__) {
65
+ if (error instanceof DesignSystemError) {
66
+ console.error('[DesignSystemError]', error.toJSON());
67
+ } else if (error instanceof Error) {
68
+ console.error('[Error]', {
69
+ name: error.name,
70
+ message: error.message,
71
+ stack: error.stack,
72
+ });
73
+ } else {
74
+ console.error('[Unknown Error]', error);
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Handle error with logging
81
+ * Combines handle() and log() for convenience
82
+ */
83
+ static handleAndLog(
84
+ error: unknown,
85
+ context?: string,
86
+ additionalContext?: Record<string, any>
87
+ ): DesignSystemError {
88
+ const handled = this.handle(error, context, additionalContext);
89
+ this.log(handled);
90
+ return handled;
91
+ }
92
+
93
+ /**
94
+ * Create a new DesignSystemError with specific code
95
+ */
96
+ static create(
97
+ message: string,
98
+ code: string,
99
+ context?: Record<string, any>
100
+ ): DesignSystemError {
101
+ return new DesignSystemError(message, code, context);
102
+ }
103
+
104
+ /**
105
+ * Wrap async function with error handling
106
+ * Returns [error, result] tuple (similar to Go pattern)
107
+ */
108
+ static async tryAsync<T>(
109
+ fn: () => Promise<T>,
110
+ context?: string
111
+ ): Promise<[DesignSystemError | null, T | null]> {
112
+ try {
113
+ const result = await fn();
114
+ return [null, result];
115
+ } catch (error) {
116
+ const handled = this.handleAndLog(error, context);
117
+ return [handled, null];
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Wrap sync function with error handling
123
+ * Returns [error, result] tuple
124
+ */
125
+ static try<T>(
126
+ fn: () => T,
127
+ context?: string
128
+ ): [DesignSystemError | null, T | null] {
129
+ try {
130
+ const result = fn();
131
+ return [null, result];
132
+ } catch (error) {
133
+ const handled = this.handleAndLog(error, context);
134
+ return [handled, null];
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Error Utilities
3
+ * Unified error handling for the design system
4
+ */
5
+
6
+ export { DesignSystemError, ErrorCodes, type ErrorCode } from './DesignSystemError';
7
+ export { ErrorHandler } from './ErrorHandler';