@umituz/react-native-design-system 4.23.114 → 4.23.115
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 +1 -1
- package/src/filesystem/infrastructure/services/directory.service.ts +35 -7
- package/src/filesystem/infrastructure/services/download.service.ts +60 -9
- package/src/filesystem/infrastructure/services/file-manager.service.ts +38 -7
- package/src/filesystem/infrastructure/services/file-writer.service.ts +7 -2
- package/src/media/infrastructure/services/MediaPickerService.ts +32 -8
- package/src/media/infrastructure/services/MediaSaveService.ts +7 -2
- package/src/services/api/ApiClient.ts +37 -6
- package/src/utils/async/index.ts +12 -0
- package/src/utils/async/retryWithBackoff.ts +177 -0
- package/src/utils/errors/DesignSystemError.ts +117 -0
- package/src/utils/errors/ErrorHandler.ts +137 -0
- package/src/utils/errors/index.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-design-system",
|
|
3
|
-
"version": "4.23.
|
|
3
|
+
"version": "4.23.115",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
throw ErrorHandler.create(
|
|
66
|
+
`Unknown directory type: ${type}`,
|
|
67
|
+
ErrorCodes.INVALID_INPUT,
|
|
68
|
+
{ type }
|
|
69
|
+
);
|
|
47
70
|
}
|
|
48
|
-
} catch {
|
|
49
|
-
|
|
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
|
-
|
|
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 (
|
|
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)
|
|
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
|
|
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 (
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: ${
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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,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
|
+
}
|