@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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
200
|
-
*
|
|
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 (!
|
|
204
|
-
return generationLogCollector.getEntries(
|
|
206
|
+
getSessionLogs(sessionId?: string): LogEntry[] {
|
|
207
|
+
if (!sessionId) return [];
|
|
208
|
+
return generationLogCollector.getEntries(sessionId);
|
|
205
209
|
}
|
|
206
210
|
|
|
207
211
|
/**
|
|
208
|
-
* End
|
|
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 (!
|
|
212
|
-
|
|
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
|
|
88
|
-
const
|
|
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
|
-
|
|
95
|
-
continue;
|
|
121
|
+
throw new Error(`${arrayField}[${i}] is null or undefined`);
|
|
96
122
|
}
|
|
97
123
|
|
|
98
124
|
if (isBase64DataUri(imageUrl)) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
139
|
+
generationLogCollector.log(sessionId, TAG, `${arrayField}[${i}/${imageUrls.length}]: already URL - pass through`);
|
|
140
|
+
processedUrls.push(imageUrl);
|
|
112
141
|
} else {
|
|
113
|
-
|
|
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
|
-
(
|
|
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((
|
|
168
|
-
|
|
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(
|
|
167
|
+
throw new Error(classifyUploadError(errorMessages[0]));
|
|
172
168
|
}
|
|
173
169
|
}
|
|
174
170
|
|