@umituz/react-native-ai-fal-provider 3.2.31 → 3.2.32

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-fal-provider",
3
- "version": "3.2.31",
3
+ "version": "3.2.32",
4
4
  "description": "FAL AI provider for React Native - implements IAIProvider interface for unified AI generation",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Input Preprocessor Utility
3
3
  * Detects and uploads base64/local file images to FAL storage before API calls
4
+ *
5
+ * Upload strategy:
6
+ * - Array fields (image_urls): SEQUENTIAL uploads to avoid bandwidth contention
7
+ * - Individual fields (image_url, face_image_url): parallel (typically only 1)
4
8
  */
5
9
 
6
10
  import { uploadToFalStorage, uploadLocalFileToFalStorage } from "./fal-storage.util";
@@ -17,10 +21,33 @@ function isLocalFileUri(value: unknown): value is string {
17
21
  );
18
22
  }
19
23
 
24
+ /**
25
+ * Classify a network error into a user-friendly message.
26
+ * Technical details are preserved in Firestore logs/session subcollection.
27
+ */
28
+ function classifyUploadError(errorMsg: string): string {
29
+ const lower = errorMsg.toLowerCase();
30
+
31
+ if (lower.includes('timed out') || lower.includes('timeout')) {
32
+ return 'Photo upload took too long. Please try again on a stronger connection (WiFi recommended).';
33
+ }
34
+ if (lower.includes('network request failed') || lower.includes('network') || lower.includes('fetch')) {
35
+ return 'Photo upload failed due to network issues. Please check your internet connection and try again.';
36
+ }
37
+ if (lower.includes('econnrefused') || lower.includes('enotfound')) {
38
+ return 'Could not reach the upload server. Please check your internet connection and try again.';
39
+ }
40
+
41
+ return errorMsg;
42
+ }
43
+
20
44
  /**
21
45
  * Preprocess input by uploading base64/local file images to FAL storage.
22
46
  * Also strips sync_mode to prevent base64 data URI responses.
23
47
  * Returns input with HTTPS URLs instead of base64/local URIs.
48
+ *
49
+ * Array fields are uploaded SEQUENTIALLY to avoid bandwidth contention
50
+ * on slow mobile connections (prevents simultaneous upload failures).
24
51
  */
25
52
  export async function preprocessInput(
26
53
  input: Record<string, unknown>,
@@ -38,7 +65,7 @@ export async function preprocessInput(
38
65
  generationLogCollector.warn(sessionId, TAG, `Stripped sync_mode from input`);
39
66
  }
40
67
 
41
- // Handle individual image URL keys
68
+ // Handle individual image URL keys (parallel — typically only 1 field)
42
69
  let individualUploadCount = 0;
43
70
  for (const key of IMAGE_URL_FIELDS) {
44
71
  const value = result[key];
@@ -78,76 +105,45 @@ export async function preprocessInput(
78
105
  generationLogCollector.log(sessionId, TAG, `${individualUploadCount} individual field upload(s) queued`);
79
106
  }
80
107
 
81
- // Handle image URL arrays
108
+ // Handle image URL arrays — SEQUENTIAL to avoid bandwidth contention
82
109
  for (const arrayField of ["image_urls", "input_image_urls", "reference_image_urls"] as const) {
83
110
  if (Array.isArray(result[arrayField]) && (result[arrayField] as unknown[]).length > 0) {
84
111
  const imageUrls = result[arrayField] as unknown[];
85
- generationLogCollector.log(sessionId, TAG, `Processing ${arrayField}: ${imageUrls.length} item(s)`);
112
+ generationLogCollector.log(sessionId, TAG, `Processing ${arrayField}: ${imageUrls.length} item(s) (sequential)`);
86
113
 
87
- const uploadTasks: Array<{ index: number; url: string | Promise<string> }> = [];
88
- const errors: string[] = [];
114
+ const processedUrls: string[] = [];
115
+ const arrayStartTime = Date.now();
89
116
 
90
117
  for (let i = 0; i < imageUrls.length; i++) {
91
118
  const imageUrl = imageUrls[i];
92
119
 
93
120
  if (!imageUrl) {
94
- errors.push(`${arrayField}[${i}] is null or undefined`);
95
- continue;
121
+ throw new Error(`${arrayField}[${i}] is null or undefined`);
96
122
  }
97
123
 
98
124
  if (isBase64DataUri(imageUrl)) {
99
- generationLogCollector.log(sessionId, TAG, `${arrayField}[${i}]: base64 (${Math.round(String(imageUrl).length / 1024)}KB) - uploading`);
100
- const uploadPromise = uploadToFalStorage(imageUrl, sessionId)
101
- .then((url) => url)
102
- .catch((error) => {
103
- const errorMessage = `Failed to upload ${arrayField}[${i}]: ${getErrorMessage(error)}`;
104
- generationLogCollector.error(sessionId, TAG, errorMessage);
105
- errors.push(errorMessage);
106
- throw new Error(errorMessage);
107
- });
108
- uploadTasks.push({ index: i, url: uploadPromise });
125
+ const sizeKB = Math.round(String(imageUrl).length / 1024);
126
+ generationLogCollector.log(sessionId, TAG, `${arrayField}[${i}/${imageUrls.length}]: base64 (${sizeKB}KB) - uploading...`);
127
+
128
+ try {
129
+ const url = await uploadToFalStorage(imageUrl, sessionId);
130
+ processedUrls.push(url);
131
+ generationLogCollector.log(sessionId, TAG, `${arrayField}[${i}/${imageUrls.length}]: upload OK`);
132
+ } catch (error) {
133
+ const elapsed = Date.now() - arrayStartTime;
134
+ const technicalMsg = getErrorMessage(error);
135
+ generationLogCollector.error(sessionId, TAG, `${arrayField}[${i}] upload FAILED after ${elapsed}ms: ${technicalMsg}`);
136
+ throw new Error(classifyUploadError(technicalMsg));
137
+ }
109
138
  } else if (typeof imageUrl === "string") {
110
- generationLogCollector.log(sessionId, TAG, `${arrayField}[${i}]: already URL - pass through`);
111
- uploadTasks.push({ index: i, url: imageUrl });
139
+ generationLogCollector.log(sessionId, TAG, `${arrayField}[${i}/${imageUrls.length}]: already URL - pass through`);
140
+ processedUrls.push(imageUrl);
112
141
  } else {
113
- errors.push(`${arrayField}[${i}] has invalid type: ${typeof imageUrl}`);
142
+ throw new Error(`${arrayField}[${i}] has invalid type: ${typeof imageUrl}`);
114
143
  }
115
144
  }
116
145
 
117
- if (errors.length > 0) {
118
- generationLogCollector.error(sessionId, TAG, `Validation errors in ${arrayField}`, { errors });
119
- throw new Error(`Image URL validation failed:\n${errors.join('\n')}`);
120
- }
121
-
122
- if (uploadTasks.length === 0) {
123
- throw new Error(`${arrayField} array must contain at least one valid image URL`);
124
- }
125
-
126
- const arrayStartTime = Date.now();
127
- const uploadResults = await Promise.allSettled(
128
- uploadTasks.map((task) => Promise.resolve(task.url))
129
- );
130
-
131
- const processedUrls: string[] = [];
132
- const uploadErrors: string[] = [];
133
-
134
- uploadResults.forEach((uploadResult, index) => {
135
- if (uploadResult.status === 'fulfilled') {
136
- processedUrls.push(uploadResult.value);
137
- } else {
138
- uploadErrors.push(
139
- `Upload ${index} failed: ${getErrorMessage(uploadResult.reason)}`
140
- );
141
- }
142
- });
143
-
144
146
  const arrayElapsed = Date.now() - arrayStartTime;
145
-
146
- if (uploadErrors.length > 0) {
147
- generationLogCollector.error(sessionId, TAG, `${arrayField} upload FAILED: ${processedUrls.length}/${uploadTasks.length} succeeded in ${arrayElapsed}ms`);
148
- throw new Error(`Image upload failures:\n${uploadErrors.join('\n')}`);
149
- }
150
-
151
147
  generationLogCollector.log(sessionId, TAG, `${arrayField}: all ${processedUrls.length} upload(s) succeeded in ${arrayElapsed}ms`);
152
148
  result[arrayField] = processedUrls;
153
149
  }
@@ -159,16 +155,16 @@ export async function preprocessInput(
159
155
  const individualUploadResults = await Promise.allSettled(uploadPromises);
160
156
 
161
157
  const failedUploads = individualUploadResults.filter(
162
- (result) => result.status === 'rejected'
158
+ (r) => r.status === 'rejected'
163
159
  );
164
160
 
165
161
  if (failedUploads.length > 0) {
166
162
  const successCount = individualUploadResults.length - failedUploads.length;
167
- const errorMessages = failedUploads.map((result) =>
168
- result.status === 'rejected' ? getErrorMessage(result.reason) : 'Unknown error'
163
+ const errorMessages = failedUploads.map((r) =>
164
+ r.status === 'rejected' ? getErrorMessage(r.reason) : 'Unknown error'
169
165
  );
170
166
  generationLogCollector.error(sessionId, TAG, `Individual uploads: ${successCount}/${individualUploadResults.length} succeeded`, { errors: errorMessages });
171
- throw new Error(`Some image uploads failed:\n${errorMessages.join('\n')}`);
167
+ throw new Error(classifyUploadError(errorMessages[0]));
172
168
  }
173
169
  }
174
170