@umituz/react-native-ai-fal-provider 2.0.37 → 2.0.39
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/infrastructure/services/fal-models.service.ts +4 -1
- package/src/infrastructure/services/fal-provider-subscription.ts +17 -7
- package/src/infrastructure/services/fal-provider.ts +11 -1
- package/src/infrastructure/services/fal-queue-operations.ts +10 -0
- package/src/infrastructure/services/request-store.ts +53 -12
- package/src/infrastructure/utils/collection-filters.util.ts +5 -167
- package/src/infrastructure/utils/collections/array-filters.util.ts +52 -0
- package/src/infrastructure/utils/collections/array-reducers.util.ts +67 -0
- package/src/infrastructure/utils/collections/array-sorters.util.ts +60 -0
- package/src/infrastructure/utils/collections/index.ts +8 -0
- package/src/infrastructure/utils/cost-tracker.ts +6 -1
- package/src/infrastructure/utils/data-parsers.util.ts +5 -187
- package/src/infrastructure/utils/error-mapper.ts +10 -0
- package/src/infrastructure/utils/fal-error-handler.util.ts +21 -18
- package/src/infrastructure/utils/fal-generation-state-manager.util.ts +9 -2
- package/src/infrastructure/utils/fal-storage.util.ts +7 -2
- package/src/infrastructure/utils/general-helpers.util.ts +5 -146
- package/src/infrastructure/utils/helpers/function-helpers.util.ts +25 -0
- package/src/infrastructure/utils/helpers/index.ts +8 -0
- package/src/infrastructure/utils/helpers/object-helpers.util.ts +44 -0
- package/src/infrastructure/utils/helpers/timing-helpers.util.ts +89 -0
- package/src/infrastructure/utils/input-preprocessor.util.ts +12 -6
- package/src/infrastructure/utils/input-validator.util.ts +34 -3
- package/src/infrastructure/utils/parsers/index.ts +10 -0
- package/src/infrastructure/utils/parsers/json-parsers.util.ts +55 -0
- package/src/infrastructure/utils/parsers/number-helpers.util.ts +19 -0
- package/src/infrastructure/utils/parsers/object-transformers.util.ts +67 -0
- package/src/infrastructure/utils/parsers/object-validators.util.ts +38 -0
- package/src/infrastructure/utils/parsers/value-parsers.util.ts +45 -0
- package/src/infrastructure/utils/type-guards/constants.ts +10 -0
- package/src/infrastructure/utils/type-guards/index.ts +8 -0
- package/src/infrastructure/utils/type-guards/model-type-guards.util.ts +56 -0
- package/src/infrastructure/utils/type-guards/validation-guards.util.ts +80 -0
- package/src/infrastructure/utils/type-guards.util.ts +5 -115
- package/src/presentation/hooks/use-fal-generation.ts +15 -4
- package/src/presentation/hooks/use-models.ts +26 -9
|
@@ -1,191 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Data Parser Utilities
|
|
3
|
-
*
|
|
3
|
+
* @deprecated This file is now split into smaller modules for better maintainability.
|
|
4
|
+
* Import from './parsers' submodules instead.
|
|
5
|
+
*
|
|
6
|
+
* This file re-exports all functions for backward compatibility.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
* Safely parse JSON with fallback
|
|
8
|
-
*/
|
|
9
|
-
export function safeJsonParse<T = unknown>(
|
|
10
|
-
data: string,
|
|
11
|
-
fallback: T
|
|
12
|
-
): T {
|
|
13
|
-
try {
|
|
14
|
-
return JSON.parse(data) as T;
|
|
15
|
-
} catch {
|
|
16
|
-
return fallback;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Safely parse JSON with null fallback
|
|
22
|
-
*/
|
|
23
|
-
export function safeJsonParseOrNull<T = unknown>(data: string): T | null {
|
|
24
|
-
try {
|
|
25
|
-
return JSON.parse(data) as T;
|
|
26
|
-
} catch {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Safely stringify object with fallback
|
|
33
|
-
*/
|
|
34
|
-
export function safeJsonStringify(
|
|
35
|
-
data: unknown,
|
|
36
|
-
fallback: string
|
|
37
|
-
): string {
|
|
38
|
-
try {
|
|
39
|
-
return JSON.stringify(data);
|
|
40
|
-
} catch {
|
|
41
|
-
return fallback;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Check if string is valid JSON
|
|
47
|
-
*/
|
|
48
|
-
export function isValidJson(data: string): boolean {
|
|
49
|
-
try {
|
|
50
|
-
JSON.parse(data);
|
|
51
|
-
return true;
|
|
52
|
-
} catch {
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Validate object structure
|
|
59
|
-
*/
|
|
60
|
-
export function validateObjectStructure<T extends Record<string, unknown>>(
|
|
61
|
-
data: unknown,
|
|
62
|
-
requiredKeys: readonly (keyof T)[]
|
|
63
|
-
): data is T {
|
|
64
|
-
if (!data || typeof data !== "object") {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
for (const key of requiredKeys) {
|
|
69
|
-
if (!(key in data)) {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Validate array of objects
|
|
79
|
-
*/
|
|
80
|
-
export function validateObjectArray<T>(
|
|
81
|
-
data: unknown,
|
|
82
|
-
validator: (item: unknown) => item is T
|
|
83
|
-
): data is T[] {
|
|
84
|
-
if (!Array.isArray(data)) {
|
|
85
|
-
return false;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return data.every(validator);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Parse number with fallback
|
|
93
|
-
*/
|
|
94
|
-
export function parseNumber(value: unknown, fallback: number): number {
|
|
95
|
-
if (typeof value === "number") {
|
|
96
|
-
return value;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (typeof value === "string") {
|
|
100
|
-
const parsed = Number.parseFloat(value);
|
|
101
|
-
return Number.isNaN(parsed) ? fallback : parsed;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return fallback;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Parse boolean with fallback
|
|
109
|
-
*/
|
|
110
|
-
export function parseBoolean(value: unknown, fallback: boolean): boolean {
|
|
111
|
-
if (typeof value === "boolean") {
|
|
112
|
-
return value;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (typeof value === "string") {
|
|
116
|
-
const lower = value.toLowerCase().trim();
|
|
117
|
-
if (lower === "true" || lower === "yes" || lower === "1") {
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
if (lower === "false" || lower === "no" || lower === "0") {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (typeof value === "number") {
|
|
126
|
-
return value !== 0;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return fallback;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Clamp number between min and max
|
|
134
|
-
*/
|
|
135
|
-
export function clampNumber(value: number, min: number, max: number): number {
|
|
136
|
-
return Math.min(Math.max(value, min), max);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Round to decimal places
|
|
141
|
-
*/
|
|
142
|
-
export function roundToDecimals(value: number, decimals: number): number {
|
|
143
|
-
const multiplier = Math.pow(10, decimals);
|
|
144
|
-
return Math.round(value * multiplier) / multiplier;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Deep clone object using JSON
|
|
149
|
-
*/
|
|
150
|
-
export function deepClone<T>(data: T): T {
|
|
151
|
-
return safeJsonParse(safeJsonStringify(data, "{}"), data);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Merge objects with later objects overriding earlier ones
|
|
156
|
-
*/
|
|
157
|
-
export function mergeObjects<T extends Record<string, unknown>>(
|
|
158
|
-
...objects: Partial<T>[]
|
|
159
|
-
): T {
|
|
160
|
-
return Object.assign({}, ...objects) as T;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Pick specified properties from object
|
|
165
|
-
*/
|
|
166
|
-
export function pickProperties<T extends Record<string, unknown>, K extends keyof T>(
|
|
167
|
-
obj: T,
|
|
168
|
-
keys: readonly K[]
|
|
169
|
-
): Pick<T, K> {
|
|
170
|
-
const result = {} as Pick<T, K>;
|
|
171
|
-
for (const key of keys) {
|
|
172
|
-
if (key in obj) {
|
|
173
|
-
result[key] = obj[key];
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return result;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Omit specified properties from object
|
|
181
|
-
*/
|
|
182
|
-
export function omitProperties<T extends Record<string, unknown>, K extends keyof T>(
|
|
183
|
-
obj: T,
|
|
184
|
-
keys: readonly K[]
|
|
185
|
-
): Omit<T, K> {
|
|
186
|
-
const result = { ...obj };
|
|
187
|
-
for (const key of keys) {
|
|
188
|
-
delete result[key];
|
|
189
|
-
}
|
|
190
|
-
return result as Omit<T, K>;
|
|
191
|
-
}
|
|
9
|
+
export * from './parsers';
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FAL Error Mapper - Maps errors to user-friendly info
|
|
3
3
|
*
|
|
4
|
+
* @deprecated This module is a re-export for backward compatibility.
|
|
5
|
+
* Import directly from './fal-error-handler.util' instead.
|
|
6
|
+
*
|
|
4
7
|
* This module re-exports error handling functions from the unified
|
|
5
8
|
* fal-error-handler.util module for backward compatibility.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* // Instead of:
|
|
12
|
+
* import { mapFalError } from './error-mapper';
|
|
13
|
+
*
|
|
14
|
+
* // Use:
|
|
15
|
+
* import { mapFalError } from './fal-error-handler.util';
|
|
6
16
|
*/
|
|
7
17
|
|
|
8
18
|
export {
|
|
@@ -94,34 +94,37 @@ function categorizeError(error: unknown): FalErrorCategory {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
97
|
+
* Build FalErrorInfo from error string and category
|
|
98
98
|
*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
type: category.type,
|
|
105
|
-
messageKey: `fal.errors.${category.messageKey}`,
|
|
106
|
-
retryable: category.retryable,
|
|
107
|
-
originalError: error.message,
|
|
108
|
-
originalErrorName: error.name,
|
|
109
|
-
stack: error.stack,
|
|
110
|
-
statusCode: extractStatusCode(error.message),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const errorString = String(error);
|
|
115
|
-
|
|
99
|
+
function buildErrorInfo(
|
|
100
|
+
category: FalErrorCategory,
|
|
101
|
+
errorString: string,
|
|
102
|
+
errorInstance?: Error
|
|
103
|
+
): FalErrorInfo {
|
|
116
104
|
return {
|
|
117
105
|
type: category.type,
|
|
118
106
|
messageKey: `fal.errors.${category.messageKey}`,
|
|
119
107
|
retryable: category.retryable,
|
|
120
108
|
originalError: errorString,
|
|
109
|
+
originalErrorName: errorInstance?.name,
|
|
110
|
+
stack: errorInstance?.stack,
|
|
121
111
|
statusCode: extractStatusCode(errorString),
|
|
122
112
|
};
|
|
123
113
|
}
|
|
124
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Map error to FalErrorInfo with full error details
|
|
117
|
+
*/
|
|
118
|
+
export function mapFalError(error: unknown): FalErrorInfo {
|
|
119
|
+
const category = categorizeError(error);
|
|
120
|
+
|
|
121
|
+
if (error instanceof Error) {
|
|
122
|
+
return buildErrorInfo(category, error.message, error);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return buildErrorInfo(category, String(error));
|
|
126
|
+
}
|
|
127
|
+
|
|
125
128
|
/**
|
|
126
129
|
* Parse FAL error and return user-friendly message
|
|
127
130
|
*/
|
|
@@ -25,6 +25,7 @@ export class FalGenerationStateManager<T> {
|
|
|
25
25
|
private isMounted = true;
|
|
26
26
|
private currentRequestId: string | null = null;
|
|
27
27
|
private lastRequest: { endpoint: string; input: FalJobInput } | null = null;
|
|
28
|
+
private lastNotifiedStatus: string | null = null; // Track last status to prevent duplicate callbacks
|
|
28
29
|
|
|
29
30
|
constructor(
|
|
30
31
|
private options?: GenerationStateOptions<T>
|
|
@@ -57,6 +58,7 @@ export class FalGenerationStateManager<T> {
|
|
|
57
58
|
clearLastRequest(): void {
|
|
58
59
|
this.lastRequest = null;
|
|
59
60
|
this.currentRequestId = null;
|
|
61
|
+
this.lastNotifiedStatus = null;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
handleQueueUpdate(status: FalQueueStatus): void {
|
|
@@ -77,8 +79,13 @@ export class FalGenerationStateManager<T> {
|
|
|
77
79
|
queuePosition: status.queuePosition,
|
|
78
80
|
};
|
|
79
81
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
// Only notify if status actually changed (idempotent callbacks)
|
|
83
|
+
const statusKey = `${normalizedStatus.status}-${normalizedStatus.requestId}`;
|
|
84
|
+
if (this.lastNotifiedStatus !== statusKey) {
|
|
85
|
+
this.lastNotifiedStatus = statusKey;
|
|
86
|
+
this.options?.onQueueUpdate?.(normalizedStatus);
|
|
87
|
+
this.options?.onProgress?.(normalizedStatus);
|
|
88
|
+
}
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
handleResult(result: T): void {
|
|
@@ -30,8 +30,13 @@ export async function uploadToFalStorage(base64: string): Promise<string> {
|
|
|
30
30
|
try {
|
|
31
31
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
32
32
|
await deleteTempFile(tempUri);
|
|
33
|
-
} catch {
|
|
34
|
-
//
|
|
33
|
+
} catch (cleanupError) {
|
|
34
|
+
// Log cleanup errors to prevent disk space leaks
|
|
35
|
+
console.warn(
|
|
36
|
+
`[fal-storage] Failed to delete temp file: ${tempUri}`,
|
|
37
|
+
cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
|
|
38
|
+
);
|
|
39
|
+
// Don't throw - cleanup errors shouldn't fail the upload
|
|
35
40
|
}
|
|
36
41
|
}
|
|
37
42
|
}
|
|
@@ -1,150 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* General Helper Utilities
|
|
3
|
-
*
|
|
3
|
+
* @deprecated This file is now split into smaller modules for better maintainability.
|
|
4
|
+
* Import from './helpers' submodules instead.
|
|
5
|
+
*
|
|
6
|
+
* This file re-exports all functions for backward compatibility.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
* Build error message with context
|
|
8
|
-
*/
|
|
9
|
-
export function buildErrorMessage(
|
|
10
|
-
type: string,
|
|
11
|
-
context: Record<string, unknown>
|
|
12
|
-
): string {
|
|
13
|
-
const contextStr = Object.entries(context)
|
|
14
|
-
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
15
|
-
.join(", ");
|
|
16
|
-
return `${type}${contextStr ? ` (${contextStr})` : ""}`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Check if value is defined (not null or undefined)
|
|
21
|
-
*/
|
|
22
|
-
export function isDefined<T>(value: T | null | undefined): value is T {
|
|
23
|
-
return value !== null && value !== undefined;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Filter out null and undefined values from object
|
|
28
|
-
*/
|
|
29
|
-
export function removeNullish<T extends Record<string, unknown>>(
|
|
30
|
-
obj: T
|
|
31
|
-
): Partial<T> {
|
|
32
|
-
return Object.fromEntries(
|
|
33
|
-
Object.entries(obj).filter(([_, value]) => isDefined(value))
|
|
34
|
-
) as Partial<T>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Generate unique ID
|
|
39
|
-
*/
|
|
40
|
-
export function generateUniqueId(prefix: string = ""): string {
|
|
41
|
-
const timestamp = Date.now().toString(36);
|
|
42
|
-
const randomStr = Math.random().toString(36).substring(2, 9);
|
|
43
|
-
return prefix ? `${prefix}_${timestamp}${randomStr}` : `${timestamp}${randomStr}`;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Create debounced function
|
|
48
|
-
*/
|
|
49
|
-
export function debounce<T extends (...args: never[]) => void>(
|
|
50
|
-
func: T,
|
|
51
|
-
wait: number
|
|
52
|
-
): (...args: Parameters<T>) => void {
|
|
53
|
-
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
54
|
-
|
|
55
|
-
return function debounced(...args: Parameters<T>) {
|
|
56
|
-
if (timeoutId) {
|
|
57
|
-
clearTimeout(timeoutId);
|
|
58
|
-
}
|
|
59
|
-
timeoutId = setTimeout(() => {
|
|
60
|
-
func(...args);
|
|
61
|
-
}, wait);
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Create throttled function
|
|
67
|
-
*/
|
|
68
|
-
export function throttle<T extends (...args: never[]) => void>(
|
|
69
|
-
func: T,
|
|
70
|
-
limit: number
|
|
71
|
-
): (...args: Parameters<T>) => void {
|
|
72
|
-
let inThrottle = false;
|
|
73
|
-
|
|
74
|
-
return function throttled(...args: Parameters<T>) {
|
|
75
|
-
if (!inThrottle) {
|
|
76
|
-
func(...args);
|
|
77
|
-
inThrottle = true;
|
|
78
|
-
setTimeout(() => {
|
|
79
|
-
inThrottle = false;
|
|
80
|
-
}, limit);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Sleep for specified milliseconds
|
|
87
|
-
*/
|
|
88
|
-
export function sleep(ms: number): Promise<void> {
|
|
89
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Retry function with exponential backoff
|
|
94
|
-
*/
|
|
95
|
-
export async function retry<T>(
|
|
96
|
-
func: () => Promise<T>,
|
|
97
|
-
options: {
|
|
98
|
-
maxRetries?: number;
|
|
99
|
-
baseDelay?: number;
|
|
100
|
-
maxDelay?: number;
|
|
101
|
-
shouldRetry?: (error: unknown) => boolean;
|
|
102
|
-
} = {}
|
|
103
|
-
): Promise<T> {
|
|
104
|
-
const {
|
|
105
|
-
maxRetries = 3,
|
|
106
|
-
baseDelay = 1000,
|
|
107
|
-
maxDelay = 10000,
|
|
108
|
-
shouldRetry = () => true,
|
|
109
|
-
} = options;
|
|
110
|
-
|
|
111
|
-
let lastError: unknown;
|
|
112
|
-
|
|
113
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
114
|
-
try {
|
|
115
|
-
return await func();
|
|
116
|
-
} catch (error) {
|
|
117
|
-
lastError = error;
|
|
118
|
-
|
|
119
|
-
if (attempt === maxRetries || !shouldRetry(error)) {
|
|
120
|
-
throw error;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
124
|
-
await sleep(delay);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
throw lastError;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* No-op function
|
|
133
|
-
*/
|
|
134
|
-
export function noop(): void {
|
|
135
|
-
// Intentionally empty
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Identity function
|
|
140
|
-
*/
|
|
141
|
-
export function identity<T>(value: T): T {
|
|
142
|
-
return value;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Constant function (returns same value regardless of input)
|
|
147
|
-
*/
|
|
148
|
-
export function constant<T>(value: T): () => T {
|
|
149
|
-
return () => value;
|
|
150
|
-
}
|
|
9
|
+
export * from './helpers';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Function Helper Utilities
|
|
3
|
+
* Common functional programming helpers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* No-op function
|
|
8
|
+
*/
|
|
9
|
+
export function noop(): void {
|
|
10
|
+
// Intentionally empty
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Identity function
|
|
15
|
+
*/
|
|
16
|
+
export function identity<T>(value: T): T {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Constant function (returns same value regardless of input)
|
|
22
|
+
*/
|
|
23
|
+
export function constant<T>(value: T): () => T {
|
|
24
|
+
return () => value;
|
|
25
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object Helper Utilities
|
|
3
|
+
* Object manipulation and validation helpers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Build error message with context
|
|
8
|
+
*/
|
|
9
|
+
export function buildErrorMessage(
|
|
10
|
+
type: string,
|
|
11
|
+
context: Record<string, unknown>
|
|
12
|
+
): string {
|
|
13
|
+
const contextStr = Object.entries(context)
|
|
14
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
15
|
+
.join(", ");
|
|
16
|
+
return `${type}${contextStr ? ` (${contextStr})` : ""}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if value is defined (not null or undefined)
|
|
21
|
+
*/
|
|
22
|
+
export function isDefined<T>(value: T | null | undefined): value is T {
|
|
23
|
+
return value !== null && value !== undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Filter out null and undefined values from object
|
|
28
|
+
*/
|
|
29
|
+
export function removeNullish<T extends Record<string, unknown>>(
|
|
30
|
+
obj: T
|
|
31
|
+
): Partial<T> {
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
Object.entries(obj).filter(([_, value]) => isDefined(value))
|
|
34
|
+
) as Partial<T>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generate unique ID
|
|
39
|
+
*/
|
|
40
|
+
export function generateUniqueId(prefix: string = ""): string {
|
|
41
|
+
const timestamp = Date.now().toString(36);
|
|
42
|
+
const randomStr = Math.random().toString(36).substring(2, 9);
|
|
43
|
+
return prefix ? `${prefix}_${timestamp}${randomStr}` : `${timestamp}${randomStr}`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Timing Helper Utilities
|
|
3
|
+
* Debounce, throttle, sleep, and retry operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create debounced function
|
|
8
|
+
*/
|
|
9
|
+
export function debounce<T extends (...args: never[]) => void>(
|
|
10
|
+
func: T,
|
|
11
|
+
wait: number
|
|
12
|
+
): (...args: Parameters<T>) => void {
|
|
13
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
14
|
+
|
|
15
|
+
return function debounced(...args: Parameters<T>) {
|
|
16
|
+
if (timeoutId) {
|
|
17
|
+
clearTimeout(timeoutId);
|
|
18
|
+
}
|
|
19
|
+
timeoutId = setTimeout(() => {
|
|
20
|
+
func(...args);
|
|
21
|
+
}, wait);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create throttled function
|
|
27
|
+
*/
|
|
28
|
+
export function throttle<T extends (...args: never[]) => void>(
|
|
29
|
+
func: T,
|
|
30
|
+
limit: number
|
|
31
|
+
): (...args: Parameters<T>) => void {
|
|
32
|
+
let inThrottle = false;
|
|
33
|
+
|
|
34
|
+
return function throttled(...args: Parameters<T>) {
|
|
35
|
+
if (!inThrottle) {
|
|
36
|
+
func(...args);
|
|
37
|
+
inThrottle = true;
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
inThrottle = false;
|
|
40
|
+
}, limit);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sleep for specified milliseconds
|
|
47
|
+
*/
|
|
48
|
+
export function sleep(ms: number): Promise<void> {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Retry function with exponential backoff
|
|
54
|
+
*/
|
|
55
|
+
export async function retry<T>(
|
|
56
|
+
func: () => Promise<T>,
|
|
57
|
+
options: {
|
|
58
|
+
maxRetries?: number;
|
|
59
|
+
baseDelay?: number;
|
|
60
|
+
maxDelay?: number;
|
|
61
|
+
shouldRetry?: (error: unknown) => boolean;
|
|
62
|
+
} = {}
|
|
63
|
+
): Promise<T> {
|
|
64
|
+
const {
|
|
65
|
+
maxRetries = 3,
|
|
66
|
+
baseDelay = 1000,
|
|
67
|
+
maxDelay = 10000,
|
|
68
|
+
shouldRetry = () => true,
|
|
69
|
+
} = options;
|
|
70
|
+
|
|
71
|
+
let lastError: unknown;
|
|
72
|
+
|
|
73
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
74
|
+
try {
|
|
75
|
+
return await func();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
lastError = error;
|
|
78
|
+
|
|
79
|
+
if (attempt === maxRetries || !shouldRetry(error)) {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
84
|
+
await sleep(delay);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw lastError;
|
|
89
|
+
}
|
|
@@ -38,9 +38,12 @@ export async function preprocessInput(
|
|
|
38
38
|
const uploadPromise = uploadToFalStorage(value)
|
|
39
39
|
.then((url) => {
|
|
40
40
|
result[key] = url;
|
|
41
|
+
return url;
|
|
41
42
|
})
|
|
42
43
|
.catch((error) => {
|
|
43
|
-
|
|
44
|
+
const errorMessage = `Failed to upload ${key}: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
45
|
+
console.error(`[preprocessInput] ${errorMessage}`);
|
|
46
|
+
throw new Error(errorMessage);
|
|
44
47
|
});
|
|
45
48
|
|
|
46
49
|
uploadPromises.push(uploadPromise);
|
|
@@ -62,11 +65,14 @@ export async function preprocessInput(
|
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
if (isBase64DataUri(imageUrl)) {
|
|
65
|
-
const uploadPromise = uploadToFalStorage(imageUrl)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
68
|
+
const uploadPromise = uploadToFalStorage(imageUrl)
|
|
69
|
+
.then((url) => url)
|
|
70
|
+
.catch((error) => {
|
|
71
|
+
const errorMessage = `Failed to upload image_urls[${i}]: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
72
|
+
console.error(`[preprocessInput] ${errorMessage}`);
|
|
73
|
+
errors.push(errorMessage);
|
|
74
|
+
throw new Error(errorMessage);
|
|
75
|
+
});
|
|
70
76
|
uploadTasks.push({ index: i, url: uploadPromise });
|
|
71
77
|
} else if (typeof imageUrl === "string") {
|
|
72
78
|
uploadTasks.push({ index: i, url: imageUrl });
|