@umituz/react-native-ai-fal-provider 3.2.52 → 3.2.53

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/domain/services/ErrorClassificationService.ts +201 -0
  3. package/src/domain/services/ImageProcessingService.ts +89 -0
  4. package/src/domain/services/PricingService.ts +101 -0
  5. package/src/domain/services/ValidationService.ts +132 -0
  6. package/src/domain/services/index.ts +13 -0
  7. package/src/index.ts +24 -22
  8. package/src/infrastructure/utils/fal-error-handler.util.ts +14 -146
  9. package/src/infrastructure/utils/fal-storage.util.ts +2 -3
  10. package/src/infrastructure/utils/helpers/index.ts +2 -4
  11. package/src/infrastructure/utils/image-helpers.util.ts +13 -27
  12. package/src/infrastructure/utils/input-preprocessor.util.ts +6 -12
  13. package/src/infrastructure/utils/input-validator.util.ts +17 -156
  14. package/src/infrastructure/utils/pricing/fal-pricing.util.ts +26 -53
  15. package/src/infrastructure/utils/type-guards/index.ts +2 -2
  16. package/src/shared/helpers.ts +149 -0
  17. package/src/shared/index.ts +8 -0
  18. package/src/shared/type-guards.ts +122 -0
  19. package/src/shared/validators.ts +281 -0
  20. package/src/infrastructure/utils/helpers/calculation-helpers.util.ts +0 -21
  21. package/src/infrastructure/utils/helpers/error-helpers.util.ts +0 -65
  22. package/src/infrastructure/utils/helpers/object-helpers.util.ts +0 -44
  23. package/src/infrastructure/utils/helpers/timing-helpers.util.ts +0 -11
  24. package/src/infrastructure/utils/type-guards/model-type-guards.util.ts +0 -56
  25. package/src/infrastructure/utils/type-guards/validation-guards.util.ts +0 -101
  26. package/src/infrastructure/utils/validators/data-uri-validator.util.ts +0 -91
  27. package/src/infrastructure/utils/validators/string-validator.util.ts +0 -64
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-fal-provider",
3
- "version": "3.2.52",
3
+ "version": "3.2.53",
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",
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Domain Service: Error Classification
3
+ * Pure business logic for categorizing and extracting error information
4
+ * No infrastructure dependencies (except for @fal-ai/client types)
5
+ */
6
+
7
+ import { ApiError, ValidationError } from "@fal-ai/client";
8
+ import type { FalErrorInfo, FalErrorCategory } from "../entities/error.types";
9
+ import { FalErrorType } from "../entities/error.types";
10
+
11
+ /**
12
+ * HTTP status code to error type mapping
13
+ */
14
+ const STATUS_TO_ERROR_TYPE: Record<number, FalErrorType> = {
15
+ 400: FalErrorType.VALIDATION,
16
+ 401: FalErrorType.AUTHENTICATION,
17
+ 402: FalErrorType.QUOTA_EXCEEDED,
18
+ 403: FalErrorType.AUTHENTICATION,
19
+ 404: FalErrorType.MODEL_NOT_FOUND,
20
+ 422: FalErrorType.VALIDATION,
21
+ 429: FalErrorType.RATE_LIMIT,
22
+ 500: FalErrorType.API_ERROR,
23
+ 502: FalErrorType.API_ERROR,
24
+ 503: FalErrorType.API_ERROR,
25
+ 504: FalErrorType.API_ERROR,
26
+ };
27
+
28
+ const RETRYABLE_TYPES = new Set<FalErrorType>([
29
+ FalErrorType.NETWORK,
30
+ FalErrorType.TIMEOUT,
31
+ FalErrorType.RATE_LIMIT,
32
+ ]);
33
+
34
+ /**
35
+ * Message-based error type detection (for non-ApiError errors)
36
+ */
37
+ const MESSAGE_PATTERNS: Array<{ type: FalErrorType; patterns: string[] }> = [
38
+ {
39
+ type: FalErrorType.NETWORK,
40
+ patterns: ["network", "fetch", "econnrefused", "enotfound", "etimedout"],
41
+ },
42
+ { type: FalErrorType.TIMEOUT, patterns: ["timeout", "timed out"] },
43
+ {
44
+ type: FalErrorType.CONTENT_POLICY,
45
+ patterns: ["nsfw", "content_policy", "content policy", "policy violation"],
46
+ },
47
+ {
48
+ type: FalErrorType.IMAGE_TOO_SMALL,
49
+ patterns: [
50
+ "image_too_small",
51
+ "image dimensions are too small",
52
+ "minimum dimensions",
53
+ ],
54
+ },
55
+ ];
56
+
57
+ /**
58
+ * Domain service for error classification
59
+ * Single responsibility: categorize errors and extract user-friendly messages
60
+ */
61
+ export class ErrorClassificationService {
62
+ /**
63
+ * Categorize error using @fal-ai/client error types
64
+ * Priority: ApiError status code > message pattern matching
65
+ *
66
+ * @param error - Error to categorize
67
+ * @returns Error category with type, message key, and retryable flag
68
+ */
69
+ static categorizeError(error: unknown): FalErrorCategory {
70
+ // 1. ApiError (includes ValidationError) - use HTTP status code
71
+ if (error instanceof ApiError) {
72
+ const typeFromStatus = STATUS_TO_ERROR_TYPE[error.status];
73
+ if (typeFromStatus) {
74
+ return {
75
+ type: typeFromStatus,
76
+ messageKey: typeFromStatus,
77
+ retryable: RETRYABLE_TYPES.has(typeFromStatus),
78
+ };
79
+ }
80
+ // Unknown status code - still an API error
81
+ return {
82
+ type: FalErrorType.API_ERROR,
83
+ messageKey: "api_error",
84
+ retryable: false,
85
+ };
86
+ }
87
+
88
+ // 2. Standard Error - match message patterns
89
+ const message = (
90
+ error instanceof Error
91
+ ? error.message
92
+ : error != null
93
+ ? String(error)
94
+ : "unknown"
95
+ ).toLowerCase();
96
+
97
+ for (const { type, patterns } of MESSAGE_PATTERNS) {
98
+ if (patterns.some((p) => message.includes(p))) {
99
+ return {
100
+ type,
101
+ messageKey: type,
102
+ retryable: RETRYABLE_TYPES.has(type),
103
+ };
104
+ }
105
+ }
106
+
107
+ // 3. No match - UNKNOWN, not retryable
108
+ return {
109
+ type: FalErrorType.UNKNOWN,
110
+ messageKey: "unknown",
111
+ retryable: false,
112
+ };
113
+ }
114
+
115
+ /**
116
+ * Extract user-readable message from error
117
+ * Uses @fal-ai/client types for structured extraction
118
+ *
119
+ * @param error - Error to extract message from
120
+ * @returns User-friendly error message
121
+ */
122
+ static extractMessage(error: unknown): string {
123
+ // ValidationError - extract field-level messages
124
+ if (error instanceof ValidationError) {
125
+ const fieldErrors = error.fieldErrors;
126
+ if (Array.isArray(fieldErrors) && fieldErrors.length > 0) {
127
+ const messages = fieldErrors
128
+ .map(
129
+ (e) =>
130
+ e && typeof e === "object" && "msg" in e && typeof e.msg === "string"
131
+ ? e.msg
132
+ : null
133
+ )
134
+ .filter((msg): msg is string => msg !== null);
135
+ if (messages.length > 0) return messages.join("; ");
136
+ }
137
+ return error.message;
138
+ }
139
+
140
+ // ApiError - extract from body or message
141
+ if (error instanceof ApiError) {
142
+ const body = error.body as { detail?: Array<{ msg?: string }> } | undefined;
143
+
144
+ if (
145
+ body &&
146
+ body.detail &&
147
+ Array.isArray(body.detail) &&
148
+ body.detail.length > 0
149
+ ) {
150
+ const firstItem = body.detail[0];
151
+ if (
152
+ firstItem &&
153
+ typeof firstItem === "object" &&
154
+ typeof firstItem.msg === "string" &&
155
+ firstItem.msg
156
+ ) {
157
+ return firstItem.msg;
158
+ }
159
+ }
160
+ return error.message;
161
+ }
162
+
163
+ // Standard Error
164
+ if (error instanceof Error) {
165
+ return error.message;
166
+ }
167
+
168
+ return error != null ? String(error) : "Unknown error";
169
+ }
170
+
171
+ /**
172
+ * Map error to FalErrorInfo with full categorization
173
+ *
174
+ * @param error - Error to map
175
+ * @returns Complete error information
176
+ */
177
+ static mapError(error: unknown): FalErrorInfo {
178
+ const category = this.categorizeError(error);
179
+ const message = this.extractMessage(error);
180
+
181
+ return {
182
+ type: category.type,
183
+ messageKey: `fal.errors.${category.messageKey}`,
184
+ retryable: category.retryable,
185
+ originalError: message,
186
+ originalErrorName: error instanceof Error ? error.name : undefined,
187
+ stack: error instanceof Error ? error.stack : undefined,
188
+ statusCode: error instanceof ApiError ? error.status : undefined,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Check if error is retryable
194
+ *
195
+ * @param error - Error to check
196
+ * @returns True if error is retryable
197
+ */
198
+ static isRetryable(error: unknown): boolean {
199
+ return this.categorizeError(error).retryable;
200
+ }
201
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Domain Service: Image Processing
3
+ * Pure business logic for image data URI manipulation
4
+ * No infrastructure dependencies
5
+ */
6
+
7
+ /**
8
+ * Domain service for image data processing
9
+ * Single responsibility: manipulate and validate image data URIs
10
+ */
11
+ export class ImageProcessingService {
12
+ /**
13
+ * Format image as data URI if not already formatted
14
+ *
15
+ * @param base64 - Base64 string (with or without data URI prefix)
16
+ * @returns Data URI formatted string
17
+ */
18
+ static formatImageDataUri(base64: string): string {
19
+ if (base64.startsWith("data:")) {
20
+ return base64;
21
+ }
22
+ return `data:image/jpeg;base64,${base64}`;
23
+ }
24
+
25
+ /**
26
+ * Extract base64 from data URI
27
+ * Uses indexOf instead of split to handle edge cases where comma might appear in base64
28
+ *
29
+ * @param dataUri - Data URI string
30
+ * @returns Base64 content
31
+ * @throws Error if data URI format is invalid
32
+ */
33
+ static extractBase64(dataUri: string): string {
34
+ if (!dataUri.startsWith("data:")) {
35
+ return dataUri;
36
+ }
37
+
38
+ // Find the first comma which separates header from data
39
+ const commaIndex = dataUri.indexOf(",");
40
+ if (commaIndex === -1) {
41
+ throw new Error(
42
+ `Invalid data URI format (no comma separator): ${dataUri.substring(0, 50)}...`
43
+ );
44
+ }
45
+
46
+ // Extract everything after the first comma
47
+ const base64Part = dataUri.substring(commaIndex + 1);
48
+
49
+ if (!base64Part || base64Part.length === 0) {
50
+ throw new Error(
51
+ `Empty base64 data in URI: ${dataUri.substring(0, 50)}...`
52
+ );
53
+ }
54
+
55
+ return base64Part;
56
+ }
57
+
58
+ /**
59
+ * Get file extension from data URI
60
+ *
61
+ * @param dataUri - Data URI string
62
+ * @returns File extension (e.g., "png", "jpeg") or null
63
+ */
64
+ static getDataUriExtension(dataUri: string): string | null {
65
+ const match = dataUri.match(/^data:image\/(\w+);base64/);
66
+ return match ? match[1] : null;
67
+ }
68
+
69
+ /**
70
+ * Extract MIME type from data URI
71
+ *
72
+ * @param dataUri - Data URI string
73
+ * @returns MIME type (e.g., "image/png") or null
74
+ */
75
+ static extractMimeType(dataUri: string): string | null {
76
+ const match = dataUri.match(/^data:([^;,]+)/);
77
+ return match ? match[1] : null;
78
+ }
79
+
80
+ /**
81
+ * Check if data URI is base64-encoded
82
+ *
83
+ * @param dataUri - Data URI string
84
+ * @returns True if base64-encoded
85
+ */
86
+ static isBase64DataUri(dataUri: string): boolean {
87
+ return dataUri.startsWith("data:") && dataUri.includes("base64,");
88
+ }
89
+ }
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Domain Service: Pricing
3
+ * Pure business logic for credit calculations
4
+ * No infrastructure dependencies
5
+ */
6
+
7
+ import type { VideoModelConfig } from "@umituz/react-native-ai-generation-content";
8
+
9
+ /**
10
+ * Pricing constants
11
+ * FAL AI pricing with markup and credit conversion
12
+ */
13
+ const COSTS = {
14
+ VIDEO_480P_PER_SECOND: 0.05,
15
+ VIDEO_720P_PER_SECOND: 0.07,
16
+ IMAGE_INPUT: 0.002,
17
+ IMAGE: 0.03,
18
+ } as const;
19
+
20
+ const MARKUP = 3.5;
21
+ const CREDIT_PRICE = 0.1;
22
+
23
+ export type GenerationResolution = "480p" | "720p";
24
+
25
+ /**
26
+ * Domain service for pricing calculations
27
+ * Single responsibility: calculate credit costs for generations
28
+ */
29
+ export class PricingService {
30
+ /**
31
+ * Calculate credits for video generation
32
+ *
33
+ * @param duration - Video duration in seconds
34
+ * @param resolution - Video resolution (480p or 720p)
35
+ * @param hasImageInput - Whether an image input is provided
36
+ * @returns Number of credits required
37
+ */
38
+ static calculateVideoCredits(
39
+ duration: number,
40
+ resolution: GenerationResolution,
41
+ hasImageInput: boolean = false
42
+ ): number {
43
+ const costPerSec =
44
+ resolution === "480p"
45
+ ? COSTS.VIDEO_480P_PER_SECOND
46
+ : COSTS.VIDEO_720P_PER_SECOND;
47
+ let cost = costPerSec * duration;
48
+ if (hasImageInput) cost += COSTS.IMAGE_INPUT;
49
+ return Math.max(1, Math.ceil((cost * MARKUP) / CREDIT_PRICE));
50
+ }
51
+
52
+ /**
53
+ * Calculate credits for image generation
54
+ *
55
+ * @returns Number of credits required
56
+ */
57
+ static calculateImageCredits(): number {
58
+ return Math.max(1, Math.ceil((COSTS.IMAGE * MARKUP) / CREDIT_PRICE));
59
+ }
60
+
61
+ /**
62
+ * Calculate credits from video model config
63
+ *
64
+ * @param config - Video model configuration with pricing info
65
+ * @param duration - Video duration in seconds
66
+ * @param resolution - Video resolution string
67
+ * @returns Number of credits required
68
+ * @throws Error if config structure is invalid
69
+ */
70
+ static calculateCreditsFromConfig(
71
+ config: VideoModelConfig,
72
+ duration: number,
73
+ resolution: string
74
+ ): number {
75
+ // Validate config structure
76
+ if (
77
+ !config ||
78
+ typeof config !== "object" ||
79
+ !config.pricing ||
80
+ typeof config.pricing !== "object" ||
81
+ !config.pricing.costPerSecond ||
82
+ typeof config.pricing.costPerSecond !== "object"
83
+ ) {
84
+ throw new Error(
85
+ "Invalid VideoModelConfig: pricing structure is missing or invalid"
86
+ );
87
+ }
88
+
89
+ const costPerSecondMap = config.pricing.costPerSecond;
90
+ const costPerSec = costPerSecondMap[resolution] ?? 0;
91
+
92
+ if (typeof costPerSec !== "number" || costPerSec < 0) {
93
+ throw new Error(
94
+ `Invalid cost per second for resolution "${resolution}": must be a non-negative number`
95
+ );
96
+ }
97
+
98
+ const cost = costPerSec * duration;
99
+ return Math.max(1, Math.ceil((cost * MARKUP) / CREDIT_PRICE));
100
+ }
101
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Domain Service: Validation
3
+ * Pure business logic for input validation
4
+ * No infrastructure dependencies
5
+ */
6
+
7
+ import { IMAGE_URL_FIELDS } from "../../infrastructure/utils/constants/image-fields.constants";
8
+ import {
9
+ isValidPrompt,
10
+ isNonEmptyString,
11
+ isValidAndSafeUrl,
12
+ isImageDataUri,
13
+ } from "../../shared/validators";
14
+
15
+ const SUSPICIOUS_PATTERNS = [
16
+ /<script/i,
17
+ /javascript:/i,
18
+ /on\w+\s*=/i,
19
+ /<iframe/i,
20
+ /<embed/i,
21
+ /<object/i,
22
+ /data:(?!image\/)/i,
23
+ /vbscript:/i,
24
+ ] as const;
25
+
26
+ function hasSuspiciousContent(value: string): boolean {
27
+ return SUSPICIOUS_PATTERNS.some((pattern) => pattern.test(value));
28
+ }
29
+
30
+ export interface ValidationError {
31
+ field: string;
32
+ message: string;
33
+ }
34
+
35
+ export class InputValidationError extends Error {
36
+ public readonly errors: readonly ValidationError[];
37
+
38
+ constructor(errors: ValidationError[]) {
39
+ const message = errors.map((e) => `${e.field}: ${e.message}`).join("; ");
40
+ super(`Input validation failed: ${message}`);
41
+ this.name = "InputValidationError";
42
+ this.errors = errors;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Domain service for input validation
48
+ * Single responsibility: validate all inputs before API calls
49
+ */
50
+ export class ValidationService {
51
+ /**
52
+ * Validate model and input parameters
53
+ *
54
+ * @param model - Model ID (unused in validation but kept for API compatibility)
55
+ * @param input - Input parameters to validate
56
+ * @throws InputValidationError if validation fails
57
+ */
58
+ static validateInput(_model: string, input: Record<string, unknown>): void {
59
+ const errors: ValidationError[] = [];
60
+
61
+ // Validate input is not empty
62
+ if (!input || typeof input !== "object" || Object.keys(input).length === 0) {
63
+ errors.push({ field: "input", message: "Input must be a non-empty object" });
64
+ }
65
+
66
+ // BLOCK sync_mode:true
67
+ if (input.sync_mode === true) {
68
+ errors.push({
69
+ field: "sync_mode",
70
+ message:
71
+ "sync_mode:true is forbidden. It returns base64 data URIs instead of HTTPS CDN URLs.",
72
+ });
73
+ }
74
+
75
+ // Validate prompt
76
+ if (input.prompt !== undefined) {
77
+ if (!isValidPrompt(input.prompt)) {
78
+ errors.push({
79
+ field: "prompt",
80
+ message: "Prompt must be a non-empty string (max 5000 characters)",
81
+ });
82
+ } else if (typeof input.prompt === "string" && hasSuspiciousContent(input.prompt)) {
83
+ errors.push({
84
+ field: "prompt",
85
+ message: "Prompt contains potentially unsafe content",
86
+ });
87
+ }
88
+ }
89
+
90
+ // Validate negative_prompt
91
+ if (input.negative_prompt !== undefined) {
92
+ if (!isValidPrompt(input.negative_prompt)) {
93
+ errors.push({
94
+ field: "negative_prompt",
95
+ message: "Negative prompt must be a non-empty string (max 5000 characters)",
96
+ });
97
+ } else if (typeof input.negative_prompt === "string" && hasSuspiciousContent(input.negative_prompt)) {
98
+ errors.push({
99
+ field: "negative_prompt",
100
+ message: "Negative prompt contains potentially unsafe content",
101
+ });
102
+ }
103
+ }
104
+
105
+ // Validate all image_url fields
106
+ for (const field of IMAGE_URL_FIELDS) {
107
+ const value = input[field];
108
+ if (value !== undefined) {
109
+ if (typeof value !== "string") {
110
+ errors.push({
111
+ field,
112
+ message: `${field} must be a string`,
113
+ });
114
+ } else if (!isNonEmptyString(value)) {
115
+ errors.push({
116
+ field,
117
+ message: `${field} cannot be empty`,
118
+ });
119
+ } else if (!isValidAndSafeUrl(value)) {
120
+ errors.push({
121
+ field,
122
+ message: `${field} must be a valid URL or image data URI`,
123
+ });
124
+ }
125
+ }
126
+ }
127
+
128
+ if (errors.length > 0) {
129
+ throw new InputValidationError(errors);
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Domain Services - Public API
3
+ * Pure business logic services with no infrastructure dependencies
4
+ */
5
+
6
+ export { ValidationService, InputValidationError } from "./ValidationService";
7
+ export type { ValidationError } from "./ValidationService";
8
+
9
+ export { PricingService } from "./PricingService";
10
+
11
+ export { ImageProcessingService } from "./ImageProcessingService";
12
+
13
+ export { ErrorClassificationService } from "./ErrorClassificationService";
package/src/index.ts CHANGED
@@ -43,11 +43,19 @@ export type {
43
43
  } from "./domain/entities/error.types";
44
44
 
45
45
  // ─── Utilities (public API) ────────────────────────────────────────────────────
46
+ // Shared utilities (consolidated)
46
47
  export {
47
48
  getErrorMessage,
48
49
  getErrorMessageOr,
49
50
  formatErrorMessage,
50
- } from "./infrastructure/utils/helpers/error-helpers.util";
51
+ buildErrorMessage,
52
+ isDefined,
53
+ removeNullish,
54
+ generateUniqueId,
55
+ sleep,
56
+ getElapsedTime,
57
+ getActualSizeKB,
58
+ } from "./shared/helpers";
51
59
 
52
60
  export {
53
61
  IMAGE_URL_FIELDS,
@@ -56,29 +64,31 @@ export {
56
64
  export type { ImageUrlField } from "./infrastructure/utils/constants/image-fields.constants";
57
65
 
58
66
  export {
67
+ isEmptyString,
68
+ isNonEmptyString,
69
+ isString,
59
70
  isDataUri,
71
+ isImageDataUri,
60
72
  isBase64DataUri,
61
73
  extractMimeType,
62
74
  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
75
  isValidBase64Image,
76
76
  isValidApiKey,
77
77
  isValidModelId,
78
78
  isValidPrompt,
79
79
  isValidTimeout,
80
80
  isValidRetryCount,
81
- } from "./infrastructure/utils/type-guards";
81
+ isValidAndSafeUrl,
82
+ } from "./shared/validators";
83
+
84
+ export {
85
+ isFalModelType,
86
+ isModelType,
87
+ isFalErrorType,
88
+ isRetryableError,
89
+ isLocalFileUri,
90
+ isHttpUrl,
91
+ } from "./shared/type-guards";
82
92
 
83
93
  export {
84
94
  formatImageDataUri,
@@ -91,14 +101,6 @@ export {
91
101
  uploadMultipleToFalStorage,
92
102
  } from "./infrastructure/utils/fal-storage.util";
93
103
 
94
- export {
95
- buildErrorMessage,
96
- isDefined,
97
- removeNullish,
98
- generateUniqueId,
99
- sleep,
100
- } from "./infrastructure/utils/helpers";
101
-
102
104
  export { preprocessInput } from "./infrastructure/utils/input-preprocessor.util";
103
105
 
104
106
  export {