@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.91",
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
- * Implements IAIProvider interface for unified AI generation
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(configData: AIProviderConfig): void {
48
- this.apiKey = configData.apiKey;
49
- this.videoFeatureModels = configData.videoFeatureModels ?? {};
50
- this.imageFeatureModels = configData.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: configData.apiKey,
61
+ credentials: config.apiKey,
54
62
  retry: {
55
- maxRetries: configData.maxRetries ?? DEFAULT_FAL_CONFIG.maxRetries,
56
- baseDelay: configData.baseDelay ?? DEFAULT_FAL_CONFIG.baseDelay,
57
- maxDelay: configData.maxDelay ?? DEFAULT_FAL_CONFIG.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 capabilities = this.getCapabilities();
100
+ const caps = this.getCapabilities();
112
101
  return (
113
- capabilities.imageFeatures.includes(feature as ImageFeatureType) ||
114
- capabilities.videoFeatures.includes(feature as VideoFeatureType)
102
+ caps.imageFeatures.includes(feature as ImageFeatureType) ||
103
+ caps.videoFeatures.includes(feature as VideoFeatureType)
115
104
  );
116
105
  }
117
106
 
118
- private validateInitialization(): void {
107
+ private validateInit(): void {
119
108
  if (!this.apiKey || !this.initialized) {
120
- throw new Error("FAL provider not initialized. Call initialize() first.");
109
+ throw new Error("FAL provider not initialized");
121
110
  }
122
111
  }
123
112
 
124
- async submitJob(
125
- model: string,
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.validateInitialization();
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
- model: string,
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.validateInitialization();
144
+ this.validateInit();
158
145
 
159
- // If there's already a running request, skip starting a new one
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("[FalProvider] Request already in progress, skipping duplicate");
148
+ console.log(`[FalProvider] Returning existing promise for ${activeRequest.model}`);
164
149
  }
165
- // Wait for the existing request by throwing a specific error
166
- throw new Error("Generation already in progress");
150
+ return activeRequest.promise as Promise<T>;
167
151
  }
168
152
 
169
- this.currentAbortController = new AbortController();
170
-
153
+ const abortController = new AbortController();
171
154
  const operationId = this.costTracker?.startOperation(model, "subscribe");
172
155
 
173
- try {
174
- const { result, requestId } = await handleFalSubscription<T>(
175
- model,
176
- input,
177
- options,
178
- this.currentAbortController.signal,
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
- return result;
191
- } finally {
192
- // Clear the abort controller when done (success or error)
193
- this.currentAbortController = null;
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.validateInitialization();
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 (this.currentAbortController) {
191
+ if (activeRequest) {
223
192
  if (typeof __DEV__ !== "undefined" && __DEV__) {
224
193
  console.log("[FalProvider] Cancelling current request");
225
194
  }
226
- this.currentAbortController.abort();
227
- this.currentAbortController = null;
195
+ activeRequest.abortController.abort();
196
+ activeRequest = null;
228
197
  }
229
198
  }
230
199
 
231
200
  hasRunningRequest(): boolean {
232
- return this.currentAbortController !== null;
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
  }