@umituz/react-native-ai-fal-provider 3.2.38 → 3.2.40

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.38",
3
+ "version": "3.2.40",
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",
@@ -68,6 +68,7 @@
68
68
  "expo-clipboard": "^8.0.8",
69
69
  "expo-crypto": "^15.0.8",
70
70
  "expo-device": "^8.0.10",
71
+ "expo-document-picker": "~14.0.8",
71
72
  "expo-file-system": "^19.0.21",
72
73
  "expo-font": "^14.0.10",
73
74
  "expo-haptics": "^15.0.8",
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Global Type Declarations
3
+ * React Native globals and other global type definitions
4
+ */
5
+
6
+ declare const __DEV__: boolean;
7
+
8
+ declare global {
9
+ const __DEV__: boolean;
10
+ }
package/src/index.ts CHANGED
@@ -1,22 +1,156 @@
1
1
  /**
2
2
  * @umituz/react-native-ai-fal-provider
3
- * FAL AI provider for React Native - implements IAIProvider interface
3
+ * FAL AI provider for React Native - Public API exports
4
4
  */
5
5
 
6
- // Domain Layer
7
- export * from "./exports/domain";
6
+ // ─── Core Provider ───────────────────────────────────────────────────────────
7
+ export { FalProvider, falProvider } from "./infrastructure/services/fal-provider";
8
+ export type { FalProvider as FalProviderType } from "./infrastructure/services/fal-provider";
9
+ export { NSFWContentError } from "./infrastructure/services/nsfw-content-error";
8
10
 
9
- // Infrastructure Layer
10
- export * from "./exports/infrastructure";
11
-
12
- // Presentation Layer
13
- export * from "./exports/presentation";
11
+ // ─── React Hook ───────────────────────────────────────────────────────────────
12
+ export { useFalGeneration } from "./presentation/hooks/use-fal-generation";
13
+ export type {
14
+ UseFalGenerationOptions,
15
+ UseFalGenerationResult,
16
+ } from "./presentation/hooks/use-fal-generation";
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";
14
24
 
15
- // Init Module Factory
25
+ // ─── Initialization ───────────────────────────────────────────────────────────
26
+ export { initializeFalProvider } from "./init/initializeFalProvider";
16
27
  export {
17
28
  createAiProviderInitModule,
18
29
  type AiProviderInitModuleConfig,
19
- } from './init/createAiProviderInitModule';
30
+ } from "./init/createAiProviderInitModule";
31
+
32
+ // ─── Error Handling ───────────────────────────────────────────────────────────
33
+ export {
34
+ mapFalError,
35
+ parseFalError,
36
+ isFalErrorRetryable,
37
+ } from "./infrastructure/utils/fal-error-handler.util";
38
+ export { FalErrorType } from "./domain/entities/error.types";
39
+ export type {
40
+ FalErrorCategory,
41
+ FalErrorInfo,
42
+ FalErrorMessages,
43
+ } from "./domain/entities/error.types";
20
44
 
21
- // Direct Initialization
22
- export { initializeFalProvider } from './init/initializeFalProvider';
45
+ // ─── Utilities (public API) ────────────────────────────────────────────────────
46
+ export {
47
+ getErrorMessage,
48
+ getErrorMessageOr,
49
+ formatErrorMessage,
50
+ } from "./infrastructure/utils/helpers/error-helpers.util";
51
+
52
+ export {
53
+ IMAGE_URL_FIELDS,
54
+ isImageField,
55
+ } from "./infrastructure/utils/constants/image-fields.constants";
56
+ export type { ImageUrlField } from "./infrastructure/utils/constants/image-fields.constants";
57
+
58
+ export {
59
+ isDataUri,
60
+ isBase64DataUri,
61
+ extractMimeType,
62
+ extractBase64Content,
63
+ } from "./infrastructure/utils/validators/data-uri-validator.util";
64
+
65
+ export {
66
+ isEmptyString,
67
+ isNonEmptyString,
68
+ isString,
69
+ } from "./infrastructure/utils/validators/string-validator.util";
70
+
71
+ export {
72
+ isFalModelType,
73
+ isModelType,
74
+ isFalErrorType,
75
+ isValidBase64Image,
76
+ isValidApiKey,
77
+ isValidModelId,
78
+ isValidPrompt,
79
+ isValidTimeout,
80
+ isValidRetryCount,
81
+ } from "./infrastructure/utils/type-guards";
82
+
83
+ export {
84
+ formatImageDataUri,
85
+ extractBase64,
86
+ getDataUriExtension,
87
+ } from "./infrastructure/utils/image-helpers.util";
88
+
89
+ export {
90
+ uploadToFalStorage,
91
+ uploadMultipleToFalStorage,
92
+ } from "./infrastructure/utils/fal-storage.util";
93
+
94
+ export {
95
+ buildErrorMessage,
96
+ isDefined,
97
+ removeNullish,
98
+ generateUniqueId,
99
+ sleep,
100
+ } from "./infrastructure/utils/helpers";
101
+
102
+ export { preprocessInput } from "./infrastructure/utils/input-preprocessor.util";
103
+
104
+ export {
105
+ calculateVideoCredits,
106
+ calculateImageCredits,
107
+ calculateCreditsFromConfig,
108
+ } from "./infrastructure/utils/pricing/fal-pricing.util";
109
+ export type { GenerationResolution } from "./infrastructure/utils/pricing/fal-pricing.util";
110
+
111
+ // ─── Types (public API) ───────────────────────────────────────────────────────
112
+ export type {
113
+ FalConfig,
114
+ FalModel,
115
+ FalModelType,
116
+ FalModelPricing,
117
+ FalJobInput,
118
+ FalJobResult,
119
+ FalLogEntry,
120
+ FalQueueStatus,
121
+ FalSubscribeOptions,
122
+ } from "./domain/entities/fal.types";
123
+ export type { FalModelConfig } from "./domain/types/fal-model-config.types";
124
+ export type {
125
+ UpscaleOptions,
126
+ PhotoRestoreOptions,
127
+ FaceSwapOptions,
128
+ ImageToImagePromptConfig,
129
+ RemoveBackgroundOptions,
130
+ RemoveObjectOptions,
131
+ ReplaceBackgroundOptions,
132
+ VideoFromImageOptions,
133
+ TextToVideoOptions,
134
+ TextToVoiceOptions,
135
+ ImageFeatureType,
136
+ VideoFeatureType,
137
+ AIProviderConfig,
138
+ AIJobStatusType,
139
+ AILogEntry,
140
+ JobSubmission,
141
+ JobStatus,
142
+ ProviderProgressInfo,
143
+ SubscribeOptions,
144
+ RunOptions,
145
+ ProviderCapabilities,
146
+ ImageFeatureInputData,
147
+ VideoFeatureInputData,
148
+ IAIProvider,
149
+ } from "./domain/types";
150
+
151
+ // ─── Request Store Management ─────────────────────────────────────────────────
152
+ export {
153
+ cleanupRequestStore,
154
+ stopAutomaticCleanup,
155
+ } from "./infrastructure/services/request-store";
156
+ export type { ActiveRequest } from "./infrastructure/services/request-store";
@@ -90,14 +90,15 @@ function formatFalError(error: unknown): string {
90
90
  if (error.status === 402) {
91
91
  return "Insufficient credits. Please check your billing.";
92
92
  }
93
- return error.message || `API error (${error.status})`;
93
+ return error.message ?? `API error (${error.status})`;
94
94
  }
95
95
 
96
96
  if (error instanceof Error) {
97
97
  return error.message;
98
98
  }
99
99
 
100
- return String(error);
100
+ // Handle null/undefined/other types safely
101
+ return error != null ? String(error) : "Unknown error";
101
102
  }
102
103
 
103
104
  // ─── Retry Logic ────────────────────────────────────────────────────────────
@@ -113,8 +114,8 @@ function isRetryableSubscribeError(error: unknown): boolean {
113
114
  // Never retry NSFW
114
115
  if (error instanceof NSFWContentError) return false;
115
116
 
116
- // Never retry user cancellation
117
- if (error instanceof Error && error.message.includes("cancelled by user")) return false;
117
+ // Never retry user cancellation - check Error instance first
118
+ if (error instanceof Error && error.message?.includes("cancelled by user")) return false;
118
119
 
119
120
  // ApiError — check status code
120
121
  if (error instanceof ApiError) {
@@ -200,15 +201,16 @@ async function singleSubscribeAttempt<T = unknown>(
200
201
  ];
201
202
 
202
203
  if (signal) {
204
+ // Check for aborted state BEFORE adding listener to prevent race condition
205
+ if (signal.aborted) {
206
+ throw new Error("Request cancelled by user");
207
+ }
203
208
  const abortPromise = new Promise<never>((_, reject) => {
204
209
  abortHandler = () => reject(new Error("Request cancelled by user"));
205
210
  signal.addEventListener("abort", abortHandler, { once: true });
206
211
  listenerAdded = true;
207
212
  });
208
213
  promises.push(abortPromise);
209
- if (signal.aborted) {
210
- throw new Error("Request cancelled by user");
211
- }
212
214
  }
213
215
 
214
216
  const rawResult = await Promise.race(promises);
@@ -11,7 +11,7 @@ import type {
11
11
  } from "../../domain/types";
12
12
  import { DEFAULT_FAL_CONFIG, FAL_CAPABILITIES } from "./fal-provider.constants";
13
13
  import { handleFalSubscription, handleFalRun } from "./fal-provider-subscription";
14
- import { preprocessInput } from "../utils";
14
+ import { preprocessInput } from "../utils/input-preprocessor.util";
15
15
  import { getErrorMessage } from "../utils/helpers/error-helpers.util";
16
16
  import { generationLogCollector } from "../utils/log-collector";
17
17
  import type { LogEntry } from "../utils/log-collector";
@@ -56,7 +56,15 @@ function sortKeys(obj: unknown): unknown {
56
56
  * 64-bit combined hash space makes accidental collisions extremely unlikely.
57
57
  */
58
58
  export function createRequestKey(model: string, input: Record<string, unknown>): string {
59
- const inputStr = JSON.stringify(sortKeys(input));
59
+ let inputStr: string;
60
+ try {
61
+ inputStr = JSON.stringify(sortKeys(input));
62
+ } catch (error) {
63
+ // Handle circular references or non-serializable values
64
+ // Fallback to a simple string representation that won't throw
65
+ const simpleKeys = Object.keys(input).sort().join(",");
66
+ inputStr = `${simpleKeys}:${Object.keys(input).length}`;
67
+ }
60
68
 
61
69
  // FNV-1a hash
62
70
  let h1 = 0x811c9dc5;
@@ -85,7 +93,7 @@ export function getExistingRequest<T>(key: string): ActiveRequest<T> | undefined
85
93
  export function storeRequest<T>(key: string, request: ActiveRequest<T>): void {
86
94
  getRequestStore().set(key, {
87
95
  ...request,
88
- createdAt: request.createdAt ?? Date.now(),
96
+ createdAt: request.createdAt, // createdAt is required, no fallback needed
89
97
  });
90
98
  ensureCleanupRunning();
91
99
  }
@@ -61,7 +61,7 @@ function categorizeError(error: unknown): FalErrorCategory {
61
61
  }
62
62
 
63
63
  // 2. Standard Error - match message patterns
64
- const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
64
+ const message = (error instanceof Error ? error.message : error != null ? String(error) : "unknown").toLowerCase();
65
65
 
66
66
  for (const { type, patterns } of MESSAGE_PATTERNS) {
67
67
  if (patterns.some((p) => message.includes(p))) {
@@ -81,8 +81,11 @@ function extractMessage(error: unknown): string {
81
81
  // ValidationError - extract field-level messages
82
82
  if (error instanceof ValidationError) {
83
83
  const fieldErrors = error.fieldErrors;
84
- if (fieldErrors.length > 0) {
85
- const messages = fieldErrors.map((e) => e.msg).filter(Boolean);
84
+ if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
85
+ // Safely extract messages from field errors with validation
86
+ const messages = fieldErrors
87
+ .map((e) => (e && typeof e === 'object' && 'msg' in e && typeof e.msg === 'string' ? e.msg : null))
88
+ .filter((msg): msg is string => msg !== null);
86
89
  if (messages.length > 0) return messages.join("; ");
87
90
  }
88
91
  return error.message;
@@ -90,10 +93,35 @@ function extractMessage(error: unknown): string {
90
93
 
91
94
  // ApiError - extract from body or message
92
95
  if (error instanceof ApiError) {
93
- // body may contain detail array
94
- const body = error.body as { detail?: Array<{ msg?: string }> } | undefined;
95
- if (body?.detail?.[0]?.msg) {
96
- return body.detail[0].msg;
96
+ // body may contain detail array - validate structure before access
97
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
98
+ const body = error.body;
99
+
100
+ // Type guard for detail array structure
101
+ interface DetailItem {
102
+ msg?: string;
103
+ }
104
+ interface BodyWithDetail {
105
+ detail?: DetailItem[];
106
+ }
107
+
108
+ const validatedBody = body as BodyWithDetail | undefined;
109
+
110
+ if (
111
+ validatedBody &&
112
+ validatedBody.detail &&
113
+ Array.isArray(validatedBody.detail) &&
114
+ validatedBody.detail.length > 0
115
+ ) {
116
+ const firstItem = validatedBody.detail[0];
117
+ if (
118
+ firstItem &&
119
+ typeof firstItem === "object" &&
120
+ typeof firstItem.msg === "string" &&
121
+ firstItem.msg
122
+ ) {
123
+ return firstItem.msg;
124
+ }
97
125
  }
98
126
  return error.message;
99
127
  }
@@ -103,7 +131,7 @@ function extractMessage(error: unknown): string {
103
131
  return error.message;
104
132
  }
105
133
 
106
- return String(error);
134
+ return error != null ? String(error) : "Unknown error";
107
135
  }
108
136
 
109
137
  /**
@@ -10,6 +10,7 @@ import {
10
10
  deleteTempFile,
11
11
  } from "@umituz/react-native-design-system/filesystem";
12
12
  import { getErrorMessage } from './helpers/error-helpers.util';
13
+ import { getElapsedTime, getActualSizeKB } from './helpers';
13
14
  import { generationLogCollector } from './log-collector';
14
15
  import { UPLOAD_CONFIG } from '../services/fal-provider.constants';
15
16
 
@@ -72,13 +73,14 @@ async function withRetry<T>(
72
73
  */
73
74
  export async function uploadToFalStorage(base64: string, sessionId: string): Promise<string> {
74
75
  const startTime = Date.now();
75
- const sizeKB = Math.round(base64.length / 1024);
76
- const actualSizeKB = Math.round(sizeKB * 0.75); // base64 inflates ~33%
76
+ const actualSizeKB = getActualSizeKB(base64);
77
77
  generationLogCollector.log(sessionId, TAG, `Starting upload (~${actualSizeKB}KB actual)`);
78
78
 
79
79
  const tempUri = await base64ToTempFile(base64);
80
80
 
81
- if (!tempUri) {
81
+ // base64ToTempFile returns a string, so this check is for defensive programming
82
+ // in case the implementation changes in the future
83
+ if (!tempUri || typeof tempUri !== 'string') {
82
84
  throw new Error("Failed to create temporary file from base64 data");
83
85
  }
84
86
 
@@ -86,6 +88,12 @@ export async function uploadToFalStorage(base64: string, sessionId: string): Pro
86
88
  const url = await withRetry(
87
89
  async () => {
88
90
  const response = await fetch(tempUri);
91
+
92
+ // Validate response before processing blob
93
+ if (!response.ok) {
94
+ throw new Error(`Failed to fetch temp file: HTTP ${response.status} ${response.statusText}`);
95
+ }
96
+
89
97
  const blob = await response.blob();
90
98
  generationLogCollector.log(sessionId, TAG, `Blob created (${blob.size} bytes), uploading to FAL CDN...`);
91
99
  return withTimeout(
@@ -98,11 +106,11 @@ export async function uploadToFalStorage(base64: string, sessionId: string): Pro
98
106
  'upload',
99
107
  );
100
108
 
101
- const elapsed = Date.now() - startTime;
109
+ const elapsed = getElapsedTime(startTime);
102
110
  generationLogCollector.log(sessionId, TAG, `Upload complete in ${elapsed}ms`, { url, actualSizeKB, elapsed });
103
111
  return url;
104
112
  } catch (error) {
105
- const elapsed = Date.now() - startTime;
113
+ const elapsed = getElapsedTime(startTime);
106
114
  generationLogCollector.error(sessionId, TAG, `Upload FAILED after ${elapsed}ms: ${getErrorMessage(error)}`, { actualSizeKB, elapsed });
107
115
  throw error;
108
116
  } finally {
@@ -125,6 +133,12 @@ export async function uploadLocalFileToFalStorage(fileUri: string, sessionId: st
125
133
  const url = await withRetry(
126
134
  async () => {
127
135
  const response = await fetch(fileUri);
136
+
137
+ // Validate response before processing blob
138
+ if (!response.ok) {
139
+ throw new Error(`Failed to fetch local file: HTTP ${response.status} ${response.statusText}`);
140
+ }
141
+
128
142
  const blob = await response.blob();
129
143
  generationLogCollector.log(sessionId, TAG, `Local file blob (${blob.size} bytes), uploading...`);
130
144
  return withTimeout(
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Calculation Helper Utilities
3
+ * Centralized calculation functions for consistent metrics across the codebase
4
+ */
5
+
6
+ /**
7
+ * Calculate elapsed time in milliseconds from a start timestamp
8
+ *
9
+ * @param startTime - Start timestamp in milliseconds (from Date.now())
10
+ * @returns Elapsed time in milliseconds
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * const start = Date.now();
15
+ * // ... do work ...
16
+ * const elapsed = getElapsedTime(start); // e.g., 1234
17
+ * ```
18
+ */
19
+ export function getElapsedTime(startTime: number): number {
20
+ return Date.now() - startTime;
21
+ }
22
+
23
+ /**
24
+ * Format elapsed time in milliseconds to human-readable string
25
+ *
26
+ * @param elapsedMs - Elapsed time in milliseconds
27
+ * @returns Formatted string (e.g., "1234ms", "1.2s")
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * formatDuration(1234); // "1234ms"
32
+ * formatDuration(1200); // "1.2s"
33
+ * formatDuration(1500); // "1.5s"
34
+ * ```
35
+ */
36
+ export function formatDuration(elapsedMs: number): string {
37
+ if (elapsedMs < 1000) {
38
+ return `${Math.round(elapsedMs)}ms`;
39
+ }
40
+ return `${(elapsedMs / 1000).toFixed(1)}s`;
41
+ }
42
+
43
+ /**
44
+ * Convert bytes to kilobytes (KB)
45
+ *
46
+ * @param bytes - Size in bytes
47
+ * @returns Size in kilobytes (rounded)
48
+ *
49
+ * @example
50
+ * ```typescript
51
+ * bytesToKB(5000); // 5
52
+ * bytesToKB(1536); // 2
53
+ * ```
54
+ */
55
+ export function bytesToKB(bytes: number): number {
56
+ return Math.round(bytes / 1024);
57
+ }
58
+
59
+ /**
60
+ * Calculate actual file size from base64 string
61
+ * Base64 encoding inflates size by ~33%, so actual size is ~75% of base64 length
62
+ *
63
+ * @param base64String - Base64 encoded string
64
+ * @returns Actual size in kilobytes (rounded)
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * getActualSizeKB("data:image/png;base64,iVBOR..."); // ~3KB for 4KB base64
69
+ * ```
70
+ */
71
+ export function getActualSizeKB(base64String: string): number {
72
+ const base64SizeKB = Math.round(base64String.length / 1024);
73
+ // Base64 inflates by ~33%, so actual size is ~75%
74
+ return Math.round(base64SizeKB * 0.75);
75
+ }
76
+
77
+ /**
78
+ * Calculate base64 size in KB (before inflation adjustment)
79
+ *
80
+ * @param base64String - Base64 encoded string
81
+ * @returns Size in kilobytes (rounded)
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * getBase64SizeKB("data:image/png;base64,iVBOR..."); // 4
86
+ * ```
87
+ */
88
+ export function getBase64SizeKB(base64String: string): number {
89
+ return Math.round(base64String.length / 1024);
90
+ }
91
+
92
+ /**
93
+ * Calculate success rate as percentage
94
+ *
95
+ * @param successCount - Number of successful operations
96
+ * @param totalCount - Total number of operations
97
+ * @returns Success rate as percentage (0-100)
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * getSuccessRate(8, 10); // 80
102
+ * getSuccessRate(5, 5); // 100
103
+ * getSuccessRate(0, 10); // 0
104
+ * ```
105
+ */
106
+ export function getSuccessRate(successCount: number, totalCount: number): number {
107
+ if (totalCount === 0) return 0;
108
+ return Math.round((successCount / totalCount) * 100);
109
+ }
110
+
111
+ /**
112
+ * Format bytes to human-readable size
113
+ *
114
+ * @param bytes - Size in bytes
115
+ * @returns Formatted string (e.g., "1.5KB", "2.3MB")
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * formatBytes(1536); // "1.5KB"
120
+ * formatBytes(2048000); // "2.0MB"
121
+ * formatBytes(500); // "500B"
122
+ * ```
123
+ */
124
+ export function formatBytes(bytes: number): string {
125
+ if (bytes < 1024) return `${bytes}B`;
126
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
127
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
128
+ }
129
+
130
+ /**
131
+ * Calculate retry count suffix for logging
132
+ *
133
+ * @param attempt - Current attempt number (0-indexed)
134
+ * @param totalAttempts - Total number of attempts
135
+ * @returns Formatted suffix string
136
+ *
137
+ * @example
138
+ * ```typescript
139
+ * getRetrySuffix(1, 2); // " (succeeded on retry 1)"
140
+ * getRetrySuffix(0, 1); // ""
141
+ * ```
142
+ */
143
+ export function getRetrySuffix(attempt: number, _totalAttempts: number): string {
144
+ if (attempt > 0) {
145
+ return ` (succeeded on retry ${attempt})`;
146
+ }
147
+ return "";
148
+ }
149
+
150
+ /**
151
+ * Calculate failure info string for logging
152
+ *
153
+ * @param attempt - Current attempt number (0-indexed)
154
+ * @param totalAttempts - Total number of attempts
155
+ * @returns Formatted failure info string
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * getFailureInfo(1, 2); // " after 2 attempts"
160
+ * getFailureInfo(0, 1); // ""
161
+ * ```
162
+ */
163
+ export function getFailureInfo(attempt: number, _totalAttempts: number): string {
164
+ if (attempt > 0) {
165
+ return ` after ${attempt + 1} attempts`;
166
+ }
167
+ return "";
168
+ }
@@ -63,24 +63,3 @@ export function formatErrorMessage(error: unknown, context: string): string {
63
63
  return `${context}: ${getErrorMessage(error)}`;
64
64
  }
65
65
 
66
- /**
67
- * Extract error name (for Error instances)
68
- * Returns undefined for non-Error types
69
- *
70
- * @param error - The error to extract name from
71
- * @returns The error name or undefined
72
- */
73
- export function getErrorName(error: unknown): string | undefined {
74
- return error instanceof Error ? error.name : undefined;
75
- }
76
-
77
- /**
78
- * Extract error stack trace
79
- * Returns undefined for non-Error types
80
- *
81
- * @param error - The error to extract stack from
82
- * @returns The error stack trace or undefined
83
- */
84
- export function getErrorStack(error: unknown): string | undefined {
85
- return error instanceof Error ? error.stack : undefined;
86
- }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Helper Utilities - Centralized Exports
3
- * Re-exports all helper utilities from submodules
4
3
  */
5
4
 
6
5
  export * from './timing-helpers.util';
7
6
  export * from './object-helpers.util';
7
+ export * from './calculation-helpers.util';
8
+ export * from './error-helpers.util';
@@ -173,9 +173,7 @@ export async function preprocessInput(
173
173
 
174
174
  if (failedUploads.length > 0) {
175
175
  const successCount = individualUploadResults.length - failedUploads.length;
176
- const errorMessages = failedUploads.map((r) =>
177
- r.status === 'rejected' ? getErrorMessage(r.reason) : 'Unknown error'
178
- );
176
+ const errorMessages = failedUploads.map((r) => getErrorMessage(r.reason));
179
177
  generationLogCollector.error(sessionId, TAG, `Individual uploads: ${successCount}/${individualUploadResults.length} succeeded`, { errors: errorMessages });
180
178
  throw new Error(classifyUploadError(errorMessages[0]));
181
179
  }
@@ -47,7 +47,25 @@ export function calculateCreditsFromConfig(
47
47
  duration: number,
48
48
  resolution: string,
49
49
  ): number {
50
- const costPerSec = config.pricing.costPerSecond[resolution] ?? 0;
50
+ // Validate config structure before accessing nested properties
51
+ if (
52
+ !config ||
53
+ typeof config !== "object" ||
54
+ !config.pricing ||
55
+ typeof config.pricing !== "object" ||
56
+ !config.pricing.costPerSecond ||
57
+ typeof config.pricing.costPerSecond !== "object"
58
+ ) {
59
+ throw new Error("Invalid VideoModelConfig: pricing structure is missing or invalid");
60
+ }
61
+
62
+ const costPerSecondMap = config.pricing.costPerSecond;
63
+ const costPerSec = costPerSecondMap[resolution] ?? 0;
64
+
65
+ if (typeof costPerSec !== "number" || costPerSec < 0) {
66
+ throw new Error(`Invalid cost per second for resolution "${resolution}": must be a non-negative number`);
67
+ }
68
+
51
69
  const cost = costPerSec * duration;
52
70
  return Math.max(1, Math.ceil((cost * MARKUP) / CREDIT_PRICE));
53
71
  }
@@ -1,8 +1,6 @@
1
1
  /**
2
2
  * Type Guards - Centralized Exports
3
- * Re-exports all type guard utilities from submodules
4
3
  */
5
4
 
6
- export * from './constants';
7
5
  export * from './model-type-guards.util';
8
6
  export * from './validation-guards.util';
@@ -22,7 +22,10 @@ export function isValidBase64Image(value: unknown): boolean {
22
22
 
23
23
  // Check data URI prefix - use direct check instead of type guard to avoid type narrowing issues
24
24
  if (value.startsWith("data:image/")) {
25
- const base64Part = value.split("base64,")[1];
25
+ const parts = value.split("base64,");
26
+ // Ensure split produced at least 2 parts and the second part exists
27
+ if (parts.length < 2) return false;
28
+ const base64Part = parts[1];
26
29
  if (!base64Part) return false;
27
30
  return base64Part.length >= MIN_BASE64_IMAGE_LENGTH;
28
31
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { providerRegistry } from '@umituz/react-native-ai-generation-content';
7
- import { falProvider } from '../infrastructure/services';
7
+ import { falProvider } from '../infrastructure/services/fal-provider';
8
8
 
9
9
  /**
10
10
  * InitModule interface (from @umituz/react-native-design-system)
@@ -79,6 +79,8 @@ export function createAiProviderInitModule(
79
79
 
80
80
  return Promise.resolve(true);
81
81
  } catch (error) {
82
+ // Use console.error for init module failures (logging may not be initialized yet)
83
+ // This is a critical startup error, so we want it visible
82
84
  console.error('[AiProviderInitModule] Initialization failed:', error);
83
85
  throw error;
84
86
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { providerRegistry } from '@umituz/react-native-ai-generation-content';
7
- import { falProvider } from '../infrastructure/services';
7
+ import { falProvider } from '../infrastructure/services/fal-provider';
8
8
 
9
9
  /**
10
10
  * Initializes FAL provider and registers it with providerRegistry in one call.
@@ -1,51 +0,0 @@
1
- /**
2
- * Domain Layer Exports
3
- */
4
-
5
- export type {
6
- FalConfig,
7
- FalModel,
8
- FalModelType,
9
- FalModelPricing,
10
- FalJobInput,
11
- FalJobResult,
12
- FalLogEntry,
13
- FalQueueStatus,
14
- FalSubscribeOptions,
15
- } from "../domain/entities/fal.types";
16
-
17
- export { FalErrorType } from "../domain/entities/error.types";
18
- export type {
19
- FalErrorCategory,
20
- FalErrorInfo,
21
- FalErrorMessages,
22
- } from "../domain/entities/error.types";
23
-
24
- export type { FalModelConfig } from "../domain/types/fal-model-config.types";
25
-
26
- export type {
27
- UpscaleOptions,
28
- PhotoRestoreOptions,
29
- FaceSwapOptions,
30
- ImageToImagePromptConfig,
31
- RemoveBackgroundOptions,
32
- RemoveObjectOptions,
33
- ReplaceBackgroundOptions,
34
- VideoFromImageOptions,
35
- TextToVideoOptions,
36
- TextToVoiceOptions,
37
- ImageFeatureType,
38
- VideoFeatureType,
39
- AIProviderConfig,
40
- AIJobStatusType,
41
- AILogEntry,
42
- JobSubmission,
43
- JobStatus,
44
- ProviderProgressInfo,
45
- SubscribeOptions,
46
- RunOptions,
47
- ProviderCapabilities,
48
- ImageFeatureInputData,
49
- VideoFeatureInputData,
50
- IAIProvider,
51
- } from "../domain/types";
@@ -1,76 +0,0 @@
1
- /**
2
- * Infrastructure Layer Exports
3
- */
4
-
5
- export {
6
- FalProvider,
7
- falProvider,
8
- NSFWContentError,
9
- cleanupRequestStore,
10
- stopAutomaticCleanup,
11
- } from "../infrastructure/services";
12
- export type { FalProviderType, ActiveRequest } from "../infrastructure/services";
13
-
14
- export {
15
- mapFalError,
16
- parseFalError,
17
- isFalErrorRetryable,
18
- } from "../infrastructure/utils";
19
-
20
- export {
21
- getErrorMessage,
22
- getErrorMessageOr,
23
- formatErrorMessage,
24
- } from "../infrastructure/utils/helpers/error-helpers.util";
25
-
26
- export {
27
- IMAGE_URL_FIELDS,
28
- isImageField,
29
- } from "../infrastructure/utils/constants/image-fields.constants";
30
- export type {
31
- ImageUrlField,
32
- } from "../infrastructure/utils/constants/image-fields.constants";
33
-
34
- export {
35
- isDataUri,
36
- isBase64DataUri,
37
- extractMimeType,
38
- extractBase64Content,
39
- } from "../infrastructure/utils/validators/data-uri-validator.util";
40
- export {
41
- isEmptyString,
42
- isNonEmptyString,
43
- isString,
44
- } from "../infrastructure/utils/validators/string-validator.util";
45
-
46
- export {
47
- isFalModelType,
48
- isModelType,
49
- isFalErrorType,
50
- isValidBase64Image,
51
- isValidApiKey,
52
- isValidModelId,
53
- isValidPrompt,
54
- isValidTimeout,
55
- isValidRetryCount,
56
- } from "../infrastructure/utils";
57
-
58
- export {
59
- formatImageDataUri,
60
- extractBase64,
61
- getDataUriExtension,
62
- uploadToFalStorage,
63
- uploadMultipleToFalStorage,
64
- buildErrorMessage,
65
- isDefined,
66
- removeNullish,
67
- generateUniqueId,
68
- sleep,
69
- } from "../infrastructure/utils";
70
-
71
- export {
72
- calculateVideoCredits,
73
- calculateImageCredits,
74
- calculateCreditsFromConfig,
75
- } from "../infrastructure/utils";
76
- export type { GenerationResolution } from "../infrastructure/utils";
@@ -1,14 +0,0 @@
1
- /**
2
- * Presentation Layer Exports
3
- */
4
-
5
- export { useFalGeneration } from "../presentation/hooks";
6
- export type { UseFalGenerationOptions, UseFalGenerationResult } from "../presentation/hooks";
7
-
8
- export {
9
- FalGenerationStateManager,
10
- } from "../infrastructure/utils/fal-generation-state-manager.util";
11
- export type {
12
- GenerationState,
13
- GenerationStateOptions,
14
- } from "../infrastructure/utils/fal-generation-state-manager.util";
@@ -1,13 +0,0 @@
1
- /**
2
- * Services Index
3
- */
4
-
5
- export { FalProvider, falProvider } from "./fal-provider";
6
- export type { FalProvider as FalProviderType } from "./fal-provider";
7
- export { NSFWContentError } from "./nsfw-content-error";
8
-
9
- export {
10
- cleanupRequestStore,
11
- stopAutomaticCleanup,
12
- } from "./request-store";
13
- export type { ActiveRequest } from "./request-store";
@@ -1,55 +0,0 @@
1
- /**
2
- * Utils Index
3
- */
4
-
5
- export {
6
- mapFalError,
7
- parseFalError,
8
- isFalErrorRetryable,
9
- } from "./fal-error-handler.util";
10
-
11
- export {
12
- isFalModelType,
13
- isModelType,
14
- isFalErrorType,
15
- isValidBase64Image,
16
- isValidApiKey,
17
- isValidModelId,
18
- isValidPrompt,
19
- isValidTimeout,
20
- isValidRetryCount,
21
- } from "./type-guards";
22
-
23
- export {
24
- formatImageDataUri,
25
- extractBase64,
26
- getDataUriExtension,
27
- } from "./image-helpers.util";
28
-
29
- export {
30
- uploadToFalStorage,
31
- uploadMultipleToFalStorage,
32
- } from "./fal-storage.util";
33
-
34
- export {
35
- buildErrorMessage,
36
- isDefined,
37
- removeNullish,
38
- generateUniqueId,
39
- sleep,
40
- } from "./helpers";
41
-
42
- export { preprocessInput } from "./input-preprocessor.util";
43
-
44
- export { generationLogCollector } from "./log-collector";
45
- export type { LogEntry } from "./log-collector";
46
-
47
- export { FalGenerationStateManager } from "./fal-generation-state-manager.util";
48
- export type { GenerationState } from "./fal-generation-state-manager.util";
49
-
50
- export {
51
- calculateVideoCredits,
52
- calculateImageCredits,
53
- calculateCreditsFromConfig,
54
- } from "./pricing/fal-pricing.util";
55
- export type { GenerationResolution } from "./pricing/fal-pricing.util";
@@ -1,6 +0,0 @@
1
- /**
2
- * Parser Utilities
3
- */
4
-
5
- export * from './json-parsers.util';
6
- export * from './object-transformers.util';
@@ -1,73 +0,0 @@
1
- /**
2
- * JSON Parser Utilities
3
- * Safe JSON parsing and validation operations
4
- */
5
-
6
- import { getErrorMessage } from '../helpers/error-helpers.util';
7
-
8
- /**
9
- * Safely parse JSON with fallback
10
- */
11
- export function safeJsonParse<T = unknown>(
12
- data: string,
13
- fallback: T
14
- ): T {
15
- try {
16
- return JSON.parse(data) as T;
17
- } catch (error) {
18
- console.warn(
19
- '[json-parsers] Failed to parse JSON, using fallback:',
20
- getErrorMessage(error),
21
- { dataPreview: data.substring(0, 100) }
22
- );
23
- return fallback;
24
- }
25
- }
26
-
27
- /**
28
- * Safely parse JSON with null fallback
29
- */
30
- export function safeJsonParseOrNull<T = unknown>(data: string): T | null {
31
- try {
32
- return JSON.parse(data) as T;
33
- } catch (error) {
34
- console.warn(
35
- '[json-parsers] Failed to parse JSON, returning null:',
36
- getErrorMessage(error),
37
- { dataPreview: data.substring(0, 100) }
38
- );
39
- return null;
40
- }
41
- }
42
-
43
- /**
44
- * Safely stringify object with fallback
45
- */
46
- export function safeJsonStringify(
47
- data: unknown,
48
- fallback: string
49
- ): string {
50
- try {
51
- return JSON.stringify(data);
52
- } catch (error) {
53
- console.warn(
54
- '[json-parsers] Failed to stringify object, using fallback:',
55
- getErrorMessage(error),
56
- { dataType: typeof data }
57
- );
58
- return fallback;
59
- }
60
- }
61
-
62
- /**
63
- * Check if string is valid JSON
64
- */
65
- export function isValidJson(data: string): boolean {
66
- try {
67
- JSON.parse(data);
68
- return true;
69
- } catch {
70
- // Don't log here - this is expected to fail for validation checks
71
- return false;
72
- }
73
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * Object Transformer Utilities
3
- * Clone, merge, pick, and omit operations
4
- */
5
-
6
- /**
7
- * Deep clone object using JSON serialization
8
- * Throws on failure (circular references, non-serializable values)
9
- * No silent fallback - caller must handle errors explicitly
10
- */
11
- export function deepClone<T>(data: T): T {
12
- const serialized = JSON.stringify(data);
13
- return JSON.parse(serialized) as T;
14
- }
15
-
16
- /**
17
- * Merge objects with later objects overriding earlier ones
18
- */
19
- export function mergeObjects<T extends Record<string, unknown>>(
20
- ...objects: Partial<T>[]
21
- ): T {
22
- return Object.assign({}, ...objects) as T;
23
- }
24
-
25
- /**
26
- * Pick specified properties from object
27
- */
28
- export function pickProperties<T extends Record<string, unknown>, K extends keyof T>(
29
- obj: T,
30
- keys: readonly K[]
31
- ): Pick<T, K> {
32
- const result = {} as Pick<T, K>;
33
- for (const key of keys) {
34
- if (key in obj) {
35
- result[key] = obj[key];
36
- }
37
- }
38
- return result;
39
- }
40
-
41
- /**
42
- * Omit specified properties from object
43
- */
44
- export function omitProperties<T extends Record<string, unknown>, K extends keyof T>(
45
- obj: T,
46
- keys: readonly K[]
47
- ): Omit<T, K> {
48
- const result = { ...obj };
49
- for (const key of keys) {
50
- delete result[key];
51
- }
52
- return result as Omit<T, K>;
53
- }
@@ -1,6 +0,0 @@
1
- /**
2
- * Presentation Layer Hooks Index
3
- */
4
-
5
- export { useFalGeneration } from "./use-fal-generation";
6
- export type { UseFalGenerationOptions, UseFalGenerationResult } from "./use-fal-generation";