@umituz/react-native-ai-fal-provider 2.0.37 → 2.0.39

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.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/infrastructure/services/fal-models.service.ts +4 -1
  3. package/src/infrastructure/services/fal-provider-subscription.ts +17 -7
  4. package/src/infrastructure/services/fal-provider.ts +11 -1
  5. package/src/infrastructure/services/fal-queue-operations.ts +10 -0
  6. package/src/infrastructure/services/request-store.ts +53 -12
  7. package/src/infrastructure/utils/collection-filters.util.ts +5 -167
  8. package/src/infrastructure/utils/collections/array-filters.util.ts +52 -0
  9. package/src/infrastructure/utils/collections/array-reducers.util.ts +67 -0
  10. package/src/infrastructure/utils/collections/array-sorters.util.ts +60 -0
  11. package/src/infrastructure/utils/collections/index.ts +8 -0
  12. package/src/infrastructure/utils/cost-tracker.ts +6 -1
  13. package/src/infrastructure/utils/data-parsers.util.ts +5 -187
  14. package/src/infrastructure/utils/error-mapper.ts +10 -0
  15. package/src/infrastructure/utils/fal-error-handler.util.ts +21 -18
  16. package/src/infrastructure/utils/fal-generation-state-manager.util.ts +9 -2
  17. package/src/infrastructure/utils/fal-storage.util.ts +7 -2
  18. package/src/infrastructure/utils/general-helpers.util.ts +5 -146
  19. package/src/infrastructure/utils/helpers/function-helpers.util.ts +25 -0
  20. package/src/infrastructure/utils/helpers/index.ts +8 -0
  21. package/src/infrastructure/utils/helpers/object-helpers.util.ts +44 -0
  22. package/src/infrastructure/utils/helpers/timing-helpers.util.ts +89 -0
  23. package/src/infrastructure/utils/input-preprocessor.util.ts +12 -6
  24. package/src/infrastructure/utils/input-validator.util.ts +34 -3
  25. package/src/infrastructure/utils/parsers/index.ts +10 -0
  26. package/src/infrastructure/utils/parsers/json-parsers.util.ts +55 -0
  27. package/src/infrastructure/utils/parsers/number-helpers.util.ts +19 -0
  28. package/src/infrastructure/utils/parsers/object-transformers.util.ts +67 -0
  29. package/src/infrastructure/utils/parsers/object-validators.util.ts +38 -0
  30. package/src/infrastructure/utils/parsers/value-parsers.util.ts +45 -0
  31. package/src/infrastructure/utils/type-guards/constants.ts +10 -0
  32. package/src/infrastructure/utils/type-guards/index.ts +8 -0
  33. package/src/infrastructure/utils/type-guards/model-type-guards.util.ts +56 -0
  34. package/src/infrastructure/utils/type-guards/validation-guards.util.ts +80 -0
  35. package/src/infrastructure/utils/type-guards.util.ts +5 -115
  36. package/src/presentation/hooks/use-fal-generation.ts +15 -4
  37. package/src/presentation/hooks/use-models.ts +26 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-fal-provider",
3
- "version": "2.0.37",
3
+ "version": "2.0.39",
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",
@@ -53,10 +53,13 @@ export function getModelPricing(modelId: string): { freeUserCost: number; premiu
53
53
  /**
54
54
  * Get credit cost for a model
55
55
  * Returns the model's free user cost if available, otherwise returns the default cost for the type
56
+ * NOTE: Use ?? instead of || to handle 0 values correctly (free models)
56
57
  */
57
58
  export function getModelCreditCost(modelId: string, modelType: ModelType): number {
58
59
  const pricing = getModelPricing(modelId);
59
- if (pricing?.freeUserCost) {
60
+ // CRITICAL: Use !== undefined instead of truthy check
61
+ // because freeUserCost can be 0 for free models!
62
+ if (pricing && pricing.freeUserCost !== undefined) {
60
63
  return pricing.freeUserCost;
61
64
  }
62
65
  return DEFAULT_CREDIT_COSTS[modelType];
@@ -23,8 +23,11 @@ export async function handleFalSubscription<T = unknown>(
23
23
  ): Promise<{ result: T; requestId: string | null }> {
24
24
  const timeoutMs = options?.timeoutMs ?? DEFAULT_FAL_CONFIG.defaultTimeoutMs;
25
25
 
26
- if (timeoutMs <= 0 || timeoutMs > 3600000) {
27
- throw new Error(`Invalid timeout: ${timeoutMs}ms. Must be between 1 and 3600000ms (1 hour)`);
26
+ // Validate timeout is a positive integer within reasonable bounds
27
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 3600000) {
28
+ throw new Error(
29
+ `Invalid timeout: ${timeoutMs}ms. Must be a positive integer between 1 and 3600000ms (1 hour)`
30
+ );
28
31
  }
29
32
 
30
33
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
@@ -32,13 +35,14 @@ export async function handleFalSubscription<T = unknown>(
32
35
  let abortHandler: (() => void) | null = null;
33
36
  let listenerAdded = false;
34
37
 
35
- if (signal?.aborted) {
36
- throw new Error("Request cancelled by user");
37
- }
38
-
39
38
  let lastStatus = "";
40
39
 
41
40
  try {
41
+ // Check if signal is already aborted BEFORE starting any async work
42
+ if (signal?.aborted) {
43
+ throw new Error("Request cancelled by user");
44
+ }
45
+
42
46
  const promises: Promise<unknown>[] = [
43
47
  fal.subscribe(model, {
44
48
  input,
@@ -77,13 +81,19 @@ export async function handleFalSubscription<T = unknown>(
77
81
  }),
78
82
  ];
79
83
 
80
- if (signal && !signal.aborted) {
84
+ // Set up abort listener BEFORE checking aborted state again to avoid race
85
+ if (signal) {
81
86
  const abortPromise = new Promise<never>((_, reject) => {
82
87
  abortHandler = () => {
83
88
  reject(new Error("Request cancelled by user"));
84
89
  };
85
90
  signal.addEventListener("abort", abortHandler);
86
91
  listenerAdded = true;
92
+
93
+ // Check again after adding listener to catch signals that arrived during setup
94
+ if (signal.aborted) {
95
+ abortHandler();
96
+ }
87
97
  });
88
98
  promises.push(abortPromise);
89
99
  }
@@ -153,7 +153,17 @@ export class FalProvider implements IAIProvider {
153
153
  rejectPromise!(error);
154
154
  throw error;
155
155
  })
156
- .finally(() => removeRequest(key));
156
+ .finally(() => {
157
+ try {
158
+ removeRequest(key);
159
+ } catch (cleanupError) {
160
+ // Log but don't throw - cleanup errors shouldn't affect the operation result
161
+ console.error(
162
+ `[fal-provider] Error removing request from store: ${key}`,
163
+ cleanupError instanceof Error ? cleanupError.message : String(cleanupError)
164
+ );
165
+ }
166
+ });
157
167
 
158
168
  return promise;
159
169
  }
@@ -27,6 +27,16 @@ function isValidFalQueueStatus(value: unknown): value is FalQueueStatus {
27
27
 
28
28
  export async function submitJob(model: string, input: Record<string, unknown>): Promise<JobSubmission> {
29
29
  const result = await fal.queue.submit(model, { input });
30
+
31
+ // Validate required fields from FAL API response
32
+ if (!result?.request_id) {
33
+ throw new Error(`FAL API response missing request_id for model ${model}`);
34
+ }
35
+
36
+ if (!result?.status_url) {
37
+ throw new Error(`FAL API response missing status_url for model ${model}`);
38
+ }
39
+
30
40
  return {
31
41
  requestId: result.request_id,
32
42
  statusUrl: result.status_url,
@@ -10,17 +10,37 @@ export interface ActiveRequest<T = unknown> {
10
10
  }
11
11
 
12
12
  const STORE_KEY = "__FAL_PROVIDER_REQUESTS__";
13
+ const LOCK_KEY = "__FAL_PROVIDER_REQUESTS_LOCK__";
13
14
  type RequestStore = Map<string, ActiveRequest>;
14
15
 
15
16
  let cleanupTimer: ReturnType<typeof setInterval> | null = null;
16
17
  const CLEANUP_INTERVAL = 60000; // 1 minute
17
18
  const MAX_REQUEST_AGE = 300000; // 5 minutes
18
19
 
20
+ /**
21
+ * Simple lock mechanism to prevent concurrent access issues
22
+ * NOTE: This is not a true mutex but provides basic protection for React Native
23
+ */
24
+ function acquireLock(): boolean {
25
+ const globalObj = globalThis as Record<string, unknown>;
26
+ if (globalObj[LOCK_KEY]) {
27
+ return false; // Lock already held
28
+ }
29
+ globalObj[LOCK_KEY] = true;
30
+ return true;
31
+ }
32
+
33
+ function releaseLock(): void {
34
+ const globalObj = globalThis as Record<string, unknown>;
35
+ globalObj[LOCK_KEY] = false;
36
+ }
37
+
19
38
  export function getRequestStore(): RequestStore {
20
- if (!(globalThis as Record<string, unknown>)[STORE_KEY]) {
21
- (globalThis as Record<string, unknown>)[STORE_KEY] = new Map();
39
+ const globalObj = globalThis as Record<string, unknown>;
40
+ if (!globalObj[STORE_KEY]) {
41
+ globalObj[STORE_KEY] = new Map();
22
42
  }
23
- return (globalThis as Record<string, unknown>)[STORE_KEY] as RequestStore;
43
+ return globalObj[STORE_KEY] as RequestStore;
24
44
  }
25
45
 
26
46
  /**
@@ -45,14 +65,27 @@ export function getExistingRequest<T>(key: string): ActiveRequest<T> | undefined
45
65
  }
46
66
 
47
67
  export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
48
- const requestWithTimestamp = {
49
- ...request,
50
- createdAt: request.createdAt ?? Date.now(),
51
- };
52
- getRequestStore().set(key, requestWithTimestamp);
53
-
54
- // Start automatic cleanup if not already running
55
- startAutomaticCleanup();
68
+ // Acquire lock for thread-safe operation
69
+ const maxRetries = 3;
70
+ let retries = 0;
71
+
72
+ while (!acquireLock() && retries < maxRetries) {
73
+ retries++;
74
+ // Brief spin wait (not ideal, but works for React Native single-threaded model)
75
+ }
76
+
77
+ try {
78
+ const requestWithTimestamp = {
79
+ ...request,
80
+ createdAt: request.createdAt ?? Date.now(),
81
+ };
82
+ getRequestStore().set(key, requestWithTimestamp);
83
+
84
+ // Start automatic cleanup if not already running
85
+ startAutomaticCleanup();
86
+ } finally {
87
+ releaseLock();
88
+ }
56
89
  }
57
90
 
58
91
  export function removeRequest(key: string): void {
@@ -138,8 +171,16 @@ function startAutomaticCleanup(): void {
138
171
 
139
172
  cleanupTimer = setInterval(() => {
140
173
  const cleanedCount = cleanupRequestStore(MAX_REQUEST_AGE);
174
+ const store = getRequestStore();
175
+
176
+ // Stop timer if no more requests in store (prevents indefinite timer)
177
+ if (store.size === 0 && cleanupTimer) {
178
+ clearInterval(cleanupTimer);
179
+ cleanupTimer = null;
180
+ }
181
+
141
182
  if (cleanedCount > 0) {
142
- // Cleanup was performed
183
+ console.log(`[request-store] Cleaned up ${cleanedCount} stale request(s)`);
143
184
  }
144
185
  }, CLEANUP_INTERVAL);
145
186
  }
@@ -1,171 +1,9 @@
1
1
  /**
2
2
  * Collection Filter Utilities
3
- * Common filter operations for arrays of objects
3
+ * @deprecated This file is now split into smaller modules for better maintainability.
4
+ * Import from './collections' submodules instead.
5
+ *
6
+ * This file re-exports all functions for backward compatibility.
4
7
  */
5
8
 
6
- /**
7
- * Filter array by property value
8
- */
9
- export function filterByProperty<T>(
10
- items: readonly T[],
11
- property: keyof T,
12
- value: unknown
13
- ): T[] {
14
- return items.filter((item) => item[property] === value);
15
- }
16
-
17
- /**
18
- * Filter array by predicate function
19
- */
20
- export function filterByPredicate<T>(
21
- items: readonly T[],
22
- predicate: (item: T) => boolean
23
- ): T[] {
24
- return items.filter(predicate);
25
- }
26
-
27
- /**
28
- * Filter array by time range (timestamp property)
29
- */
30
- export function filterByTimeRange<T>(
31
- items: readonly T[],
32
- timestampProperty: keyof T,
33
- startTime: number,
34
- endTime: number
35
- ): T[] {
36
- return items.filter((item) => {
37
- const timestamp = item[timestampProperty] as unknown as number;
38
- return timestamp >= startTime && timestamp <= endTime;
39
- });
40
- }
41
-
42
- /**
43
- * Filter array by multiple property values (OR logic)
44
- */
45
- export function filterByAnyProperty<T>(
46
- items: readonly T[],
47
- property: keyof T,
48
- values: readonly unknown[]
49
- ): T[] {
50
- const valueSet = new Set(values);
51
- return items.filter((item) => valueSet.has(item[property]));
52
- }
53
-
54
- /**
55
- * Sort array by date property (descending - newest first)
56
- */
57
- export function sortByDateDescending<T>(
58
- items: readonly T[],
59
- dateProperty: keyof T
60
- ): T[] {
61
- return [...items].sort((a, b) => {
62
- const timeA = new Date(a[dateProperty] as unknown as string).getTime();
63
- const timeB = new Date(b[dateProperty] as unknown as string).getTime();
64
- return timeB - timeA;
65
- });
66
- }
67
-
68
- /**
69
- * Sort array by date property (ascending - oldest first)
70
- */
71
- export function sortByDateAscending<T>(
72
- items: readonly T[],
73
- dateProperty: keyof T
74
- ): T[] {
75
- return [...items].sort((a, b) => {
76
- const timeA = new Date(a[dateProperty] as unknown as string).getTime();
77
- const timeB = new Date(b[dateProperty] as unknown as string).getTime();
78
- return timeA - timeB;
79
- });
80
- }
81
-
82
- /**
83
- * Sort array by number property (descending)
84
- */
85
- export function sortByNumberDescending<T>(
86
- items: readonly T[],
87
- numberProperty: keyof T
88
- ): T[] {
89
- return [...items].sort((a, b) => {
90
- const numA = a[numberProperty] as unknown as number;
91
- const numB = b[numberProperty] as unknown as number;
92
- return numB - numA;
93
- });
94
- }
95
-
96
- /**
97
- * Sort array by number property (ascending)
98
- */
99
- export function sortByNumberAscending<T>(
100
- items: readonly T[],
101
- numberProperty: keyof T
102
- ): T[] {
103
- return [...items].sort((a, b) => {
104
- const numA = a[numberProperty] as unknown as number;
105
- const numB = b[numberProperty] as unknown as number;
106
- return numA - numB;
107
- });
108
- }
109
-
110
- /**
111
- * Reduce array to sum of number property
112
- */
113
- export function sumByProperty<T>(
114
- items: readonly T[],
115
- numberProperty: keyof T
116
- ): number {
117
- return items.reduce((sum, item) => {
118
- const value = item[numberProperty] as unknown as number;
119
- return sum + (typeof value === "number" ? value : 0);
120
- }, 0);
121
- }
122
-
123
- /**
124
- * Group array by property value
125
- */
126
- export function groupByProperty<T>(
127
- items: readonly T[],
128
- property: keyof T
129
- ): Map<unknown, T[]> {
130
- const groups = new Map<unknown, T[]>();
131
- for (const item of items) {
132
- const key = item[property];
133
- const existing = groups.get(key);
134
- if (existing) {
135
- existing.push(item);
136
- } else {
137
- groups.set(key, [item]);
138
- }
139
- }
140
- return groups;
141
- }
142
-
143
- /**
144
- * Chunk array into smaller arrays of specified size
145
- */
146
- export function chunkArray<T>(items: readonly T[], chunkSize: number): T[][] {
147
- const result: T[][] = [];
148
- for (let i = 0; i < items.length; i += chunkSize) {
149
- result.push([...items.slice(i, i + chunkSize)]);
150
- }
151
- return result;
152
- }
153
-
154
- /**
155
- * Get distinct values of a property from array
156
- */
157
- export function distinctByProperty<T>(
158
- items: readonly T[],
159
- property: keyof T
160
- ): unknown[] {
161
- const seen = new Set<unknown>();
162
- const result: unknown[] = [];
163
- for (const item of items) {
164
- const value = item[property];
165
- if (!seen.has(value)) {
166
- seen.add(value);
167
- result.push(value);
168
- }
169
- }
170
- return result;
171
- }
9
+ export * from './collections';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Array Filter Utilities
3
+ * Filter operations for arrays of objects
4
+ */
5
+
6
+ /**
7
+ * Filter array by property value
8
+ */
9
+ export function filterByProperty<T>(
10
+ items: readonly T[],
11
+ property: keyof T,
12
+ value: unknown
13
+ ): T[] {
14
+ return items.filter((item) => item[property] === value);
15
+ }
16
+
17
+ /**
18
+ * Filter array by predicate function
19
+ */
20
+ export function filterByPredicate<T>(
21
+ items: readonly T[],
22
+ predicate: (item: T) => boolean
23
+ ): T[] {
24
+ return items.filter(predicate);
25
+ }
26
+
27
+ /**
28
+ * Filter array by time range (timestamp property)
29
+ */
30
+ export function filterByTimeRange<T>(
31
+ items: readonly T[],
32
+ timestampProperty: keyof T,
33
+ startTime: number,
34
+ endTime: number
35
+ ): T[] {
36
+ return items.filter((item) => {
37
+ const timestamp = item[timestampProperty] as unknown as number;
38
+ return timestamp >= startTime && timestamp <= endTime;
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Filter array by multiple property values (OR logic)
44
+ */
45
+ export function filterByAnyProperty<T>(
46
+ items: readonly T[],
47
+ property: keyof T,
48
+ values: readonly unknown[]
49
+ ): T[] {
50
+ const valueSet = new Set(values);
51
+ return items.filter((item) => valueSet.has(item[property]));
52
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Array Reducer Utilities
3
+ * Reduce, group, chunk, and aggregation operations
4
+ */
5
+
6
+ /**
7
+ * Reduce array to sum of number property
8
+ */
9
+ export function sumByProperty<T>(
10
+ items: readonly T[],
11
+ numberProperty: keyof T
12
+ ): number {
13
+ return items.reduce((sum, item) => {
14
+ const value = item[numberProperty] as unknown as number;
15
+ return sum + (typeof value === "number" ? value : 0);
16
+ }, 0);
17
+ }
18
+
19
+ /**
20
+ * Group array by property value
21
+ */
22
+ export function groupByProperty<T>(
23
+ items: readonly T[],
24
+ property: keyof T
25
+ ): Map<unknown, T[]> {
26
+ const groups = new Map<unknown, T[]>();
27
+ for (const item of items) {
28
+ const key = item[property];
29
+ const existing = groups.get(key);
30
+ if (existing) {
31
+ existing.push(item);
32
+ } else {
33
+ groups.set(key, [item]);
34
+ }
35
+ }
36
+ return groups;
37
+ }
38
+
39
+ /**
40
+ * Chunk array into smaller arrays of specified size
41
+ */
42
+ export function chunkArray<T>(items: readonly T[], chunkSize: number): T[][] {
43
+ const result: T[][] = [];
44
+ for (let i = 0; i < items.length; i += chunkSize) {
45
+ result.push([...items.slice(i, i + chunkSize)]);
46
+ }
47
+ return result;
48
+ }
49
+
50
+ /**
51
+ * Get distinct values of a property from array
52
+ */
53
+ export function distinctByProperty<T>(
54
+ items: readonly T[],
55
+ property: keyof T
56
+ ): unknown[] {
57
+ const seen = new Set<unknown>();
58
+ const result: unknown[] = [];
59
+ for (const item of items) {
60
+ const value = item[property];
61
+ if (!seen.has(value)) {
62
+ seen.add(value);
63
+ result.push(value);
64
+ }
65
+ }
66
+ return result;
67
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Array Sorter Utilities
3
+ * Sort operations for arrays of objects
4
+ */
5
+
6
+ /**
7
+ * Sort array by date property (descending - newest first)
8
+ */
9
+ export function sortByDateDescending<T>(
10
+ items: readonly T[],
11
+ dateProperty: keyof T
12
+ ): T[] {
13
+ return [...items].sort((a, b) => {
14
+ const timeA = new Date(a[dateProperty] as unknown as string).getTime();
15
+ const timeB = new Date(b[dateProperty] as unknown as string).getTime();
16
+ return timeB - timeA;
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Sort array by date property (ascending - oldest first)
22
+ */
23
+ export function sortByDateAscending<T>(
24
+ items: readonly T[],
25
+ dateProperty: keyof T
26
+ ): T[] {
27
+ return [...items].sort((a, b) => {
28
+ const timeA = new Date(a[dateProperty] as unknown as string).getTime();
29
+ const timeB = new Date(b[dateProperty] as unknown as string).getTime();
30
+ return timeA - timeB;
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Sort array by number property (descending)
36
+ */
37
+ export function sortByNumberDescending<T>(
38
+ items: readonly T[],
39
+ numberProperty: keyof T
40
+ ): T[] {
41
+ return [...items].sort((a, b) => {
42
+ const numA = a[numberProperty] as unknown as number;
43
+ const numB = b[numberProperty] as unknown as number;
44
+ return numB - numA;
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Sort array by number property (ascending)
50
+ */
51
+ export function sortByNumberAscending<T>(
52
+ items: readonly T[],
53
+ numberProperty: keyof T
54
+ ): T[] {
55
+ return [...items].sort((a, b) => {
56
+ const numA = a[numberProperty] as unknown as number;
57
+ const numB = b[numberProperty] as unknown as number;
58
+ return numA - numB;
59
+ });
60
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Collection Utilities - Centralized Exports
3
+ * Re-exports all collection utilities from submodules
4
+ */
5
+
6
+ export * from './array-filters.util';
7
+ export * from './array-sorters.util';
8
+ export * from './array-reducers.util';
@@ -56,9 +56,14 @@ export class CostTracker {
56
56
  };
57
57
  }
58
58
  } catch (error) {
59
- // Silently return default cost info on error
59
+ // Log error but continue with default cost info
60
+ console.warn(
61
+ `[cost-tracker] Failed to get model cost info for ${modelId}:`,
62
+ error instanceof Error ? error.message : String(error)
63
+ );
60
64
  }
61
65
 
66
+ // Return default cost info (0 cost) if model not found or error occurred
62
67
  return {
63
68
  model: modelId,
64
69
  costPerRequest: 0,