@umituz/react-native-design-system 4.23.116 → 4.23.118
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/device/presentation/hooks/useAnonymousUser.ts +25 -31
- package/src/device/presentation/hooks/useDeviceInfo.ts +47 -91
- package/src/init/useAppInitialization.ts +24 -57
- package/src/media/domain/entities/Media.ts +1 -0
- package/src/media/infrastructure/utils/media-collection-utils.ts +9 -6
- package/src/media/presentation/hooks/useMedia.ts +73 -128
- package/src/molecules/alerts/AlertBanner.tsx +24 -46
- package/src/molecules/alerts/AlertInline.tsx +8 -9
- package/src/molecules/alerts/AlertModal.tsx +23 -14
- package/src/molecules/alerts/AlertToast.tsx +25 -53
- package/src/molecules/alerts/components/AlertContent.tsx +79 -0
- package/src/molecules/alerts/components/AlertIcon.tsx +31 -0
- package/src/molecules/alerts/components/index.ts +6 -0
- package/src/molecules/alerts/hooks/index.ts +6 -0
- package/src/molecules/alerts/hooks/useAlertAutoDismiss.ts +26 -0
- package/src/molecules/alerts/hooks/useAlertDismissHandler.ts +21 -0
- package/src/molecules/alerts/utils/alertUtils.ts +0 -21
- package/src/storage/cache/presentation/useCachedValue.ts +24 -65
- package/src/storage/presentation/hooks/useStorageState.ts +20 -29
- package/src/utilities/sharing/presentation/hooks/useSharing.ts +75 -140
- package/src/utils/errors/DesignSystemError.ts +57 -1
- package/src/utils/errors/ErrorHandler.ts +105 -1
- package/src/utils/errors/adapters/CacheErrorAdapter.ts +68 -0
- package/src/utils/errors/adapters/ImageErrorAdapter.ts +91 -0
- package/src/utils/errors/adapters/StorageErrorAdapter.ts +107 -0
- package/src/utils/errors/index.ts +5 -1
- package/src/utils/errors/types/Result.ts +64 -0
- package/src/utils/hooks/index.ts +12 -0
- package/src/utils/hooks/types/AsyncOperationTypes.ts +75 -0
- package/src/utils/hooks/useAsyncOperation.ts +223 -0
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* useCachedValue Hook
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { useCallback,
|
|
5
|
+
import { useCallback, useRef, useMemo } from 'react';
|
|
6
6
|
import { cacheManager } from '../domain/CacheManager';
|
|
7
7
|
import type { CacheConfig } from '../domain/types/Cache';
|
|
8
|
+
import { useAsyncOperation } from '../../../utils/hooks';
|
|
8
9
|
|
|
9
10
|
export function useCachedValue<T>(
|
|
10
11
|
cacheName: string,
|
|
@@ -12,95 +13,53 @@ export function useCachedValue<T>(
|
|
|
12
13
|
fetcher: () => Promise<T>,
|
|
13
14
|
config?: CacheConfig & { ttl?: number }
|
|
14
15
|
) {
|
|
15
|
-
const [value, setValue] = useState<T | undefined>(undefined);
|
|
16
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
17
|
-
const [error, setError] = useState<Error | null>(null);
|
|
18
|
-
|
|
19
16
|
const fetcherRef = useRef(fetcher);
|
|
20
17
|
const configRef = useRef(config);
|
|
21
18
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const cached = cache.get(key);
|
|
25
|
-
|
|
26
|
-
if (cached !== undefined) {
|
|
27
|
-
setValue(cached);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
setIsLoading(true);
|
|
32
|
-
setError(null);
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
const data = await fetcherRef.current!();
|
|
36
|
-
cache.set(key, data, configRef.current?.ttl);
|
|
37
|
-
setValue(data);
|
|
38
|
-
} catch (err) {
|
|
39
|
-
setError(err as Error);
|
|
40
|
-
} finally {
|
|
41
|
-
setIsLoading(false);
|
|
42
|
-
}
|
|
43
|
-
}, [cacheName, key]);
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
let isMounted = true;
|
|
47
|
-
|
|
48
|
-
const doLoad = async () => {
|
|
19
|
+
const { data: value, isLoading, error, execute, setData } = useAsyncOperation<T | undefined, Error>(
|
|
20
|
+
async () => {
|
|
49
21
|
const cache = cacheManager.getCache<T>(cacheName, configRef.current);
|
|
50
22
|
const cached = cache.get(key);
|
|
51
23
|
|
|
52
24
|
if (cached !== undefined) {
|
|
53
|
-
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (isMounted) {
|
|
58
|
-
setIsLoading(true);
|
|
59
|
-
setError(null);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
const data = await fetcherRef.current!();
|
|
64
|
-
cache.set(key, data, configRef.current?.ttl);
|
|
65
|
-
if (isMounted) setValue(data);
|
|
66
|
-
} catch (err) {
|
|
67
|
-
if (isMounted) setError(err as Error);
|
|
68
|
-
} finally {
|
|
69
|
-
if (isMounted) setIsLoading(false);
|
|
25
|
+
return cached;
|
|
70
26
|
}
|
|
71
|
-
};
|
|
72
27
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
28
|
+
const data = await fetcherRef.current!();
|
|
29
|
+
cache.set(key, data, configRef.current?.ttl);
|
|
30
|
+
return data;
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
immediate: true,
|
|
34
|
+
initialData: undefined,
|
|
35
|
+
errorHandler: (err) => err as Error,
|
|
36
|
+
}
|
|
37
|
+
);
|
|
79
38
|
|
|
80
39
|
const invalidate = useCallback(() => {
|
|
81
40
|
const cache = cacheManager.getCache<T>(cacheName);
|
|
82
41
|
cache.delete(key);
|
|
83
|
-
|
|
84
|
-
}, [cacheName, key]);
|
|
42
|
+
setData(undefined);
|
|
43
|
+
}, [cacheName, key, setData]);
|
|
85
44
|
|
|
86
45
|
const invalidatePattern = useCallback((pattern: string): number => {
|
|
87
46
|
const cache = cacheManager.getCache<T>(cacheName);
|
|
88
47
|
const count = cache.invalidatePattern(pattern);
|
|
89
|
-
|
|
48
|
+
setData(undefined);
|
|
90
49
|
return count;
|
|
91
|
-
}, [cacheName]);
|
|
50
|
+
}, [cacheName, setData]);
|
|
92
51
|
|
|
93
52
|
const refetch = useCallback(() => {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}, [
|
|
53
|
+
setData(undefined);
|
|
54
|
+
execute();
|
|
55
|
+
}, [execute, setData]);
|
|
97
56
|
|
|
98
|
-
return {
|
|
57
|
+
return useMemo(() => ({
|
|
99
58
|
value,
|
|
100
59
|
isLoading,
|
|
101
60
|
error,
|
|
102
61
|
invalidate,
|
|
103
62
|
invalidatePattern,
|
|
104
63
|
refetch,
|
|
105
|
-
};
|
|
64
|
+
}), [value, isLoading, error, invalidate, invalidatePattern, refetch]);
|
|
106
65
|
}
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* Combines React state with automatic storage persistence
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { useState,
|
|
8
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
9
9
|
import { storageRepository } from '../../infrastructure/repositories/AsyncStorageRepository';
|
|
10
10
|
import { unwrap } from '../../domain/entities/StorageResult';
|
|
11
11
|
import type { StorageKey } from '../../domain/value-objects/StorageKey';
|
|
12
|
+
import { useAsyncOperation } from '../../../utils/hooks';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Storage State Hook
|
|
@@ -26,37 +27,27 @@ export const useStorageState = <T>(
|
|
|
26
27
|
): [T, (value: T) => Promise<void>, boolean] => {
|
|
27
28
|
const keyString = typeof key === 'string' ? key : String(key);
|
|
28
29
|
const [state, setState] = useState<T>(defaultValue);
|
|
29
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
30
30
|
|
|
31
31
|
// Load initial value from storage
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
} catch {
|
|
46
|
-
// Hata durumunda bile cleanup yap
|
|
47
|
-
if (isMounted) {
|
|
48
|
-
setIsLoading(false);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
loadFromStorage();
|
|
32
|
+
const { data, isLoading } = useAsyncOperation<T, Error>(
|
|
33
|
+
async () => {
|
|
34
|
+
const result = await storageRepository.getItem(keyString, defaultValue);
|
|
35
|
+
return unwrap(result, defaultValue);
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
immediate: true,
|
|
39
|
+
initialData: defaultValue,
|
|
40
|
+
errorHandler: (err) => err as Error,
|
|
41
|
+
onSuccess: (value) => setState(value),
|
|
42
|
+
}
|
|
43
|
+
);
|
|
54
44
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
// Sync state with loaded data
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (data !== undefined && data !== null) {
|
|
48
|
+
setState(data);
|
|
49
|
+
}
|
|
50
|
+
}, [data]);
|
|
60
51
|
|
|
61
52
|
// Update state and persist to storage
|
|
62
53
|
const updateState = useCallback(
|
|
@@ -8,174 +8,109 @@
|
|
|
8
8
|
* @layer presentation/hooks
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import { useCallback, useMemo } from 'react';
|
|
12
12
|
import { SharingService } from '../../infrastructure/services/SharingService';
|
|
13
13
|
import type { ShareOptions } from '../../domain/entities/Share';
|
|
14
|
+
import { useAsyncOperation } from '../../../../utils/hooks';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* useSharing hook for sharing files via system share sheet
|
|
17
|
-
*
|
|
18
|
-
* USAGE:
|
|
19
|
-
* ```typescript
|
|
20
|
-
* const { share, shareWithAutoType, isAvailable, isSharing } = useSharing();
|
|
21
|
-
*
|
|
22
|
-
* // Check availability
|
|
23
|
-
* if (!isAvailable) {
|
|
24
|
-
* return <Text>Sharing not available</Text>;
|
|
25
|
-
* }
|
|
26
|
-
*
|
|
27
|
-
* // Basic share
|
|
28
|
-
* const handleShare = async () => {
|
|
29
|
-
* await share('file:///path/to/file.jpg', {
|
|
30
|
-
* dialogTitle: 'Share Photo',
|
|
31
|
-
* mimeType: 'image/jpeg',
|
|
32
|
-
* });
|
|
33
|
-
* };
|
|
34
|
-
*
|
|
35
|
-
* // Auto-detect MIME type
|
|
36
|
-
* const handleShareAuto = async () => {
|
|
37
|
-
* await shareWithAutoType(
|
|
38
|
-
* 'file:///path/to/document.pdf',
|
|
39
|
-
* 'document.pdf',
|
|
40
|
-
* 'Share Document'
|
|
41
|
-
* );
|
|
42
|
-
* };
|
|
43
|
-
* ```
|
|
44
18
|
*/
|
|
45
19
|
export const useSharing = () => {
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Check sharing availability on mount
|
|
55
|
-
*/
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
const checkAvailability = async () => {
|
|
58
|
-
const available = await SharingService.isAvailable();
|
|
59
|
-
if (isMountedRef.current) {
|
|
60
|
-
setIsAvailable(available);
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
checkAvailability();
|
|
65
|
-
|
|
66
|
-
return () => {
|
|
67
|
-
isMountedRef.current = false;
|
|
68
|
-
};
|
|
69
|
-
}, []);
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Share a file via system share sheet
|
|
73
|
-
*/
|
|
74
|
-
const share = useCallback(async (uri: string, options?: ShareOptions): Promise<boolean> => {
|
|
75
|
-
if (isMountedRef.current) {
|
|
76
|
-
setIsSharing(true);
|
|
77
|
-
setError(null);
|
|
20
|
+
// Check availability on mount
|
|
21
|
+
const availabilityOp = useAsyncOperation<boolean, string>(
|
|
22
|
+
() => SharingService.isAvailable(),
|
|
23
|
+
{
|
|
24
|
+
immediate: true,
|
|
25
|
+
initialData: false,
|
|
26
|
+
errorHandler: () => 'Failed to check sharing availability',
|
|
78
27
|
}
|
|
28
|
+
);
|
|
79
29
|
|
|
80
|
-
|
|
30
|
+
// Share operations
|
|
31
|
+
const shareOp = useAsyncOperation<boolean, string>(
|
|
32
|
+
async (uri: string, options?: ShareOptions) => {
|
|
81
33
|
const result = await SharingService.shareFile(uri, options);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
setError(result.error || 'Failed to share file');
|
|
85
|
-
return false;
|
|
34
|
+
if (!result.success) {
|
|
35
|
+
throw new Error(result.error || 'Failed to share file');
|
|
86
36
|
}
|
|
87
|
-
|
|
88
37
|
return true;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
return false;
|
|
95
|
-
} finally {
|
|
96
|
-
if (isMountedRef.current) {
|
|
97
|
-
setIsSharing(false);
|
|
98
|
-
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
immediate: false,
|
|
41
|
+
errorHandler: (err) => err instanceof Error ? err.message : 'Failed to share file',
|
|
99
42
|
}
|
|
100
|
-
|
|
43
|
+
);
|
|
101
44
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (isMountedRef.current) {
|
|
108
|
-
setIsSharing(true);
|
|
109
|
-
setError(null);
|
|
45
|
+
const shareWithAutoTypeOp = useAsyncOperation<boolean, string>(
|
|
46
|
+
async (uri: string, filename: string, dialogTitle?: string) => {
|
|
47
|
+
const result = await SharingService.shareWithAutoType(uri, filename, dialogTitle);
|
|
48
|
+
if (!result.success) {
|
|
49
|
+
throw new Error(result.error || 'Failed to share file');
|
|
110
50
|
}
|
|
51
|
+
return true;
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
immediate: false,
|
|
55
|
+
errorHandler: (err) => err instanceof Error ? err.message : 'Failed to share file',
|
|
56
|
+
}
|
|
57
|
+
);
|
|
111
58
|
|
|
112
|
-
|
|
113
|
-
|
|
59
|
+
const shareMultipleOp = useAsyncOperation<boolean, string>(
|
|
60
|
+
async (uris: string[], options?: ShareOptions) => {
|
|
61
|
+
const result = await SharingService.shareMultipleFiles(uris, options);
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
throw new Error(result.error || 'Failed to share files');
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
immediate: false,
|
|
69
|
+
errorHandler: (err) => err instanceof Error ? err.message : 'Failed to share files',
|
|
70
|
+
}
|
|
71
|
+
);
|
|
114
72
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
73
|
+
const share = useCallback(
|
|
74
|
+
async (uri: string, options?: ShareOptions): Promise<boolean> => {
|
|
75
|
+
const result = await shareOp.execute(uri, options);
|
|
76
|
+
return result ?? false;
|
|
77
|
+
},
|
|
78
|
+
[shareOp]
|
|
79
|
+
);
|
|
119
80
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
setError(errorMessage);
|
|
125
|
-
}
|
|
126
|
-
return false;
|
|
127
|
-
} finally {
|
|
128
|
-
if (isMountedRef.current) {
|
|
129
|
-
setIsSharing(false);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
81
|
+
const shareWithAutoType = useCallback(
|
|
82
|
+
async (uri: string, filename: string, dialogTitle?: string): Promise<boolean> => {
|
|
83
|
+
const result = await shareWithAutoTypeOp.execute(uri, filename, dialogTitle);
|
|
84
|
+
return result ?? false;
|
|
132
85
|
},
|
|
133
|
-
[]
|
|
86
|
+
[shareWithAutoTypeOp]
|
|
134
87
|
);
|
|
135
88
|
|
|
136
|
-
/**
|
|
137
|
-
* Share multiple files
|
|
138
|
-
*/
|
|
139
89
|
const shareMultiple = useCallback(
|
|
140
90
|
async (uris: string[], options?: ShareOptions): Promise<boolean> => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
setError(null);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
try {
|
|
147
|
-
const result = await SharingService.shareMultipleFiles(uris, options);
|
|
148
|
-
|
|
149
|
-
if (!result.success && isMountedRef.current) {
|
|
150
|
-
setError(result.error || 'Failed to share files');
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return true;
|
|
155
|
-
} catch (err) {
|
|
156
|
-
const errorMessage = err instanceof Error ? err.message : 'Failed to share files';
|
|
157
|
-
if (isMountedRef.current) {
|
|
158
|
-
setError(errorMessage);
|
|
159
|
-
}
|
|
160
|
-
return false;
|
|
161
|
-
} finally {
|
|
162
|
-
if (isMountedRef.current) {
|
|
163
|
-
setIsSharing(false);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
91
|
+
const result = await shareMultipleOp.execute(uris, options);
|
|
92
|
+
return result ?? false;
|
|
166
93
|
},
|
|
167
|
-
[]
|
|
94
|
+
[shareMultipleOp]
|
|
168
95
|
);
|
|
169
96
|
|
|
170
97
|
return useMemo(() => ({
|
|
171
|
-
// Functions
|
|
172
98
|
share,
|
|
173
99
|
shareWithAutoType,
|
|
174
100
|
shareMultiple,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
101
|
+
isAvailable: availabilityOp.data ?? false,
|
|
102
|
+
isSharing: shareOp.isLoading || shareWithAutoTypeOp.isLoading || shareMultipleOp.isLoading,
|
|
103
|
+
error: shareOp.error || shareWithAutoTypeOp.error || shareMultipleOp.error,
|
|
104
|
+
}), [
|
|
105
|
+
share,
|
|
106
|
+
shareWithAutoType,
|
|
107
|
+
shareMultiple,
|
|
108
|
+
availabilityOp.data,
|
|
109
|
+
shareOp.isLoading,
|
|
110
|
+
shareWithAutoTypeOp.isLoading,
|
|
111
|
+
shareMultipleOp.isLoading,
|
|
112
|
+
shareOp.error,
|
|
113
|
+
shareWithAutoTypeOp.error,
|
|
114
|
+
shareMultipleOp.error,
|
|
115
|
+
]);
|
|
181
116
|
};
|
|
@@ -21,10 +21,31 @@ export class DesignSystemError extends Error {
|
|
|
21
21
|
*/
|
|
22
22
|
public readonly timestamp: Date;
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Error category for grouping
|
|
26
|
+
*/
|
|
27
|
+
public readonly category: ErrorCategory;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Operation that failed
|
|
31
|
+
*/
|
|
32
|
+
public readonly operation?: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Original error cause
|
|
36
|
+
*/
|
|
37
|
+
public readonly cause?: unknown;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether operation can be retried
|
|
41
|
+
*/
|
|
42
|
+
public readonly retryable: boolean;
|
|
43
|
+
|
|
24
44
|
constructor(
|
|
25
45
|
message: string,
|
|
26
46
|
code: string,
|
|
27
|
-
context?: Record<string, any
|
|
47
|
+
context?: Record<string, any>,
|
|
48
|
+
metadata?: ErrorMetadata
|
|
28
49
|
) {
|
|
29
50
|
super(message);
|
|
30
51
|
this.name = 'DesignSystemError';
|
|
@@ -32,6 +53,12 @@ export class DesignSystemError extends Error {
|
|
|
32
53
|
this.context = context;
|
|
33
54
|
this.timestamp = new Date();
|
|
34
55
|
|
|
56
|
+
// Extract metadata
|
|
57
|
+
this.category = metadata?.category ?? ErrorCategory.UNKNOWN;
|
|
58
|
+
this.operation = metadata?.operation;
|
|
59
|
+
this.cause = metadata?.cause;
|
|
60
|
+
this.retryable = metadata?.retryable ?? false;
|
|
61
|
+
|
|
35
62
|
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
36
63
|
if (Error.captureStackTrace) {
|
|
37
64
|
Error.captureStackTrace(this, DesignSystemError);
|
|
@@ -115,3 +142,32 @@ export const ErrorCodes = {
|
|
|
115
142
|
} as const;
|
|
116
143
|
|
|
117
144
|
export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Error Categories for grouping related errors
|
|
148
|
+
*/
|
|
149
|
+
export enum ErrorCategory {
|
|
150
|
+
FILESYSTEM = 'FILESYSTEM',
|
|
151
|
+
NETWORK = 'NETWORK',
|
|
152
|
+
MEDIA = 'MEDIA',
|
|
153
|
+
STORAGE = 'STORAGE',
|
|
154
|
+
CACHE = 'CACHE',
|
|
155
|
+
IMAGE = 'IMAGE',
|
|
156
|
+
VALIDATION = 'VALIDATION',
|
|
157
|
+
THEME = 'THEME',
|
|
158
|
+
PERMISSION = 'PERMISSION',
|
|
159
|
+
INITIALIZATION = 'INITIALIZATION',
|
|
160
|
+
UNKNOWN = 'UNKNOWN',
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Error metadata for rich context
|
|
165
|
+
*/
|
|
166
|
+
export interface ErrorMetadata {
|
|
167
|
+
category?: ErrorCategory;
|
|
168
|
+
operation?: string;
|
|
169
|
+
key?: string;
|
|
170
|
+
cause?: unknown;
|
|
171
|
+
retryable?: boolean;
|
|
172
|
+
userMessage?: string;
|
|
173
|
+
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* Provides consistent error handling, logging, and reporting.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { DesignSystemError, ErrorCodes } from './DesignSystemError';
|
|
8
|
+
import { DesignSystemError, ErrorCodes, ErrorCategory, type ErrorMetadata } from './DesignSystemError';
|
|
9
|
+
import type { Result } from './types/Result';
|
|
10
|
+
import { ok, err } from './types/Result';
|
|
9
11
|
|
|
10
12
|
export class ErrorHandler {
|
|
11
13
|
/**
|
|
@@ -134,4 +136,106 @@ export class ErrorHandler {
|
|
|
134
136
|
return [handled, null];
|
|
135
137
|
}
|
|
136
138
|
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Normalize any error to DesignSystemError with metadata
|
|
142
|
+
*/
|
|
143
|
+
static normalize(
|
|
144
|
+
error: unknown,
|
|
145
|
+
code: string,
|
|
146
|
+
metadata?: ErrorMetadata
|
|
147
|
+
): DesignSystemError {
|
|
148
|
+
if (error instanceof DesignSystemError) {
|
|
149
|
+
return error;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (error instanceof Error) {
|
|
153
|
+
return new DesignSystemError(
|
|
154
|
+
error.message,
|
|
155
|
+
code,
|
|
156
|
+
{
|
|
157
|
+
originalError: error.name,
|
|
158
|
+
stack: error.stack,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
...metadata,
|
|
162
|
+
cause: error,
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (typeof error === 'string') {
|
|
168
|
+
return new DesignSystemError(error, code, undefined, metadata);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return new DesignSystemError(
|
|
172
|
+
'An unknown error occurred',
|
|
173
|
+
code,
|
|
174
|
+
{ originalError: String(error) },
|
|
175
|
+
metadata
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Wrap async function with timeout
|
|
181
|
+
*/
|
|
182
|
+
static async withTimeout<T>(
|
|
183
|
+
promise: Promise<T>,
|
|
184
|
+
timeoutMs: number,
|
|
185
|
+
context?: string
|
|
186
|
+
): Promise<T> {
|
|
187
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
reject(
|
|
190
|
+
new DesignSystemError(
|
|
191
|
+
`${context || 'Operation'} timed out after ${timeoutMs}ms`,
|
|
192
|
+
ErrorCodes.TIMEOUT_ERROR,
|
|
193
|
+
{ timeoutMs, context },
|
|
194
|
+
{
|
|
195
|
+
category: ErrorCategory.NETWORK,
|
|
196
|
+
retryable: true,
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
);
|
|
200
|
+
}, timeoutMs);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return Promise.race([promise, timeoutPromise]);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Wrap async function returning Result type
|
|
208
|
+
*/
|
|
209
|
+
static async tryAsyncResult<T>(
|
|
210
|
+
fn: () => Promise<T>,
|
|
211
|
+
code: string,
|
|
212
|
+
metadata?: ErrorMetadata
|
|
213
|
+
): Promise<Result<T, DesignSystemError>> {
|
|
214
|
+
try {
|
|
215
|
+
const result = await fn();
|
|
216
|
+
return ok(result);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const normalized = this.normalize(error, code, metadata);
|
|
219
|
+
this.log(normalized);
|
|
220
|
+
return err(normalized);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Wrap sync function returning Result type
|
|
226
|
+
*/
|
|
227
|
+
static tryResult<T>(
|
|
228
|
+
fn: () => T,
|
|
229
|
+
code: string,
|
|
230
|
+
metadata?: ErrorMetadata
|
|
231
|
+
): Result<T, DesignSystemError> {
|
|
232
|
+
try {
|
|
233
|
+
const result = fn();
|
|
234
|
+
return ok(result);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
const normalized = this.normalize(error, code, metadata);
|
|
237
|
+
this.log(normalized);
|
|
238
|
+
return err(normalized);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
137
241
|
}
|