@umituz/react-native-ai-fal-provider 2.0.28 → 2.0.30

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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * FAL Error Handler
3
+ * Unified error handling for FAL AI operations
4
+ */
5
+
6
+ import type { FalErrorInfo, FalErrorCategory, FalErrorType } from "../../domain/entities/error.types";
7
+ import { FalErrorType as ErrorTypeEnum } from "../../domain/entities/error.types";
8
+ import { safeJsonParseOrNull } from "./data-parsers.util";
9
+
10
+ const STATUS_CODES = ["400", "401", "402", "403", "404", "422", "429", "500", "502", "503", "504"];
11
+
12
+ interface FalApiErrorDetail {
13
+ msg?: string;
14
+ type?: string;
15
+ loc?: string[];
16
+ }
17
+
18
+ interface FalApiError {
19
+ body?: { detail?: FalApiErrorDetail[] } | string;
20
+ message?: string;
21
+ }
22
+
23
+ const ERROR_PATTERNS: Record<FalErrorType, string[]> = {
24
+ [ErrorTypeEnum.NETWORK]: ["network", "fetch", "connection", "econnrefused", "enotfound", "etimedout"],
25
+ [ErrorTypeEnum.TIMEOUT]: ["timeout", "timed out"],
26
+ [ErrorTypeEnum.IMAGE_TOO_SMALL]: ["image_too_small", "image dimensions are too small", "minimum dimensions"],
27
+ [ErrorTypeEnum.VALIDATION]: ["validation", "invalid", "unprocessable", "422", "bad request", "400"],
28
+ [ErrorTypeEnum.CONTENT_POLICY]: ["content_policy", "content policy", "policy violation", "nsfw", "inappropriate"],
29
+ [ErrorTypeEnum.RATE_LIMIT]: ["rate limit", "too many requests", "429"],
30
+ [ErrorTypeEnum.AUTHENTICATION]: ["unauthorized", "401", "forbidden", "403", "api key", "authentication"],
31
+ [ErrorTypeEnum.QUOTA_EXCEEDED]: ["quota exceeded", "insufficient credits", "billing", "payment required", "402"],
32
+ [ErrorTypeEnum.MODEL_NOT_FOUND]: ["model not found", "endpoint not found", "404", "not found"],
33
+ [ErrorTypeEnum.API_ERROR]: ["api error", "502", "503", "504", "500", "internal server error"],
34
+ [ErrorTypeEnum.UNKNOWN]: [],
35
+ };
36
+
37
+ const RETRYABLE_TYPES = new Set([
38
+ ErrorTypeEnum.NETWORK,
39
+ ErrorTypeEnum.TIMEOUT,
40
+ ErrorTypeEnum.RATE_LIMIT,
41
+ ]);
42
+
43
+ /**
44
+ * Extract HTTP status code from error message
45
+ */
46
+ function extractStatusCode(errorString: string): number | undefined {
47
+ const code = STATUS_CODES.find((c) => errorString.includes(c));
48
+ return code ? parseInt(code, 10) : undefined;
49
+ }
50
+
51
+ /**
52
+ * Parse FAL API error and extract user-friendly message
53
+ */
54
+ function parseFalApiError(error: unknown): string {
55
+ const fallback = error instanceof Error ? error.message : String(error);
56
+
57
+ const falError = error as FalApiError;
58
+ if (!falError?.body) return fallback;
59
+
60
+ const body = typeof falError.body === "string"
61
+ ? safeJsonParseOrNull<{ detail?: FalApiErrorDetail[] }>(falError.body)
62
+ : falError.body;
63
+
64
+ const detail = body?.detail?.[0];
65
+ return detail?.msg ?? falError.message ?? fallback;
66
+ }
67
+
68
+ /**
69
+ * Check if error string matches any of the provided patterns
70
+ */
71
+ function matchesPatterns(errorString: string, patterns: string[]): boolean {
72
+ return patterns.some((pattern) => errorString.includes(pattern));
73
+ }
74
+
75
+ /**
76
+ * Categorize FAL error based on error message patterns
77
+ */
78
+ function categorizeError(error: unknown): FalErrorCategory {
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ const errorString = message.toLowerCase();
81
+
82
+ for (const [type, patterns] of Object.entries(ERROR_PATTERNS)) {
83
+ if (patterns.length > 0 && matchesPatterns(errorString, patterns)) {
84
+ const errorType = type as FalErrorType;
85
+ return {
86
+ type: errorType,
87
+ messageKey: errorType,
88
+ retryable: RETRYABLE_TYPES.has(errorType),
89
+ };
90
+ }
91
+ }
92
+
93
+ return { type: ErrorTypeEnum.UNKNOWN, messageKey: "unknown", retryable: false };
94
+ }
95
+
96
+ /**
97
+ * Map error to FalErrorInfo with full error details
98
+ */
99
+ export function mapFalError(error: unknown): FalErrorInfo {
100
+ const category = categorizeError(error);
101
+
102
+ if (error instanceof Error) {
103
+ return {
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
+
116
+ return {
117
+ type: category.type,
118
+ messageKey: `fal.errors.${category.messageKey}`,
119
+ retryable: category.retryable,
120
+ originalError: errorString,
121
+ statusCode: extractStatusCode(errorString),
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Parse FAL error and return user-friendly message
127
+ */
128
+ export function parseFalError(error: unknown): string {
129
+ const userMessage = parseFalApiError(error);
130
+ if (!userMessage || userMessage.trim().length === 0) {
131
+ return "An unknown error occurred. Please try again.";
132
+ }
133
+ return userMessage;
134
+ }
135
+
136
+ /**
137
+ * Categorize FAL error
138
+ */
139
+ export function categorizeFalError(error: unknown): FalErrorCategory {
140
+ return categorizeError(error);
141
+ }
142
+
143
+ /**
144
+ * Check if FAL error is retryable
145
+ */
146
+ export function isFalErrorRetryable(error: unknown): boolean {
147
+ return categorizeFalError(error).retryable;
148
+ }
149
+
150
+ /**
151
+ * Extract status code from error
152
+ */
153
+ export { extractStatusCode };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * FAL Generation State Manager
3
+ * Manages state and refs for FAL generation operations
4
+ */
5
+
6
+ import type { FalJobInput, FalLogEntry, FalQueueStatus } from "../../domain/entities/fal.types";
7
+
8
+ export interface GenerationState<T> {
9
+ data: T | null;
10
+ error: Error | null;
11
+ isLoading: boolean;
12
+ isCancelling: boolean;
13
+ requestId: string | null;
14
+ lastRequest: { endpoint: string; input: FalJobInput } | null;
15
+ }
16
+
17
+ export interface GenerationStateOptions<T> {
18
+ onQueueUpdate?: (status: FalQueueStatus) => void;
19
+ onProgress?: (status: FalQueueStatus) => void;
20
+ onError?: (error: Error) => void;
21
+ onResult?: (result: T) => void;
22
+ }
23
+
24
+ export class FalGenerationStateManager<T> {
25
+ private isMounted = true;
26
+ private currentRequestId: string | null = null;
27
+ private lastRequest: { endpoint: string; input: FalJobInput } | null = null;
28
+
29
+ constructor(
30
+ private options?: GenerationStateOptions<T>
31
+ ) {}
32
+
33
+ setIsMounted(mounted: boolean): void {
34
+ this.isMounted = mounted;
35
+ }
36
+
37
+ checkMounted(): boolean {
38
+ return this.isMounted;
39
+ }
40
+
41
+ setCurrentRequestId(requestId: string | null): void {
42
+ this.currentRequestId = requestId;
43
+ }
44
+
45
+ getCurrentRequestId(): string | null {
46
+ return this.currentRequestId;
47
+ }
48
+
49
+ setLastRequest(endpoint: string, input: FalJobInput): void {
50
+ this.lastRequest = { endpoint, input };
51
+ }
52
+
53
+ getLastRequest(): { endpoint: string; input: FalJobInput } | null {
54
+ return this.lastRequest;
55
+ }
56
+
57
+ clearLastRequest(): void {
58
+ this.lastRequest = null;
59
+ this.currentRequestId = null;
60
+ }
61
+
62
+ handleQueueUpdate(status: FalQueueStatus): void {
63
+ if (!this.isMounted) return;
64
+
65
+ if (status.requestId) {
66
+ this.currentRequestId = status.requestId;
67
+ }
68
+
69
+ const normalizedStatus: FalQueueStatus = {
70
+ status: status.status,
71
+ requestId: status.requestId ?? this.currentRequestId ?? "",
72
+ logs: status.logs?.map((log: FalLogEntry) => ({
73
+ message: log.message,
74
+ level: log.level,
75
+ timestamp: log.timestamp,
76
+ })),
77
+ queuePosition: status.queuePosition,
78
+ };
79
+
80
+ this.options?.onQueueUpdate?.(normalizedStatus);
81
+ this.options?.onProgress?.(normalizedStatus);
82
+ }
83
+
84
+ handleResult(result: T): void {
85
+ if (!this.isMounted) return;
86
+ this.options?.onResult?.(result);
87
+ }
88
+
89
+ handleError(error: Error): void {
90
+ if (!this.isMounted) return;
91
+ this.options?.onError?.(error);
92
+ }
93
+ }
@@ -17,21 +17,21 @@ export async function uploadToFalStorage(base64: string): Promise<string> {
17
17
  // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
18
18
  const tempUri = (await base64ToTempFile(base64));
19
19
 
20
+ if (!tempUri) {
21
+ throw new Error("Failed to create temporary file from base64 data");
22
+ }
23
+
20
24
  try {
21
25
  const response = await fetch(tempUri);
22
26
  const blob = await response.blob();
23
27
  const url = await fal.storage.upload(blob);
24
28
  return url;
25
29
  } finally {
26
- if (tempUri) {
27
- try {
28
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
29
- await deleteTempFile(tempUri);
30
- } catch (cleanupError) {
31
- // Log cleanup failure but don't throw
32
- // eslint-disable-next-line no-console
33
- console.warn(`Failed to cleanup temp file ${tempUri}:`, cleanupError);
34
- }
30
+ try {
31
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
32
+ await deleteTempFile(tempUri);
33
+ } catch {
34
+ // Silently ignore cleanup errors
35
35
  }
36
36
  }
37
37
  }
@@ -1,208 +1,31 @@
1
1
  /**
2
2
  * Formatting Utilities
3
3
  * Common formatting functions for display and data presentation
4
- */
5
-
6
- /**
7
- * Format number with decimal places
8
- */
9
- export function formatNumber(value: number, decimals: number = 2): string {
10
- if (Number.isNaN(value) || !Number.isFinite(value)) {
11
- return "0";
12
- }
13
-
14
- // Return integer if no decimal part
15
- if (value % 1 === 0) {
16
- return value.toString();
17
- }
18
-
19
- return value.toFixed(decimals);
20
- }
21
-
22
- /**
23
- * Format currency amount
24
- */
25
- export function formatCurrency(amount: number, currency: string = "USD"): string {
26
- const formatted = formatNumber(amount, 2);
27
- return `${currency} ${formatted}`;
28
- }
29
-
30
- /**
31
- * Format bytes to human-readable size
32
- */
33
- export function formatBytes(bytes: number, decimals: number = 2): string {
34
- if (bytes === 0) return "0 Bytes";
35
-
36
- const k = 1024;
37
- const dm = decimals < 0 ? 0 : decimals;
38
- const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
39
- const i = Math.floor(Math.log(bytes) / Math.log(k));
40
-
41
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
42
- }
43
-
44
- /**
45
- * Format duration in milliseconds to human-readable string
46
- */
47
- export function formatDuration(milliseconds: number): string {
48
- if (milliseconds < 1000) {
49
- return `${milliseconds}ms`;
50
- }
51
-
52
- const seconds = Math.floor(milliseconds / 1000);
53
- if (seconds < 60) {
54
- return `${seconds}s`;
55
- }
56
-
57
- const minutes = Math.floor(seconds / 60);
58
- const remainingSeconds = seconds % 60;
59
- if (minutes < 60) {
60
- return remainingSeconds > 0
61
- ? `${minutes}m ${remainingSeconds}s`
62
- : `${minutes}m`;
63
- }
64
-
65
- const hours = Math.floor(minutes / 60);
66
- const remainingMinutes = minutes % 60;
67
- return remainingMinutes > 0
68
- ? `${hours}h ${remainingMinutes}m`
69
- : `${hours}h`;
70
- }
71
-
72
- /**
73
- * Format percentage
74
- */
75
- export function formatPercentage(value: number, decimals: number = 1): string {
76
- if (value < 0) return "0%";
77
- if (value > 100) return "100%";
78
- return `${formatNumber(value, decimals)}%`;
79
- }
80
-
81
- /**
82
- * Format date to locale string
83
- */
84
- export function formatDate(date: Date | string, locale: string = "en-US"): string {
85
- const dateObj = typeof date === "string" ? new Date(date) : date;
86
- return dateObj.toLocaleDateString(locale, {
87
- year: "numeric",
88
- month: "short",
89
- day: "numeric",
90
- });
91
- }
92
-
93
- /**
94
- * Format date and time to locale string
95
- */
96
- export function formatDateTime(date: Date | string, locale: string = "en-US"): string {
97
- const dateObj = typeof date === "string" ? new Date(date) : date;
98
- return dateObj.toLocaleString(locale, {
99
- year: "numeric",
100
- month: "short",
101
- day: "numeric",
102
- hour: "2-digit",
103
- minute: "2-digit",
104
- });
105
- }
106
-
107
- /**
108
- * Format relative time (e.g., "2 hours ago")
109
- */
110
- export function formatRelativeTime(date: Date | string, locale: string = "en-US"): string {
111
- const dateObj = typeof date === "string" ? new Date(date) : date;
112
- const now = new Date();
113
- const diffMs = now.getTime() - dateObj.getTime();
114
- const diffSec = Math.floor(diffMs / 1000);
115
- const diffMin = Math.floor(diffSec / 60);
116
- const diffHour = Math.floor(diffMin / 60);
117
- const diffDay = Math.floor(diffHour / 24);
118
-
119
- const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
120
-
121
- if (diffSec < 60) {
122
- return rtf.format(-diffSec, "second");
123
- }
124
- if (diffMin < 60) {
125
- return rtf.format(-diffMin, "minute");
126
- }
127
- if (diffHour < 24) {
128
- return rtf.format(-diffHour, "hour");
129
- }
130
- if (diffDay < 30) {
131
- return rtf.format(-diffDay, "day");
132
- }
133
- if (diffDay < 365) {
134
- const months = Math.floor(diffDay / 30);
135
- return rtf.format(-months, "month");
136
- }
137
- const years = Math.floor(diffDay / 365);
138
- return rtf.format(-years, "year");
139
- }
140
-
141
- /**
142
- * Truncate text with ellipsis
143
- */
144
- export function truncateText(text: string, maxLength: number): string {
145
- if (text.length <= maxLength) {
146
- return text;
147
- }
148
- return text.slice(0, maxLength - 3) + "...";
149
- }
150
-
151
- /**
152
- * Capitalize first letter of string
153
- */
154
- export function capitalize(text: string): string {
155
- if (!text) return text;
156
- return text.charAt(0).toUpperCase() + text.slice(1);
157
- }
158
-
159
- /**
160
- * Convert string to title case
161
- */
162
- export function toTitleCase(text: string): string {
163
- return text
164
- .toLowerCase()
165
- .split(" ")
166
- .map((word) => capitalize(word))
167
- .join(" ");
168
- }
169
-
170
- /**
171
- * Convert string to slug
172
- */
173
- export function toSlug(text: string): string {
174
- return text
175
- .toLowerCase()
176
- .trim()
177
- .replace(/[^\w\s-]/g, "")
178
- .replace(/[\s_-]+/g, "-")
179
- .replace(/^-+|-+$/g, "");
180
- }
181
-
182
- /**
183
- * Format list of items with conjunction
184
- */
185
- export function formatList(items: readonly string[], conjunction: string = "and"): string {
186
- if (items.length === 0) return "";
187
- if (items.length === 1) return items[0] ?? "";
188
- if (items.length === 2) return items.join(` ${conjunction} `);
189
-
190
- const allButLast = items.slice(0, -1);
191
- const last = items[items.length - 1];
192
- return `${allButLast.join(", ")}, ${conjunction} ${last}`;
193
- }
194
-
195
- /**
196
- * Pluralize word based on count
197
- */
198
- export function pluralize(word: string, count: number): string {
199
- if (count === 1) return word;
200
- return `${word}s`;
201
- }
202
-
203
- /**
204
- * Format count with plural word
205
- */
206
- export function formatCount(word: string, count: number): string {
207
- return `${count} ${pluralize(word, count)}`;
208
- }
4
+ *
5
+ * This module re-exports formatting utilities from specialized modules
6
+ * for better organization and maintainability.
7
+ */
8
+
9
+ export {
10
+ formatDate,
11
+ formatDateTime,
12
+ formatRelativeTime,
13
+ } from "./date-format.util";
14
+
15
+ export {
16
+ formatNumber,
17
+ formatCurrency,
18
+ formatBytes,
19
+ formatDuration,
20
+ formatPercentage,
21
+ } from "./number-format.util";
22
+
23
+ export {
24
+ truncateText,
25
+ capitalize,
26
+ toTitleCase,
27
+ toSlug,
28
+ formatList,
29
+ pluralize,
30
+ formatCount,
31
+ } from "./string-format.util";
@@ -39,6 +39,7 @@ export {
39
39
  mapFalError,
40
40
  parseFalError,
41
41
  isFalErrorRetryable,
42
+ extractStatusCode,
42
43
  } from "./error-mapper";
43
44
 
44
45
  export {
@@ -151,3 +152,9 @@ export {
151
152
  type ValidationError,
152
153
  } from "./input-validator.util";
153
154
 
155
+ // FAL generation state manager
156
+ export {
157
+ FalGenerationStateManager,
158
+ type GenerationState,
159
+ type GenerationStateOptions,
160
+ } from "./fal-generation-state-manager.util";
@@ -29,7 +29,7 @@ export async function preprocessInput(
29
29
  input: Record<string, unknown>,
30
30
  ): Promise<Record<string, unknown>> {
31
31
  const result = { ...input };
32
- const uploadPromises: Promise<void>[] = [];
32
+ const uploadPromises: Promise<unknown>[] = [];
33
33
 
34
34
  // Handle individual image URL keys
35
35
  for (const key of IMAGE_URL_KEYS) {
@@ -50,7 +50,7 @@ export async function preprocessInput(
50
50
  // Handle image_urls array (for multi-person generation)
51
51
  if (Array.isArray(result.image_urls) && result.image_urls.length > 0) {
52
52
  const imageUrls = result.image_urls as unknown[];
53
- const processedUrls: string[] = [];
53
+ const uploadTasks: Array<{ index: number; url: string | Promise<string> }> = [];
54
54
  const errors: string[] = [];
55
55
 
56
56
  for (let i = 0; i < imageUrls.length; i++) {
@@ -62,19 +62,14 @@ export async function preprocessInput(
62
62
  }
63
63
 
64
64
  if (isBase64DataUri(imageUrl)) {
65
- const index = i;
66
- const uploadPromise = uploadToFalStorage(imageUrl)
67
- .then((url) => {
68
- processedUrls[index] = url;
69
- })
70
- .catch((error) => {
71
- errors.push(`Failed to upload image_urls[${index}]: ${error instanceof Error ? error.message : "Unknown error"}`);
72
- throw new Error(`Failed to upload image_urls[${index}]: ${error instanceof Error ? error.message : "Unknown error"}`);
73
- });
74
-
75
- uploadPromises.push(uploadPromise);
65
+ const uploadPromise = uploadToFalStorage(imageUrl).catch((error) => {
66
+ const errorMessage = `Failed to upload image_urls[${i}]: ${error instanceof Error ? error.message : "Unknown error"}`;
67
+ errors.push(errorMessage);
68
+ throw new Error(errorMessage);
69
+ });
70
+ uploadTasks.push({ index: i, url: uploadPromise });
76
71
  } else if (typeof imageUrl === "string") {
77
- processedUrls[i] = imageUrl;
72
+ uploadTasks.push({ index: i, url: imageUrl });
78
73
  } else {
79
74
  errors.push(`image_urls[${i}] has invalid type: ${typeof imageUrl}`);
80
75
  }
@@ -84,6 +79,13 @@ export async function preprocessInput(
84
79
  throw new Error(`Image URL validation failed:\n${errors.join('\n')}`);
85
80
  }
86
81
 
82
+ // Wait for all uploads and build the final array without sparse elements
83
+ const processedUrls = await Promise.all(
84
+ uploadTasks
85
+ .sort((a, b) => a.index - b.index)
86
+ .map((task) => Promise.resolve(task.url))
87
+ );
88
+
87
89
  result.image_urls = processedUrls;
88
90
  }
89
91
 
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Number Formatting Utilities
3
+ * Functions for formatting numbers and quantities
4
+ */
5
+
6
+ /**
7
+ * Format number with decimal places
8
+ */
9
+ export function formatNumber(value: number, decimals: number = 2): string {
10
+ if (Number.isNaN(value) || !Number.isFinite(value)) {
11
+ return "0";
12
+ }
13
+
14
+ // Return integer if no decimal part
15
+ if (value % 1 === 0) {
16
+ return value.toString();
17
+ }
18
+
19
+ return value.toFixed(decimals);
20
+ }
21
+
22
+ /**
23
+ * Format currency amount
24
+ */
25
+ export function formatCurrency(amount: number, currency: string = "USD"): string {
26
+ const formatted = formatNumber(amount, 2);
27
+ return `${currency} ${formatted}`;
28
+ }
29
+
30
+ /**
31
+ * Format bytes to human-readable size
32
+ */
33
+ export function formatBytes(bytes: number, decimals: number = 2): string {
34
+ if (bytes === 0) return "0 Bytes";
35
+
36
+ const k = 1024;
37
+ const dm = decimals < 0 ? 0 : decimals;
38
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB"];
39
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
40
+
41
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
42
+ }
43
+
44
+ /**
45
+ * Format duration in milliseconds to human-readable string
46
+ */
47
+ export function formatDuration(milliseconds: number): string {
48
+ if (milliseconds < 1000) {
49
+ return `${milliseconds}ms`;
50
+ }
51
+
52
+ const seconds = Math.floor(milliseconds / 1000);
53
+ if (seconds < 60) {
54
+ return `${seconds}s`;
55
+ }
56
+
57
+ const minutes = Math.floor(seconds / 60);
58
+ const remainingSeconds = seconds % 60;
59
+ if (minutes < 60) {
60
+ return remainingSeconds > 0
61
+ ? `${minutes}m ${remainingSeconds}s`
62
+ : `${minutes}m`;
63
+ }
64
+
65
+ const hours = Math.floor(minutes / 60);
66
+ const remainingMinutes = minutes % 60;
67
+ return remainingMinutes > 0
68
+ ? `${hours}h ${remainingMinutes}m`
69
+ : `${hours}h`;
70
+ }
71
+
72
+ /**
73
+ * Format percentage
74
+ */
75
+ export function formatPercentage(value: number, decimals: number = 1): string {
76
+ if (value < 0) return "0%";
77
+ if (value > 100) return "100%";
78
+ return `${formatNumber(value, decimals)}%`;
79
+ }