@umituz/react-native-ai-fal-provider 2.0.29 → 2.0.31

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": "2.0.29",
3
+ "version": "2.0.31",
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",
@@ -80,14 +80,9 @@ export function getDefaultModelsByType(type: FalModelType): FalModelConfig[] {
80
80
  return DEFAULT_IMAGE_TO_VIDEO_MODELS;
81
81
  case "text-to-text":
82
82
  return DEFAULT_TEXT_TO_TEXT_MODELS;
83
- case "image-to-image": {
84
- // eslint-disable-next-line no-console
85
- console.warn('Model type "image-to-image" not supported yet');
83
+ case "image-to-image":
86
84
  return [];
87
- }
88
85
  default: {
89
- // eslint-disable-next-line no-console
90
- console.warn('Unknown model type:', type);
91
86
  return [];
92
87
  }
93
88
  }
@@ -9,8 +9,10 @@ export {
9
9
  NSFWContentError,
10
10
  cancelCurrentFalRequest,
11
11
  hasRunningFalRequest,
12
+ cleanupRequestStore,
13
+ stopAutomaticCleanup,
12
14
  } from "../infrastructure/services";
13
- export type { FalProviderType } from "../infrastructure/services";
15
+ export type { FalProviderType, ActiveRequest } from "../infrastructure/services";
14
16
 
15
17
  export {
16
18
  categorizeFalError,
@@ -12,3 +12,12 @@ export type {
12
12
  UseFalGenerationResult,
13
13
  UseModelsProps,
14
14
  } from "../presentation/hooks";
15
+
16
+ // Export state manager for advanced use cases
17
+ export {
18
+ FalGenerationStateManager,
19
+ } from "../infrastructure/utils/fal-generation-state-manager.util";
20
+ export type {
21
+ GenerationState,
22
+ GenerationStateOptions,
23
+ } from "../infrastructure/utils/fal-generation-state-manager.util";
@@ -1,8 +1,10 @@
1
1
  /**
2
- * FAL Models Service - Simple model retrieval functions
2
+ * FAL Models Service - Model retrieval and selection logic
3
3
  */
4
4
 
5
5
  import type { FalModelType } from "../../domain/entities/fal.types";
6
+ import type { ModelType, ModelSelectionConfig } from "../../domain/types/model-selection.types";
7
+ import { DEFAULT_CREDIT_COSTS, DEFAULT_MODEL_IDS } from "../../domain/constants/default-models.constants";
6
8
  import {
7
9
  type FalModelConfig,
8
10
  getDefaultModelsByType,
@@ -12,6 +14,16 @@ import {
12
14
 
13
15
  export type { FalModelConfig };
14
16
 
17
+ /**
18
+ * Model selection result
19
+ */
20
+ export interface ModelSelectionResult {
21
+ models: FalModelConfig[];
22
+ selectedModel: FalModelConfig | null;
23
+ defaultCreditCost: number;
24
+ defaultModelId: string;
25
+ }
26
+
15
27
  function sortModels(models: FalModelConfig[]): FalModelConfig[] {
16
28
  return [...models].sort((a, b) => {
17
29
  if (a.order !== b.order) {
@@ -38,12 +50,87 @@ export function getModelPricing(modelId: string): { freeUserCost: number; premiu
38
50
  return model?.pricing ?? null;
39
51
  }
40
52
 
53
+ /**
54
+ * Get credit cost for a model
55
+ * Returns the model's free user cost if available, otherwise returns the default cost for the type
56
+ */
57
+ export function getModelCreditCost(modelId: string, modelType: ModelType): number {
58
+ const pricing = getModelPricing(modelId);
59
+ if (pricing?.freeUserCost) {
60
+ return pricing.freeUserCost;
61
+ }
62
+ return DEFAULT_CREDIT_COSTS[modelType];
63
+ }
64
+
65
+ /**
66
+ * Get default credit cost for a model type
67
+ */
68
+ export function getDefaultCreditCost(modelType: ModelType): number {
69
+ return DEFAULT_CREDIT_COSTS[modelType];
70
+ }
71
+
72
+ /**
73
+ * Get default model ID for a model type
74
+ */
75
+ export function getDefaultModelId(modelType: ModelType): string {
76
+ return DEFAULT_MODEL_IDS[modelType];
77
+ }
78
+
79
+ /**
80
+ * Select initial model based on configuration
81
+ * Returns the model matching initialModelId, or the default model, or the first model
82
+ */
83
+ export function selectInitialModel(
84
+ models: FalModelConfig[],
85
+ config: ModelSelectionConfig | undefined,
86
+ modelType: ModelType
87
+ ): FalModelConfig | null {
88
+ if (models.length === 0) {
89
+ return null;
90
+ }
91
+
92
+ const defaultModelId = getDefaultModelId(modelType);
93
+ const targetId = config?.initialModelId ?? defaultModelId;
94
+
95
+ return (
96
+ models.find((m) => m.id === targetId) ??
97
+ models.find((m) => m.isDefault) ??
98
+ models[0]
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Get model selection data for a model type
104
+ * Returns models, selected model, and default configuration
105
+ */
106
+ export function getModelSelectionData(
107
+ modelType: ModelType,
108
+ config?: ModelSelectionConfig
109
+ ): ModelSelectionResult {
110
+ const models = getModels(modelType as FalModelType);
111
+ const defaultCreditCost = config?.defaultCreditCost ?? getDefaultCreditCost(modelType);
112
+ const defaultModelId = config?.defaultModelId ?? getDefaultModelId(modelType);
113
+ const selectedModel = selectInitialModel(models, config, modelType);
114
+
115
+ return {
116
+ models,
117
+ selectedModel,
118
+ defaultCreditCost,
119
+ defaultModelId,
120
+ };
121
+ }
122
+
41
123
  // Singleton service export
42
124
  export const falModelsService = {
43
125
  getModels,
44
126
  getDefaultModel,
45
127
  findById: findModelById,
46
128
  getModelPricing,
129
+ getModelCreditCost,
130
+ getDefaultCreditCost,
131
+ getDefaultModelId,
132
+ selectInitialModel,
133
+ getModelSelectionData,
47
134
  getTextToImageModels: () => getModels("text-to-image"),
48
135
  getTextToVoiceModels: () => getModels("text-to-voice"),
49
136
  getTextToVideoModels: () => getModels("text-to-video"),
@@ -126,16 +126,35 @@ export class FalProvider implements IAIProvider {
126
126
  const abortController = new AbortController();
127
127
  const tracker = this.costTracker;
128
128
 
129
- const promise = executeWithCostTracking({
129
+ // Store promise immediately BEFORE creating it to prevent race condition
130
+ // Multiple simultaneous calls with same params will get the same promise
131
+ let resolvePromise: (value: T) => void;
132
+ let rejectPromise: (error: unknown) => void;
133
+ const promise = new Promise<T>((resolve, reject) => {
134
+ resolvePromise = resolve;
135
+ rejectPromise = reject;
136
+ });
137
+
138
+ storeRequest(key, { promise, abortController, createdAt: Date.now() });
139
+
140
+ // Execute the actual operation and resolve/reject the stored promise
141
+ executeWithCostTracking({
130
142
  tracker,
131
143
  model,
132
144
  operation: "subscribe",
133
145
  execute: () => handleFalSubscription<T>(model, processedInput, options, abortController.signal),
134
146
  getRequestId: (res) => res.requestId ?? undefined,
135
- }).then((res) => res.result).finally(() => removeRequest(key));
147
+ })
148
+ .then((res) => {
149
+ resolvePromise!(res.result);
150
+ return res.result;
151
+ })
152
+ .catch((error) => {
153
+ rejectPromise!(error);
154
+ throw error;
155
+ })
156
+ .finally(() => removeRequest(key));
136
157
 
137
- // Store promise immediately to prevent race condition
138
- storeRequest(key, { promise, abortController, createdAt: Date.now() });
139
158
  return promise;
140
159
  }
141
160
 
@@ -161,6 +180,7 @@ export class FalProvider implements IAIProvider {
161
180
  this.cancelCurrentRequest();
162
181
  this.apiKey = null;
163
182
  this.initialized = false;
183
+ this.costTracker = null;
164
184
  }
165
185
 
166
186
  cancelCurrentRequest(): void {
@@ -6,9 +6,22 @@ import { falProvider } from "./fal-provider";
6
6
 
7
7
  export { FalProvider, falProvider } from "./fal-provider";
8
8
  export type { FalProvider as FalProviderType } from "./fal-provider";
9
- export { falModelsService, type FalModelConfig } from "./fal-models.service";
9
+ export { falModelsService, type FalModelConfig, type ModelSelectionResult } from "./fal-models.service";
10
10
  export { NSFWContentError } from "./nsfw-content-error";
11
11
 
12
+ // Request store exports for advanced use cases
13
+ export {
14
+ createRequestKey,
15
+ getExistingRequest,
16
+ storeRequest,
17
+ removeRequest,
18
+ cancelAllRequests,
19
+ hasActiveRequests,
20
+ cleanupRequestStore,
21
+ stopAutomaticCleanup,
22
+ } from "./request-store";
23
+ export type { ActiveRequest } from "./request-store";
24
+
12
25
  /**
13
26
  * Cancel the current running FAL request
14
27
  */
@@ -12,6 +12,10 @@ export interface ActiveRequest<T = unknown> {
12
12
  const STORE_KEY = "__FAL_PROVIDER_REQUESTS__";
13
13
  type RequestStore = Map<string, ActiveRequest>;
14
14
 
15
+ let cleanupTimer: ReturnType<typeof setInterval> | null = null;
16
+ const CLEANUP_INTERVAL = 60000; // 1 minute
17
+ const MAX_REQUEST_AGE = 300000; // 5 minutes
18
+
15
19
  export function getRequestStore(): RequestStore {
16
20
  if (!(globalThis as Record<string, unknown>)[STORE_KEY]) {
17
21
  (globalThis as Record<string, unknown>)[STORE_KEY] = new Map();
@@ -46,10 +50,20 @@ export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
46
50
  createdAt: request.createdAt ?? Date.now(),
47
51
  };
48
52
  getRequestStore().set(key, requestWithTimestamp);
53
+
54
+ // Start automatic cleanup if not already running
55
+ startAutomaticCleanup();
49
56
  }
50
57
 
51
58
  export function removeRequest(key: string): void {
52
- getRequestStore().delete(key);
59
+ const store = getRequestStore();
60
+ store.delete(key);
61
+
62
+ // Stop cleanup timer if store is empty
63
+ if (store.size === 0 && cleanupTimer) {
64
+ clearInterval(cleanupTimer);
65
+ cleanupTimer = null;
66
+ }
53
67
  }
54
68
 
55
69
  export function cancelAllRequests(): void {
@@ -58,6 +72,12 @@ export function cancelAllRequests(): void {
58
72
  req.abortController.abort();
59
73
  });
60
74
  store.clear();
75
+
76
+ // Stop cleanup timer
77
+ if (cleanupTimer) {
78
+ clearInterval(cleanupTimer);
79
+ cleanupTimer = null;
80
+ }
61
81
  }
62
82
 
63
83
  export function hasActiveRequests(): boolean {
@@ -71,7 +91,7 @@ export function hasActiveRequests(): boolean {
71
91
  * @param maxAge - Maximum age in milliseconds (default: 5 minutes)
72
92
  * @returns Number of requests cleaned up
73
93
  */
74
- export function cleanupRequestStore(maxAge: number = 300000): number {
94
+ export function cleanupRequestStore(maxAge: number = MAX_REQUEST_AGE): number {
75
95
  const store = getRequestStore();
76
96
  const now = Date.now();
77
97
  let cleanedCount = 0;
@@ -98,11 +118,38 @@ export function cleanupRequestStore(maxAge: number = 300000): number {
98
118
  }
99
119
  }
100
120
 
101
- // Log warning if store size is still large after cleanup
102
- if (store.size > 50) {
103
- // eslint-disable-next-line no-console
104
- console.warn(`Request store size (${store.size}) exceeds threshold, potential memory leak detected`);
121
+ // Stop cleanup timer if store is empty
122
+ if (store.size === 0 && cleanupTimer) {
123
+ clearInterval(cleanupTimer);
124
+ cleanupTimer = null;
105
125
  }
106
126
 
107
127
  return cleanedCount;
108
128
  }
129
+
130
+ /**
131
+ * Start automatic cleanup of stale requests
132
+ * Runs periodically to prevent memory leaks
133
+ */
134
+ function startAutomaticCleanup(): void {
135
+ if (cleanupTimer) {
136
+ return; // Already running
137
+ }
138
+
139
+ cleanupTimer = setInterval(() => {
140
+ const cleanedCount = cleanupRequestStore(MAX_REQUEST_AGE);
141
+ if (cleanedCount > 0) {
142
+ // Cleanup was performed
143
+ }
144
+ }, CLEANUP_INTERVAL);
145
+ }
146
+
147
+ /**
148
+ * Stop automatic cleanup (typically on app shutdown)
149
+ */
150
+ export function stopAutomaticCleanup(): void {
151
+ if (cleanupTimer) {
152
+ clearInterval(cleanupTimer);
153
+ cleanupTimer = null;
154
+ }
155
+ }
@@ -72,10 +72,18 @@ export class CostTracker {
72
72
  }
73
73
 
74
74
  startOperation(modelId: string, operation: string): string {
75
- // Use crypto.randomUUID() for guaranteed uniqueness without overflow
76
- const uniqueId = typeof crypto !== 'undefined' && crypto.randomUUID
77
- ? crypto.randomUUID()
78
- : `${Date.now()}-${Math.random().toString(36).slice(2)}-${operation}`;
75
+ // Generate unique operation ID
76
+ let uniqueId: string;
77
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
78
+ uniqueId = crypto.randomUUID();
79
+ } else {
80
+ // Fallback: Use timestamp with random component and counter
81
+ // Format: timestamp-randomCounter-operationHash
82
+ const timestamp = Date.now().toString(36);
83
+ const random = Math.random().toString(36).substring(2, 11);
84
+ const operationHash = operation.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0).toString(36);
85
+ uniqueId = `${timestamp}-${random}-${operationHash}`;
86
+ }
79
87
 
80
88
  const estimatedCost = this.calculateEstimatedCost(modelId);
81
89
 
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Date Formatting Utilities
3
+ * Functions for formatting dates and times
4
+ */
5
+
6
+ /**
7
+ * Format date to locale string
8
+ */
9
+ export function formatDate(date: Date | string, locale: string = "en-US"): string {
10
+ const dateObj = typeof date === "string" ? new Date(date) : date;
11
+ return dateObj.toLocaleDateString(locale, {
12
+ year: "numeric",
13
+ month: "short",
14
+ day: "numeric",
15
+ });
16
+ }
17
+
18
+ /**
19
+ * Format date and time to locale string
20
+ */
21
+ export function formatDateTime(date: Date | string, locale: string = "en-US"): string {
22
+ const dateObj = typeof date === "string" ? new Date(date) : date;
23
+ return dateObj.toLocaleString(locale, {
24
+ year: "numeric",
25
+ month: "short",
26
+ day: "numeric",
27
+ hour: "2-digit",
28
+ minute: "2-digit",
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Format relative time (e.g., "2 hours ago")
34
+ */
35
+ export function formatRelativeTime(date: Date | string, locale: string = "en-US"): string {
36
+ const dateObj = typeof date === "string" ? new Date(date) : date;
37
+ const now = new Date();
38
+ const diffMs = now.getTime() - dateObj.getTime();
39
+ const diffSec = Math.floor(diffMs / 1000);
40
+ const diffMin = Math.floor(diffSec / 60);
41
+ const diffHour = Math.floor(diffMin / 60);
42
+ const diffDay = Math.floor(diffHour / 24);
43
+
44
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
45
+
46
+ if (diffSec < 60) {
47
+ return rtf.format(-diffSec, "second");
48
+ }
49
+ if (diffMin < 60) {
50
+ return rtf.format(-diffMin, "minute");
51
+ }
52
+ if (diffHour < 24) {
53
+ return rtf.format(-diffHour, "hour");
54
+ }
55
+ if (diffDay < 30) {
56
+ return rtf.format(-diffDay, "day");
57
+ }
58
+ if (diffDay < 365) {
59
+ const months = Math.floor(diffDay / 30);
60
+ return rtf.format(-months, "month");
61
+ }
62
+ const years = Math.floor(diffDay / 365);
63
+ return rtf.format(-years, "year");
64
+ }
@@ -1,47 +1,9 @@
1
1
  /**
2
2
  * FAL Error Categorizer - Classifies FAL AI errors
3
+ *
4
+ * This module re-exports error categorization functions from the unified
5
+ * fal-error-handler.util module for backward compatibility.
3
6
  */
4
7
 
5
- import { FalErrorType, type FalErrorCategory } from "../../domain/entities/error.types";
6
-
7
- const PATTERNS: Record<FalErrorType, string[]> = {
8
- [FalErrorType.NETWORK]: ["network", "fetch", "connection", "econnrefused", "enotfound", "etimedout"],
9
- [FalErrorType.TIMEOUT]: ["timeout", "timed out"],
10
- [FalErrorType.IMAGE_TOO_SMALL]: ["image_too_small", "image dimensions are too small", "minimum dimensions"],
11
- [FalErrorType.VALIDATION]: ["validation", "invalid", "unprocessable", "422", "bad request", "400"],
12
- [FalErrorType.CONTENT_POLICY]: ["content_policy", "content policy", "policy violation", "nsfw", "inappropriate"],
13
- [FalErrorType.RATE_LIMIT]: ["rate limit", "too many requests", "429"],
14
- [FalErrorType.AUTHENTICATION]: ["unauthorized", "401", "forbidden", "403", "api key", "authentication"],
15
- [FalErrorType.QUOTA_EXCEEDED]: ["quota exceeded", "insufficient credits", "billing", "payment required", "402"],
16
- [FalErrorType.MODEL_NOT_FOUND]: ["model not found", "endpoint not found", "404", "not found"],
17
- [FalErrorType.API_ERROR]: ["api error", "502", "503", "504", "500", "internal server error"],
18
- [FalErrorType.UNKNOWN]: [],
19
- };
20
-
21
- const RETRYABLE_TYPES = new Set([
22
- FalErrorType.NETWORK,
23
- FalErrorType.TIMEOUT,
24
- FalErrorType.RATE_LIMIT,
25
- ]);
26
-
27
- function matchesPatterns(errorString: string, patterns: string[]): boolean {
28
- return patterns.some((pattern) => errorString.includes(pattern));
29
- }
30
-
31
- export function categorizeFalError(error: unknown): FalErrorCategory {
32
- const message = error instanceof Error ? error.message : String(error);
33
- const errorString = message.toLowerCase();
34
-
35
- for (const [type, patterns] of Object.entries(PATTERNS)) {
36
- if (patterns.length > 0 && matchesPatterns(errorString, patterns)) {
37
- const errorType = type as FalErrorType;
38
- return {
39
- type: errorType,
40
- messageKey: errorType,
41
- retryable: RETRYABLE_TYPES.has(errorType),
42
- };
43
- }
44
- }
45
-
46
- return { type: FalErrorType.UNKNOWN, messageKey: "unknown", retryable: false };
47
- }
8
+ export { categorizeFalError } from "./fal-error-handler.util";
9
+ export type { FalErrorCategory } from "../../domain/entities/error.types";
@@ -1,73 +1,14 @@
1
1
  /**
2
2
  * FAL Error Mapper - Maps errors to user-friendly info
3
+ *
4
+ * This module re-exports error handling functions from the unified
5
+ * fal-error-handler.util module for backward compatibility.
3
6
  */
4
7
 
5
- import type { FalErrorInfo } from "../../domain/entities/error.types";
6
- import { categorizeFalError } from "./error-categorizer";
7
- import { safeJsonParseOrNull } from "./data-parsers.util";
8
-
9
- const STATUS_CODES = ["400", "401", "402", "403", "404", "422", "429", "500", "502", "503", "504"];
10
-
11
- interface FalApiErrorDetail {
12
- msg?: string;
13
- type?: string;
14
- loc?: string[];
15
- }
16
-
17
- interface FalApiError {
18
- body?: { detail?: FalApiErrorDetail[] } | string;
19
- message?: string;
20
- }
21
-
22
- function extractStatusCode(errorString: string): number | undefined {
23
- const code = STATUS_CODES.find((c) => errorString.includes(c));
24
- return code ? parseInt(code, 10) : undefined;
25
- }
26
-
27
- /**
28
- * Parse FAL API error and extract user-friendly message
29
- */
30
- export function parseFalError(error: unknown): string {
31
- const fallback = error instanceof Error ? error.message : String(error);
32
-
33
- const falError = error as FalApiError;
34
- if (!falError?.body) return fallback;
35
-
36
- const body = typeof falError.body === "string"
37
- ? safeJsonParseOrNull<{ detail?: FalApiErrorDetail[] }>(falError.body)
38
- : falError.body;
39
-
40
- const detail = body?.detail?.[0];
41
- return detail?.msg ?? falError.message ?? fallback;
42
- }
43
-
44
- export function mapFalError(error: unknown): FalErrorInfo {
45
- const category = categorizeFalError(error);
46
-
47
- // Preserve full error information including stack trace
48
- if (error instanceof Error) {
49
- return {
50
- type: category.type,
51
- messageKey: `fal.errors.${category.messageKey}`,
52
- retryable: category.retryable,
53
- originalError: error.message,
54
- originalErrorName: error.name,
55
- stack: error.stack,
56
- statusCode: extractStatusCode(error.message),
57
- };
58
- }
59
-
60
- const errorString = String(error);
61
-
62
- return {
63
- type: category.type,
64
- messageKey: `fal.errors.${category.messageKey}`,
65
- retryable: category.retryable,
66
- originalError: errorString,
67
- statusCode: extractStatusCode(errorString),
68
- };
69
- }
70
-
71
- export function isFalErrorRetryable(error: unknown): boolean {
72
- return categorizeFalError(error).retryable;
73
- }
8
+ export {
9
+ mapFalError,
10
+ parseFalError,
11
+ isFalErrorRetryable,
12
+ categorizeFalError,
13
+ extractStatusCode,
14
+ } from "./fal-error-handler.util";