@umituz/react-native-ai-fal-provider 1.0.91 → 1.0.93
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": "1.0.
|
|
3
|
+
"version": "1.0.93",
|
|
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,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FAL Provider
|
|
3
|
-
*
|
|
2
|
+
* FAL Provider - Implements IAIProvider interface
|
|
3
|
+
* Uses Promise Deduplication Pattern to prevent duplicate requests
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { fal } from "@fal-ai/client";
|
|
@@ -25,76 +25,65 @@ import {
|
|
|
25
25
|
buildImageFeatureInput as buildImageFeatureInputImpl,
|
|
26
26
|
buildVideoFeatureInput as buildVideoFeatureInputImpl,
|
|
27
27
|
} from "../builders";
|
|
28
|
-
import {
|
|
29
|
-
handleFalSubscription,
|
|
30
|
-
handleFalRun,
|
|
31
|
-
} from "./fal-provider-subscription";
|
|
28
|
+
import { handleFalSubscription, handleFalRun } from "./fal-provider-subscription";
|
|
32
29
|
import { CostTracker } from "../utils/cost-tracker";
|
|
33
30
|
|
|
34
31
|
declare const __DEV__: boolean | undefined;
|
|
35
32
|
|
|
33
|
+
interface ActiveRequest<T = unknown> {
|
|
34
|
+
promise: Promise<T>;
|
|
35
|
+
abortController: AbortController;
|
|
36
|
+
model: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Module-level state for Promise Deduplication
|
|
41
|
+
* Persists across hot reloads (module cache is preserved)
|
|
42
|
+
*/
|
|
43
|
+
let activeRequest: ActiveRequest | null = null;
|
|
44
|
+
|
|
36
45
|
export class FalProvider implements IAIProvider {
|
|
37
46
|
readonly providerId = "fal";
|
|
38
47
|
readonly providerName = "FAL AI";
|
|
39
48
|
|
|
40
49
|
private apiKey: string | null = null;
|
|
41
50
|
private initialized = false;
|
|
42
|
-
private currentAbortController: AbortController | null = null;
|
|
43
51
|
private costTracker: CostTracker | null = null;
|
|
44
52
|
private videoFeatureModels: Record<string, string> = {};
|
|
45
53
|
private imageFeatureModels: Record<string, string> = {};
|
|
46
54
|
|
|
47
|
-
initialize(
|
|
48
|
-
this.apiKey =
|
|
49
|
-
this.videoFeatureModels =
|
|
50
|
-
this.imageFeatureModels =
|
|
55
|
+
initialize(config: AIProviderConfig): void {
|
|
56
|
+
this.apiKey = config.apiKey;
|
|
57
|
+
this.videoFeatureModels = config.videoFeatureModels ?? {};
|
|
58
|
+
this.imageFeatureModels = config.imageFeatureModels ?? {};
|
|
51
59
|
|
|
52
60
|
fal.config({
|
|
53
|
-
credentials:
|
|
61
|
+
credentials: config.apiKey,
|
|
54
62
|
retry: {
|
|
55
|
-
maxRetries:
|
|
56
|
-
baseDelay:
|
|
57
|
-
maxDelay:
|
|
63
|
+
maxRetries: config.maxRetries ?? DEFAULT_FAL_CONFIG.maxRetries,
|
|
64
|
+
baseDelay: config.baseDelay ?? DEFAULT_FAL_CONFIG.baseDelay,
|
|
65
|
+
maxDelay: config.maxDelay ?? DEFAULT_FAL_CONFIG.maxDelay,
|
|
58
66
|
},
|
|
59
67
|
});
|
|
60
68
|
|
|
61
69
|
this.initialized = true;
|
|
62
|
-
|
|
63
70
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
64
71
|
console.log("[FalProvider] Initialized");
|
|
65
72
|
}
|
|
66
73
|
}
|
|
67
74
|
|
|
68
|
-
/**
|
|
69
|
-
* Enable cost tracking
|
|
70
|
-
*/
|
|
71
75
|
enableCostTracking(config?: CostTrackerConfig): void {
|
|
72
76
|
this.costTracker = new CostTracker(config);
|
|
73
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
74
|
-
console.log("[FalProvider] Cost tracking enabled");
|
|
75
|
-
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
/**
|
|
79
|
-
* Disable cost tracking
|
|
80
|
-
*/
|
|
81
79
|
disableCostTracking(): void {
|
|
82
80
|
this.costTracker = null;
|
|
83
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
84
|
-
console.log("[FalProvider] Cost tracking disabled");
|
|
85
|
-
}
|
|
86
81
|
}
|
|
87
82
|
|
|
88
|
-
/**
|
|
89
|
-
* Check if cost tracking is enabled
|
|
90
|
-
*/
|
|
91
83
|
isCostTrackingEnabled(): boolean {
|
|
92
84
|
return this.costTracker !== null;
|
|
93
85
|
}
|
|
94
86
|
|
|
95
|
-
/**
|
|
96
|
-
* Get cost tracker instance
|
|
97
|
-
*/
|
|
98
87
|
getCostTracker(): CostTracker | null {
|
|
99
88
|
return this.costTracker;
|
|
100
89
|
}
|
|
@@ -108,24 +97,21 @@ export class FalProvider implements IAIProvider {
|
|
|
108
97
|
}
|
|
109
98
|
|
|
110
99
|
isFeatureSupported(feature: ImageFeatureType | VideoFeatureType): boolean {
|
|
111
|
-
const
|
|
100
|
+
const caps = this.getCapabilities();
|
|
112
101
|
return (
|
|
113
|
-
|
|
114
|
-
|
|
102
|
+
caps.imageFeatures.includes(feature as ImageFeatureType) ||
|
|
103
|
+
caps.videoFeatures.includes(feature as VideoFeatureType)
|
|
115
104
|
);
|
|
116
105
|
}
|
|
117
106
|
|
|
118
|
-
private
|
|
107
|
+
private validateInit(): void {
|
|
119
108
|
if (!this.apiKey || !this.initialized) {
|
|
120
|
-
throw new Error("FAL provider not initialized
|
|
109
|
+
throw new Error("FAL provider not initialized");
|
|
121
110
|
}
|
|
122
111
|
}
|
|
123
112
|
|
|
124
|
-
async submitJob(
|
|
125
|
-
|
|
126
|
-
input: Record<string, unknown>,
|
|
127
|
-
): Promise<JobSubmission> {
|
|
128
|
-
this.validateInitialization();
|
|
113
|
+
async submitJob(model: string, input: Record<string, unknown>): Promise<JobSubmission> {
|
|
114
|
+
this.validateInit();
|
|
129
115
|
const result = await fal.queue.submit(model, { input });
|
|
130
116
|
return {
|
|
131
117
|
requestId: result.request_id,
|
|
@@ -135,63 +121,50 @@ export class FalProvider implements IAIProvider {
|
|
|
135
121
|
}
|
|
136
122
|
|
|
137
123
|
async getJobStatus(model: string, requestId: string): Promise<JobStatus> {
|
|
138
|
-
this.
|
|
124
|
+
this.validateInit();
|
|
139
125
|
const status = await fal.queue.status(model, { requestId, logs: true });
|
|
140
126
|
return mapFalStatusToJobStatus(status as unknown as FalQueueStatus);
|
|
141
127
|
}
|
|
142
128
|
|
|
143
|
-
async getJobResult<T = unknown>(
|
|
144
|
-
|
|
145
|
-
requestId: string,
|
|
146
|
-
): Promise<T> {
|
|
147
|
-
this.validateInitialization();
|
|
129
|
+
async getJobResult<T = unknown>(model: string, requestId: string): Promise<T> {
|
|
130
|
+
this.validateInit();
|
|
148
131
|
const result = await fal.queue.result(model, { requestId });
|
|
149
132
|
return result.data as T;
|
|
150
133
|
}
|
|
151
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Promise Deduplication: If request in progress, return existing promise
|
|
137
|
+
* Prevents duplicate API calls from React re-renders
|
|
138
|
+
*/
|
|
152
139
|
async subscribe<T = unknown>(
|
|
153
140
|
model: string,
|
|
154
141
|
input: Record<string, unknown>,
|
|
155
142
|
options?: SubscribeOptions<T>,
|
|
156
143
|
): Promise<T> {
|
|
157
|
-
this.
|
|
144
|
+
this.validateInit();
|
|
158
145
|
|
|
159
|
-
|
|
160
|
-
// This prevents React re-renders from cancelling ongoing generations
|
|
161
|
-
if (this.currentAbortController) {
|
|
146
|
+
if (activeRequest) {
|
|
162
147
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
163
|
-
console.log(
|
|
148
|
+
console.log(`[FalProvider] Returning existing promise for ${activeRequest.model}`);
|
|
164
149
|
}
|
|
165
|
-
|
|
166
|
-
throw new Error("Generation already in progress");
|
|
150
|
+
return activeRequest.promise as Promise<T>;
|
|
167
151
|
}
|
|
168
152
|
|
|
169
|
-
|
|
170
|
-
|
|
153
|
+
const abortController = new AbortController();
|
|
171
154
|
const operationId = this.costTracker?.startOperation(model, "subscribe");
|
|
172
155
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
if (operationId && this.costTracker) {
|
|
182
|
-
this.costTracker.completeOperation(
|
|
183
|
-
operationId,
|
|
184
|
-
model,
|
|
185
|
-
"subscribe",
|
|
186
|
-
requestId ?? undefined,
|
|
187
|
-
);
|
|
188
|
-
}
|
|
156
|
+
const promise = handleFalSubscription<T>(model, input, options, abortController.signal)
|
|
157
|
+
.then(({ result, requestId }) => {
|
|
158
|
+
if (operationId && this.costTracker) {
|
|
159
|
+
this.costTracker.completeOperation(operationId, model, "subscribe", requestId ?? undefined);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
});
|
|
189
163
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
164
|
+
activeRequest = { promise, abortController, model };
|
|
165
|
+
promise.finally(() => { activeRequest = null; });
|
|
166
|
+
|
|
167
|
+
return promise;
|
|
195
168
|
}
|
|
196
169
|
|
|
197
170
|
async run<T = unknown>(
|
|
@@ -199,16 +172,12 @@ export class FalProvider implements IAIProvider {
|
|
|
199
172
|
input: Record<string, unknown>,
|
|
200
173
|
options?: RunOptions,
|
|
201
174
|
): Promise<T> {
|
|
202
|
-
this.
|
|
203
|
-
|
|
175
|
+
this.validateInit();
|
|
204
176
|
const operationId = this.costTracker?.startOperation(model, "run");
|
|
205
|
-
|
|
206
177
|
const result = await handleFalRun<T>(model, input, options);
|
|
207
|
-
|
|
208
178
|
if (operationId && this.costTracker) {
|
|
209
179
|
this.costTracker.completeOperation(operationId, model, "run");
|
|
210
180
|
}
|
|
211
|
-
|
|
212
181
|
return result;
|
|
213
182
|
}
|
|
214
183
|
|
|
@@ -219,46 +188,36 @@ export class FalProvider implements IAIProvider {
|
|
|
219
188
|
}
|
|
220
189
|
|
|
221
190
|
cancelCurrentRequest(): void {
|
|
222
|
-
if (
|
|
191
|
+
if (activeRequest) {
|
|
223
192
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
224
193
|
console.log("[FalProvider] Cancelling current request");
|
|
225
194
|
}
|
|
226
|
-
|
|
227
|
-
|
|
195
|
+
activeRequest.abortController.abort();
|
|
196
|
+
activeRequest = null;
|
|
228
197
|
}
|
|
229
198
|
}
|
|
230
199
|
|
|
231
200
|
hasRunningRequest(): boolean {
|
|
232
|
-
return
|
|
201
|
+
return activeRequest !== null;
|
|
233
202
|
}
|
|
234
203
|
|
|
235
204
|
getImageFeatureModel(feature: ImageFeatureType): string {
|
|
236
205
|
const model = this.imageFeatureModels[feature];
|
|
237
|
-
if (!model) {
|
|
238
|
-
throw new Error(`No model configured for image feature: ${feature}`);
|
|
239
|
-
}
|
|
206
|
+
if (!model) throw new Error(`No model for image feature: ${feature}`);
|
|
240
207
|
return model;
|
|
241
208
|
}
|
|
242
209
|
|
|
243
|
-
buildImageFeatureInput(
|
|
244
|
-
feature: ImageFeatureType,
|
|
245
|
-
data: ImageFeatureInputData,
|
|
246
|
-
): Record<string, unknown> {
|
|
210
|
+
buildImageFeatureInput(feature: ImageFeatureType, data: ImageFeatureInputData): Record<string, unknown> {
|
|
247
211
|
return buildImageFeatureInputImpl(feature, data);
|
|
248
212
|
}
|
|
249
213
|
|
|
250
214
|
getVideoFeatureModel(feature: VideoFeatureType): string {
|
|
251
215
|
const model = this.videoFeatureModels[feature];
|
|
252
|
-
if (!model) {
|
|
253
|
-
throw new Error(`No model configured for video feature: ${feature}`);
|
|
254
|
-
}
|
|
216
|
+
if (!model) throw new Error(`No model for video feature: ${feature}`);
|
|
255
217
|
return model;
|
|
256
218
|
}
|
|
257
219
|
|
|
258
|
-
buildVideoFeatureInput(
|
|
259
|
-
feature: VideoFeatureType,
|
|
260
|
-
data: VideoFeatureInputData,
|
|
261
|
-
): Record<string, unknown> {
|
|
220
|
+
buildVideoFeatureInput(feature: VideoFeatureType, data: VideoFeatureInputData): Record<string, unknown> {
|
|
262
221
|
return buildVideoFeatureInputImpl(feature, data);
|
|
263
222
|
}
|
|
264
223
|
}
|