@umituz/react-native-design-system 4.23.117 → 4.23.119
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
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Error Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapts cache errors to unified DesignSystemError format.
|
|
5
|
+
* Maintains backward compatibility with CacheError.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DesignSystemError, ErrorCodes, ErrorCategory } from '../DesignSystemError';
|
|
9
|
+
import type { ErrorMetadata } from '../DesignSystemError';
|
|
10
|
+
import { CacheError } from '../../../storage/cache/domain/ErrorHandler';
|
|
11
|
+
|
|
12
|
+
export class CacheErrorAdapter {
|
|
13
|
+
/**
|
|
14
|
+
* Create a DesignSystemError for cache operations
|
|
15
|
+
*/
|
|
16
|
+
static create(message: string, context: string, cause?: unknown): DesignSystemError {
|
|
17
|
+
const metadata: ErrorMetadata = {
|
|
18
|
+
category: ErrorCategory.CACHE,
|
|
19
|
+
operation: context,
|
|
20
|
+
cause,
|
|
21
|
+
retryable: true,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
return new DesignSystemError(
|
|
25
|
+
`${context}: ${message}`,
|
|
26
|
+
ErrorCodes.CACHE_ERROR,
|
|
27
|
+
{ context },
|
|
28
|
+
metadata
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert legacy CacheError to DesignSystemError
|
|
34
|
+
*/
|
|
35
|
+
static fromCacheError(error: CacheError): DesignSystemError {
|
|
36
|
+
return new DesignSystemError(
|
|
37
|
+
error.message,
|
|
38
|
+
ErrorCodes.CACHE_ERROR,
|
|
39
|
+
{ cacheErrorCode: error.code },
|
|
40
|
+
{
|
|
41
|
+
category: ErrorCategory.CACHE,
|
|
42
|
+
retryable: true,
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Handle with timeout (replaces cache ErrorHandler.withTimeout)
|
|
49
|
+
*/
|
|
50
|
+
static async withTimeout<T>(
|
|
51
|
+
promise: Promise<T>,
|
|
52
|
+
timeoutMs: number,
|
|
53
|
+
context: string
|
|
54
|
+
): Promise<T> {
|
|
55
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
56
|
+
setTimeout(() => {
|
|
57
|
+
reject(
|
|
58
|
+
this.create(
|
|
59
|
+
`Operation timed out after ${timeoutMs}ms`,
|
|
60
|
+
context
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
}, timeoutMs);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return Promise.race([promise, timeoutPromise]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Error Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapts image errors to unified DesignSystemError format.
|
|
5
|
+
* Maintains backward compatibility with ImageError.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DesignSystemError, ErrorCodes, ErrorCategory } from '../DesignSystemError';
|
|
9
|
+
import type { ErrorMetadata } from '../DesignSystemError';
|
|
10
|
+
import {
|
|
11
|
+
ImageError,
|
|
12
|
+
IMAGE_ERROR_CODES,
|
|
13
|
+
type ImageErrorCode,
|
|
14
|
+
} from '../../../image/infrastructure/utils/ImageErrorHandler';
|
|
15
|
+
|
|
16
|
+
export class ImageErrorAdapter {
|
|
17
|
+
/**
|
|
18
|
+
* Map ImageErrorCode to DesignSystemError code
|
|
19
|
+
*/
|
|
20
|
+
private static mapErrorCode(code: ImageErrorCode): string {
|
|
21
|
+
switch (code) {
|
|
22
|
+
case IMAGE_ERROR_CODES.INVALID_URI:
|
|
23
|
+
case IMAGE_ERROR_CODES.INVALID_DIMENSIONS:
|
|
24
|
+
case IMAGE_ERROR_CODES.INVALID_QUALITY:
|
|
25
|
+
case IMAGE_ERROR_CODES.VALIDATION_ERROR:
|
|
26
|
+
return ErrorCodes.VALIDATION_ERROR;
|
|
27
|
+
|
|
28
|
+
case IMAGE_ERROR_CODES.MANIPULATION_FAILED:
|
|
29
|
+
case IMAGE_ERROR_CODES.CONVERSION_FAILED:
|
|
30
|
+
return ErrorCodes.IMAGE_LOAD_ERROR;
|
|
31
|
+
|
|
32
|
+
case IMAGE_ERROR_CODES.STORAGE_FAILED:
|
|
33
|
+
return ErrorCodes.STORAGE_ERROR;
|
|
34
|
+
|
|
35
|
+
default:
|
|
36
|
+
return ErrorCodes.UNKNOWN_ERROR;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a DesignSystemError for image operations
|
|
42
|
+
*/
|
|
43
|
+
static create(
|
|
44
|
+
message: string,
|
|
45
|
+
code: ImageErrorCode,
|
|
46
|
+
operation?: string
|
|
47
|
+
): DesignSystemError {
|
|
48
|
+
const metadata: ErrorMetadata = {
|
|
49
|
+
category: ErrorCategory.IMAGE,
|
|
50
|
+
operation,
|
|
51
|
+
retryable: code === IMAGE_ERROR_CODES.MANIPULATION_FAILED,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return new DesignSystemError(
|
|
55
|
+
message,
|
|
56
|
+
this.mapErrorCode(code),
|
|
57
|
+
{ imageErrorCode: code, operation },
|
|
58
|
+
metadata
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Convert legacy ImageError to DesignSystemError
|
|
64
|
+
*/
|
|
65
|
+
static fromImageError(error: ImageError): DesignSystemError {
|
|
66
|
+
return this.create(error.message, error.code as ImageErrorCode, error.operation);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handle unknown image errors
|
|
71
|
+
*/
|
|
72
|
+
static handleUnknown(error: unknown, operation?: string): DesignSystemError {
|
|
73
|
+
if (error instanceof ImageError) {
|
|
74
|
+
return this.fromImageError(error);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const message = error instanceof Error ? error.message : 'Unknown image error occurred';
|
|
78
|
+
|
|
79
|
+
return new DesignSystemError(
|
|
80
|
+
message,
|
|
81
|
+
ErrorCodes.IMAGE_LOAD_ERROR,
|
|
82
|
+
{ operation },
|
|
83
|
+
{
|
|
84
|
+
category: ErrorCategory.IMAGE,
|
|
85
|
+
operation,
|
|
86
|
+
cause: error,
|
|
87
|
+
retryable: true,
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Error Adapter
|
|
3
|
+
*
|
|
4
|
+
* Adapts storage errors to unified DesignSystemError format.
|
|
5
|
+
* Maintains backward compatibility with StorageError hierarchy.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { DesignSystemError, ErrorCodes, ErrorCategory } from '../DesignSystemError';
|
|
9
|
+
import type { ErrorMetadata } from '../DesignSystemError';
|
|
10
|
+
import {
|
|
11
|
+
StorageError,
|
|
12
|
+
StorageReadError,
|
|
13
|
+
StorageWriteError,
|
|
14
|
+
StorageDeleteError,
|
|
15
|
+
StorageSerializationError,
|
|
16
|
+
StorageDeserializationError,
|
|
17
|
+
} from '../../../storage/domain/errors/StorageError';
|
|
18
|
+
|
|
19
|
+
export class StorageErrorAdapter {
|
|
20
|
+
/**
|
|
21
|
+
* Create a DesignSystemError for storage read failures
|
|
22
|
+
*/
|
|
23
|
+
static readError(key: string, cause?: unknown): DesignSystemError {
|
|
24
|
+
const metadata: ErrorMetadata = {
|
|
25
|
+
category: ErrorCategory.STORAGE,
|
|
26
|
+
operation: 'read',
|
|
27
|
+
key,
|
|
28
|
+
cause,
|
|
29
|
+
retryable: true,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return new DesignSystemError(
|
|
33
|
+
`Failed to read from storage: ${key}`,
|
|
34
|
+
ErrorCodes.STORAGE_ERROR,
|
|
35
|
+
{ key },
|
|
36
|
+
metadata
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create a DesignSystemError for storage write failures
|
|
42
|
+
*/
|
|
43
|
+
static writeError(key: string, cause?: unknown): DesignSystemError {
|
|
44
|
+
const metadata: ErrorMetadata = {
|
|
45
|
+
category: ErrorCategory.STORAGE,
|
|
46
|
+
operation: 'write',
|
|
47
|
+
key,
|
|
48
|
+
cause,
|
|
49
|
+
retryable: true,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return new DesignSystemError(
|
|
53
|
+
`Failed to write to storage: ${key}`,
|
|
54
|
+
ErrorCodes.STORAGE_ERROR,
|
|
55
|
+
{ key },
|
|
56
|
+
metadata
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a DesignSystemError for storage delete failures
|
|
62
|
+
*/
|
|
63
|
+
static deleteError(key: string, cause?: unknown): DesignSystemError {
|
|
64
|
+
const metadata: ErrorMetadata = {
|
|
65
|
+
category: ErrorCategory.STORAGE,
|
|
66
|
+
operation: 'delete',
|
|
67
|
+
key,
|
|
68
|
+
cause,
|
|
69
|
+
retryable: true,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return new DesignSystemError(
|
|
73
|
+
`Failed to delete from storage: ${key}`,
|
|
74
|
+
ErrorCodes.STORAGE_ERROR,
|
|
75
|
+
{ key },
|
|
76
|
+
metadata
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Convert legacy StorageError to DesignSystemError
|
|
82
|
+
*/
|
|
83
|
+
static fromStorageError(error: StorageError): DesignSystemError {
|
|
84
|
+
let operation = 'unknown';
|
|
85
|
+
|
|
86
|
+
if (error instanceof StorageReadError) operation = 'read';
|
|
87
|
+
else if (error instanceof StorageWriteError) operation = 'write';
|
|
88
|
+
else if (error instanceof StorageDeleteError) operation = 'delete';
|
|
89
|
+
else if (error instanceof StorageSerializationError) operation = 'serialize';
|
|
90
|
+
else if (error instanceof StorageDeserializationError) operation = 'deserialize';
|
|
91
|
+
|
|
92
|
+
const cause = 'cause' in error ? error.cause : undefined;
|
|
93
|
+
|
|
94
|
+
return new DesignSystemError(
|
|
95
|
+
error.message,
|
|
96
|
+
ErrorCodes.STORAGE_ERROR,
|
|
97
|
+
{ key: error.key },
|
|
98
|
+
{
|
|
99
|
+
category: ErrorCategory.STORAGE,
|
|
100
|
+
operation,
|
|
101
|
+
key: error.key,
|
|
102
|
+
cause,
|
|
103
|
+
retryable: true,
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -3,5 +3,9 @@
|
|
|
3
3
|
* Unified error handling for the design system
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
export { DesignSystemError, ErrorCodes, type ErrorCode } from './DesignSystemError';
|
|
6
|
+
export { DesignSystemError, ErrorCodes, ErrorCategory, type ErrorCode, type ErrorMetadata } from './DesignSystemError';
|
|
7
7
|
export { ErrorHandler } from './ErrorHandler';
|
|
8
|
+
|
|
9
|
+
// Result type for explicit error handling
|
|
10
|
+
export type { Result } from './types/Result';
|
|
11
|
+
export { ok, err, unwrap, unwrapOr, map, mapError } from './types/Result';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Type
|
|
3
|
+
*
|
|
4
|
+
* Rust/Go-inspired Result type for explicit error handling.
|
|
5
|
+
* Alternative to throwing errors or returning tuples.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type Result<T, E = Error> =
|
|
9
|
+
| { success: true; value: T; error: null }
|
|
10
|
+
| { success: false; value: null; error: E };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a successful result
|
|
14
|
+
*/
|
|
15
|
+
export function ok<T, E = Error>(value: T): Result<T, E> {
|
|
16
|
+
return { success: true, value, error: null };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create an error result
|
|
21
|
+
*/
|
|
22
|
+
export function err<T, E = Error>(error: E): Result<T, E> {
|
|
23
|
+
return { success: false, value: null, error };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Unwrap result value or throw error
|
|
28
|
+
*/
|
|
29
|
+
export function unwrap<T, E = Error>(result: Result<T, E>): T {
|
|
30
|
+
if (result.success) {
|
|
31
|
+
return result.value;
|
|
32
|
+
}
|
|
33
|
+
throw result.error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Unwrap result value or return default
|
|
38
|
+
*/
|
|
39
|
+
export function unwrapOr<T, E = Error>(result: Result<T, E>, defaultValue: T): T {
|
|
40
|
+
return result.success ? result.value : defaultValue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Map result value if successful
|
|
45
|
+
*/
|
|
46
|
+
export function map<T, U, E = Error>(
|
|
47
|
+
result: Result<T, E>,
|
|
48
|
+
fn: (value: T) => U
|
|
49
|
+
): Result<U, E> {
|
|
50
|
+
if (result.success) {
|
|
51
|
+
return ok(fn(result.value));
|
|
52
|
+
}
|
|
53
|
+
return { success: false, value: null, error: result.error };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Map result error if failed
|
|
58
|
+
*/
|
|
59
|
+
export function mapError<T, E, F>(
|
|
60
|
+
result: Result<T, E>,
|
|
61
|
+
fn: (error: E) => F
|
|
62
|
+
): Result<T, F> {
|
|
63
|
+
return result.success ? result : err(fn(result.error));
|
|
64
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Hooks Barrel Export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { useAsyncOperation } from './useAsyncOperation';
|
|
6
|
+
export type {
|
|
7
|
+
AsyncOperationOptions,
|
|
8
|
+
AsyncOperationState,
|
|
9
|
+
AsyncOperationActions,
|
|
10
|
+
AsyncOperationReturn,
|
|
11
|
+
ErrorHandler,
|
|
12
|
+
} from './types/AsyncOperationTypes';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AsyncOperation Types
|
|
3
|
+
* Type definitions for useAsyncOperation hook
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ErrorHandler<E = Error> = (error: unknown) => E;
|
|
7
|
+
|
|
8
|
+
export interface AsyncOperationOptions<T, E = Error> {
|
|
9
|
+
/** Skip execution (useful for conditional operations) */
|
|
10
|
+
skip?: boolean;
|
|
11
|
+
|
|
12
|
+
/** Execute immediately on mount */
|
|
13
|
+
immediate?: boolean;
|
|
14
|
+
|
|
15
|
+
/** Initial data value */
|
|
16
|
+
initialData?: T | null;
|
|
17
|
+
|
|
18
|
+
/** Convert error to custom type */
|
|
19
|
+
errorHandler?: ErrorHandler<E>;
|
|
20
|
+
|
|
21
|
+
/** Callback on success */
|
|
22
|
+
onSuccess?: (data: T) => void;
|
|
23
|
+
|
|
24
|
+
/** Callback on error */
|
|
25
|
+
onError?: (error: E) => void;
|
|
26
|
+
|
|
27
|
+
/** Callback on finally (success or error) */
|
|
28
|
+
onFinally?: () => void;
|
|
29
|
+
|
|
30
|
+
/** Enable retry functionality */
|
|
31
|
+
enableRetry?: boolean;
|
|
32
|
+
|
|
33
|
+
/** Maximum retry attempts */
|
|
34
|
+
maxRetries?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AsyncOperationState<T, E = Error> {
|
|
38
|
+
/** Current data value */
|
|
39
|
+
data: T | null;
|
|
40
|
+
|
|
41
|
+
/** Loading state */
|
|
42
|
+
isLoading: boolean;
|
|
43
|
+
|
|
44
|
+
/** Error state */
|
|
45
|
+
error: E | null;
|
|
46
|
+
|
|
47
|
+
/** Is operation idle (not executed yet) */
|
|
48
|
+
isIdle: boolean;
|
|
49
|
+
|
|
50
|
+
/** Is operation successful */
|
|
51
|
+
isSuccess: boolean;
|
|
52
|
+
|
|
53
|
+
/** Is operation in error state */
|
|
54
|
+
isError: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AsyncOperationActions<T, E = Error> {
|
|
58
|
+
/** Execute the async operation */
|
|
59
|
+
execute: (...args: any[]) => Promise<T | null>;
|
|
60
|
+
|
|
61
|
+
/** Retry the last operation */
|
|
62
|
+
retry: () => Promise<T | null>;
|
|
63
|
+
|
|
64
|
+
/** Reset to initial state */
|
|
65
|
+
reset: () => void;
|
|
66
|
+
|
|
67
|
+
/** Set data manually */
|
|
68
|
+
setData: (data: T | null) => void;
|
|
69
|
+
|
|
70
|
+
/** Set error manually */
|
|
71
|
+
setError: (error: E | null) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type AsyncOperationReturn<T, E = Error> =
|
|
75
|
+
AsyncOperationState<T, E> & AsyncOperationActions<T, E>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAsyncOperation Hook
|
|
3
|
+
*
|
|
4
|
+
* Eliminates duplicate async error handling patterns across hooks.
|
|
5
|
+
* Handles isMountedRef + try-catch-finally + loading/error state management.
|
|
6
|
+
*
|
|
7
|
+
* Based on the useAsyncData pattern from useDeviceInfo.ts but generalized
|
|
8
|
+
* for broader use cases.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // Simple usage (like useAsyncData)
|
|
13
|
+
* const { data, isLoading, error, execute } = useAsyncOperation(
|
|
14
|
+
* async () => await fetchData(),
|
|
15
|
+
* { immediate: true }
|
|
16
|
+
* );
|
|
17
|
+
*
|
|
18
|
+
* // Manual execution (like useMedia)
|
|
19
|
+
* const { execute, isLoading, error } = useAsyncOperation(
|
|
20
|
+
* async (uri: string) => await pickImage(uri),
|
|
21
|
+
* { immediate: false }
|
|
22
|
+
* );
|
|
23
|
+
* const result = await execute('file://...');
|
|
24
|
+
*
|
|
25
|
+
* // With custom error handling
|
|
26
|
+
* const { data, error } = useAsyncOperation(
|
|
27
|
+
* async () => await riskyOperation(),
|
|
28
|
+
* {
|
|
29
|
+
* errorHandler: (err) => err instanceof Error ? err.message : 'Failed',
|
|
30
|
+
* onError: (err) => console.error('Operation failed:', err),
|
|
31
|
+
* }
|
|
32
|
+
* );
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
|
37
|
+
import type {
|
|
38
|
+
AsyncOperationOptions,
|
|
39
|
+
AsyncOperationReturn,
|
|
40
|
+
ErrorHandler,
|
|
41
|
+
} from './types/AsyncOperationTypes';
|
|
42
|
+
|
|
43
|
+
const defaultErrorHandler: ErrorHandler<Error> = (error: unknown): Error => {
|
|
44
|
+
if (error instanceof Error) return error;
|
|
45
|
+
return new Error(String(error));
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function useAsyncOperation<T, E = Error>(
|
|
49
|
+
operation: (...args: any[]) => Promise<T>,
|
|
50
|
+
options: AsyncOperationOptions<T, E> = {}
|
|
51
|
+
): AsyncOperationReturn<T, E> {
|
|
52
|
+
const {
|
|
53
|
+
skip = false,
|
|
54
|
+
immediate = false,
|
|
55
|
+
initialData = null,
|
|
56
|
+
errorHandler = defaultErrorHandler as ErrorHandler<E>,
|
|
57
|
+
onSuccess,
|
|
58
|
+
onError,
|
|
59
|
+
onFinally,
|
|
60
|
+
enableRetry = false,
|
|
61
|
+
maxRetries = 3,
|
|
62
|
+
} = options;
|
|
63
|
+
|
|
64
|
+
// State
|
|
65
|
+
const [data, setData] = useState<T | null>(initialData);
|
|
66
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
67
|
+
const [error, setErrorState] = useState<E | null>(null);
|
|
68
|
+
const [isIdle, setIsIdle] = useState(true);
|
|
69
|
+
|
|
70
|
+
// Refs for cleanup and retry
|
|
71
|
+
const isMountedRef = useRef(true);
|
|
72
|
+
const lastArgsRef = useRef<any[]>([]);
|
|
73
|
+
const retryCountRef = useRef(0);
|
|
74
|
+
|
|
75
|
+
// Stable callback refs
|
|
76
|
+
const onSuccessRef = useRef(onSuccess);
|
|
77
|
+
const onErrorRef = useRef(onError);
|
|
78
|
+
const onFinallyRef = useRef(onFinally);
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
onSuccessRef.current = onSuccess;
|
|
82
|
+
onErrorRef.current = onError;
|
|
83
|
+
onFinallyRef.current = onFinally;
|
|
84
|
+
}, [onSuccess, onError, onFinally]);
|
|
85
|
+
|
|
86
|
+
// Cleanup on unmount
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
return () => {
|
|
89
|
+
isMountedRef.current = false;
|
|
90
|
+
};
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const execute = useCallback(
|
|
94
|
+
async (...args: any[]): Promise<T | null> => {
|
|
95
|
+
if (!isMountedRef.current || skip) return null;
|
|
96
|
+
|
|
97
|
+
// Store args for retry
|
|
98
|
+
lastArgsRef.current = args;
|
|
99
|
+
|
|
100
|
+
if (isMountedRef.current) {
|
|
101
|
+
setIsLoading(true);
|
|
102
|
+
setErrorState(null);
|
|
103
|
+
setIsIdle(false);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const result = await operation(...args);
|
|
108
|
+
|
|
109
|
+
if (isMountedRef.current) {
|
|
110
|
+
setData(result);
|
|
111
|
+
onSuccessRef.current?.(result);
|
|
112
|
+
retryCountRef.current = 0; // Reset retry count on success
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const handledError = errorHandler(err);
|
|
118
|
+
|
|
119
|
+
if (isMountedRef.current) {
|
|
120
|
+
setErrorState(handledError);
|
|
121
|
+
onErrorRef.current?.(handledError);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return null;
|
|
125
|
+
} finally {
|
|
126
|
+
if (isMountedRef.current) {
|
|
127
|
+
setIsLoading(false);
|
|
128
|
+
onFinallyRef.current?.();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
[operation, skip, errorHandler]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const retry = useCallback(async (): Promise<T | null> => {
|
|
136
|
+
if (!enableRetry) {
|
|
137
|
+
if (__DEV__) {
|
|
138
|
+
console.warn('Retry is not enabled for this operation');
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (retryCountRef.current >= maxRetries) {
|
|
144
|
+
if (__DEV__) {
|
|
145
|
+
console.warn(`Max retries (${maxRetries}) reached`);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
retryCountRef.current++;
|
|
151
|
+
return execute(...lastArgsRef.current);
|
|
152
|
+
}, [execute, enableRetry, maxRetries]);
|
|
153
|
+
|
|
154
|
+
const reset = useCallback(() => {
|
|
155
|
+
if (isMountedRef.current) {
|
|
156
|
+
setData(initialData);
|
|
157
|
+
setErrorState(null);
|
|
158
|
+
setIsLoading(false);
|
|
159
|
+
setIsIdle(true);
|
|
160
|
+
retryCountRef.current = 0;
|
|
161
|
+
lastArgsRef.current = [];
|
|
162
|
+
}
|
|
163
|
+
}, [initialData]);
|
|
164
|
+
|
|
165
|
+
const setDataManual = useCallback((newData: T | null) => {
|
|
166
|
+
if (isMountedRef.current) {
|
|
167
|
+
setData(newData);
|
|
168
|
+
setErrorState(null);
|
|
169
|
+
setIsIdle(false);
|
|
170
|
+
}
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
const setErrorManual = useCallback((newError: E | null) => {
|
|
174
|
+
if (isMountedRef.current) {
|
|
175
|
+
setErrorState(newError);
|
|
176
|
+
setIsIdle(false);
|
|
177
|
+
}
|
|
178
|
+
}, []);
|
|
179
|
+
|
|
180
|
+
// Auto-execute on mount if immediate
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (immediate && !skip) {
|
|
183
|
+
execute();
|
|
184
|
+
}
|
|
185
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
186
|
+
}, [immediate, skip]);
|
|
187
|
+
|
|
188
|
+
// Derived state
|
|
189
|
+
const isSuccess = !isIdle && !isLoading && error === null && data !== null;
|
|
190
|
+
const isError = !isIdle && !isLoading && error !== null;
|
|
191
|
+
|
|
192
|
+
return useMemo(
|
|
193
|
+
() => ({
|
|
194
|
+
// State
|
|
195
|
+
data,
|
|
196
|
+
isLoading,
|
|
197
|
+
error,
|
|
198
|
+
isIdle,
|
|
199
|
+
isSuccess,
|
|
200
|
+
isError,
|
|
201
|
+
|
|
202
|
+
// Actions
|
|
203
|
+
execute,
|
|
204
|
+
retry,
|
|
205
|
+
reset,
|
|
206
|
+
setData: setDataManual,
|
|
207
|
+
setError: setErrorManual,
|
|
208
|
+
}),
|
|
209
|
+
[
|
|
210
|
+
data,
|
|
211
|
+
isLoading,
|
|
212
|
+
error,
|
|
213
|
+
isIdle,
|
|
214
|
+
isSuccess,
|
|
215
|
+
isError,
|
|
216
|
+
execute,
|
|
217
|
+
retry,
|
|
218
|
+
reset,
|
|
219
|
+
setDataManual,
|
|
220
|
+
setErrorManual,
|
|
221
|
+
]
|
|
222
|
+
);
|
|
223
|
+
}
|