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

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.33",
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",
@@ -28,8 +28,6 @@ export class FalProvider implements IAIProvider {
28
28
  private apiKey: string | null = null;
29
29
  private initialized = false;
30
30
  private lastRequestKey: string | null = null;
31
- /** Tracks the active sessionId so callers can retrieve logs */
32
- private activeSessionId: string | null = null;
33
31
 
34
32
  initialize(config: AIProviderConfig): void {
35
33
  this.apiKey = config.apiKey;
@@ -80,7 +78,6 @@ export class FalProvider implements IAIProvider {
80
78
  this.validateInit();
81
79
  validateInput(model, input);
82
80
  const sessionId = generationLogCollector.startSession();
83
- this.activeSessionId = sessionId;
84
81
  generationLogCollector.log(sessionId, 'fal-provider', `submitJob() for model: ${model}`);
85
82
  const processedInput = await preprocessInput(input, sessionId);
86
83
  return queueOps.submitJob(model, processedInput);
@@ -108,7 +105,6 @@ export class FalProvider implements IAIProvider {
108
105
 
109
106
  // Start a fresh log session for this generation
110
107
  const sessionId = generationLogCollector.startSession();
111
- this.activeSessionId = sessionId;
112
108
  generationLogCollector.log(sessionId, TAG, `subscribe() called for model: ${model}`);
113
109
 
114
110
  const preprocessStart = Date.now();
@@ -140,7 +136,12 @@ export class FalProvider implements IAIProvider {
140
136
  .then((res) => {
141
137
  const totalElapsed = Date.now() - totalStart;
142
138
  generationLogCollector.log(sessionId, TAG, `Generation SUCCESS in ${totalElapsed}ms (preprocess: ${preprocessElapsed}ms)`);
143
- resolvePromise(res.result);
139
+ // Attach providerSessionId to result for concurrent-safe log retrieval
140
+ const result = res.result;
141
+ if (result && typeof result === 'object') {
142
+ Object.defineProperty(result, '__providerSessionId', { value: sessionId, enumerable: false });
143
+ }
144
+ resolvePromise(result);
144
145
  })
145
146
  .catch((error) => {
146
147
  const totalElapsed = Date.now() - totalStart;
@@ -163,7 +164,6 @@ export class FalProvider implements IAIProvider {
163
164
  validateInput(model, input);
164
165
 
165
166
  const sessionId = generationLogCollector.startSession();
166
- this.activeSessionId = sessionId;
167
167
  generationLogCollector.log(sessionId, 'fal-provider', `run() for model: ${model}`);
168
168
 
169
169
  const processedInput = await preprocessInput(input, sessionId);
@@ -173,13 +173,17 @@ export class FalProvider implements IAIProvider {
173
173
  throw new Error("Request cancelled by user");
174
174
  }
175
175
 
176
- return handleFalRun<T>(model, processedInput, sessionId, options);
176
+ const result = await handleFalRun<T>(model, processedInput, sessionId, options);
177
+ // Attach providerSessionId to result for concurrent-safe log retrieval
178
+ if (result && typeof result === 'object') {
179
+ Object.defineProperty(result, '__providerSessionId', { value: sessionId, enumerable: false });
180
+ }
181
+ return result;
177
182
  }
178
183
 
179
184
  reset(): void {
180
185
  cancelAllRequests();
181
186
  this.lastRequestKey = null;
182
- this.activeSessionId = null;
183
187
  this.apiKey = null;
184
188
  this.initialized = false;
185
189
  }
@@ -196,22 +200,21 @@ export class FalProvider implements IAIProvider {
196
200
  }
197
201
 
198
202
  /**
199
- * Get all log entries collected during the current/last generation session.
200
- * Returns empty array if no active session.
203
+ * Get log entries for a specific provider session.
204
+ * Extract sessionId from result.__providerSessionId for concurrent safety.
201
205
  */
202
- getSessionLogs(): LogEntry[] {
203
- if (!this.activeSessionId) return [];
204
- return generationLogCollector.getEntries(this.activeSessionId);
206
+ getSessionLogs(sessionId?: string): LogEntry[] {
207
+ if (!sessionId) return [];
208
+ return generationLogCollector.getEntries(sessionId);
205
209
  }
206
210
 
207
211
  /**
208
- * End the current log session and return all entries. Clears the buffer.
212
+ * End a provider log session and return all entries. Clears the buffer.
213
+ * Extract sessionId from result.__providerSessionId for concurrent safety.
209
214
  */
210
- endLogSession(): LogEntry[] {
211
- if (!this.activeSessionId) return [];
212
- const entries = generationLogCollector.endSession(this.activeSessionId);
213
- this.activeSessionId = null;
214
- return entries;
215
+ endLogSession(sessionId?: string): LogEntry[] {
216
+ if (!sessionId) return [];
217
+ return generationLogCollector.endSession(sessionId);
215
218
  }
216
219
  }
217
220
 
@@ -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