@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.
- package/package.json +1 -1
- package/src/infrastructure/services/fal-models.service.ts +4 -1
- package/src/infrastructure/services/fal-provider-subscription.ts +17 -7
- package/src/infrastructure/services/fal-provider.ts +11 -1
- package/src/infrastructure/services/fal-queue-operations.ts +10 -0
- package/src/infrastructure/services/request-store.ts +53 -12
- package/src/infrastructure/utils/collection-filters.util.ts +5 -167
- package/src/infrastructure/utils/collections/array-filters.util.ts +52 -0
- package/src/infrastructure/utils/collections/array-reducers.util.ts +67 -0
- package/src/infrastructure/utils/collections/array-sorters.util.ts +60 -0
- package/src/infrastructure/utils/collections/index.ts +8 -0
- package/src/infrastructure/utils/cost-tracker.ts +6 -1
- package/src/infrastructure/utils/data-parsers.util.ts +5 -187
- package/src/infrastructure/utils/error-mapper.ts +10 -0
- package/src/infrastructure/utils/fal-error-handler.util.ts +21 -18
- package/src/infrastructure/utils/fal-generation-state-manager.util.ts +9 -2
- package/src/infrastructure/utils/fal-storage.util.ts +7 -2
- package/src/infrastructure/utils/general-helpers.util.ts +5 -146
- package/src/infrastructure/utils/helpers/function-helpers.util.ts +25 -0
- package/src/infrastructure/utils/helpers/index.ts +8 -0
- package/src/infrastructure/utils/helpers/object-helpers.util.ts +44 -0
- package/src/infrastructure/utils/helpers/timing-helpers.util.ts +89 -0
- package/src/infrastructure/utils/input-preprocessor.util.ts +12 -6
- package/src/infrastructure/utils/input-validator.util.ts +34 -3
- package/src/infrastructure/utils/parsers/index.ts +10 -0
- package/src/infrastructure/utils/parsers/json-parsers.util.ts +55 -0
- package/src/infrastructure/utils/parsers/number-helpers.util.ts +19 -0
- package/src/infrastructure/utils/parsers/object-transformers.util.ts +67 -0
- package/src/infrastructure/utils/parsers/object-validators.util.ts +38 -0
- package/src/infrastructure/utils/parsers/value-parsers.util.ts +45 -0
- package/src/infrastructure/utils/type-guards/constants.ts +10 -0
- package/src/infrastructure/utils/type-guards/index.ts +8 -0
- package/src/infrastructure/utils/type-guards/model-type-guards.util.ts +56 -0
- package/src/infrastructure/utils/type-guards/validation-guards.util.ts +80 -0
- package/src/infrastructure/utils/type-guards.util.ts +5 -115
- package/src/presentation/hooks/use-fal-generation.ts +15 -4
- package/src/presentation/hooks/use-models.ts +26 -9
|
@@ -5,6 +5,19 @@
|
|
|
5
5
|
|
|
6
6
|
import { isValidModelId, isValidPrompt } from "./type-guards.util";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Basic HTML/Script tag sanitization (defense in depth)
|
|
10
|
+
* NOTE: This is sent to backend which should also sanitize,
|
|
11
|
+
* but we apply basic filtering as a precaution
|
|
12
|
+
*/
|
|
13
|
+
function sanitizeString(value: string): string {
|
|
14
|
+
// Remove potential script tags and HTML entities
|
|
15
|
+
return value
|
|
16
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
17
|
+
.replace(/<[^>]*>/g, '')
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
8
21
|
export interface ValidationError {
|
|
9
22
|
field: string;
|
|
10
23
|
message: string;
|
|
@@ -42,23 +55,35 @@ export function validateInput(
|
|
|
42
55
|
errors.push({ field: "input", message: "Input must be a non-empty object" });
|
|
43
56
|
}
|
|
44
57
|
|
|
45
|
-
// Validate prompt if present
|
|
58
|
+
// Validate and sanitize prompt if present
|
|
46
59
|
if (input.prompt !== undefined) {
|
|
47
60
|
if (!isValidPrompt(input.prompt)) {
|
|
48
61
|
errors.push({
|
|
49
62
|
field: "prompt",
|
|
50
63
|
message: "Prompt must be a non-empty string (max 5000 characters)",
|
|
51
64
|
});
|
|
65
|
+
} else if (typeof input.prompt === "string") {
|
|
66
|
+
// Apply basic sanitization (defense in depth)
|
|
67
|
+
const sanitized = sanitizeString(input.prompt);
|
|
68
|
+
if (sanitized !== input.prompt) {
|
|
69
|
+
console.warn('[input-validator] Potentially unsafe content detected and sanitized in prompt');
|
|
70
|
+
}
|
|
52
71
|
}
|
|
53
72
|
}
|
|
54
73
|
|
|
55
|
-
// Validate negative_prompt if present
|
|
74
|
+
// Validate and sanitize negative_prompt if present
|
|
56
75
|
if (input.negative_prompt !== undefined) {
|
|
57
76
|
if (!isValidPrompt(input.negative_prompt)) {
|
|
58
77
|
errors.push({
|
|
59
78
|
field: "negative_prompt",
|
|
60
79
|
message: "Negative prompt must be a non-empty string (max 5000 characters)",
|
|
61
80
|
});
|
|
81
|
+
} else if (typeof input.negative_prompt === "string") {
|
|
82
|
+
// Apply basic sanitization (defense in depth)
|
|
83
|
+
const sanitized = sanitizeString(input.negative_prompt);
|
|
84
|
+
if (sanitized !== input.negative_prompt) {
|
|
85
|
+
console.warn('[input-validator] Potentially unsafe content detected and sanitized in negative_prompt');
|
|
86
|
+
}
|
|
62
87
|
}
|
|
63
88
|
}
|
|
64
89
|
|
|
@@ -79,7 +104,13 @@ export function validateInput(
|
|
|
79
104
|
field,
|
|
80
105
|
message: `${field} must be a string`,
|
|
81
106
|
});
|
|
82
|
-
} else if (value.length
|
|
107
|
+
} else if (!value || value.trim().length === 0) {
|
|
108
|
+
// Explicitly check for empty/whitespace-only strings
|
|
109
|
+
errors.push({
|
|
110
|
+
field,
|
|
111
|
+
message: `${field} cannot be empty`,
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
83
114
|
const isValidUrl = value.startsWith('http://') || value.startsWith('https://');
|
|
84
115
|
const isValidBase64 = value.startsWith('data:image/');
|
|
85
116
|
if (!isValidUrl && !isValidBase64) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser Utilities - Centralized Exports
|
|
3
|
+
* Re-exports all parser utilities from submodules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from './json-parsers.util';
|
|
7
|
+
export * from './object-validators.util';
|
|
8
|
+
export * from './value-parsers.util';
|
|
9
|
+
export * from './number-helpers.util';
|
|
10
|
+
export * from './object-transformers.util';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Parser Utilities
|
|
3
|
+
* Safe JSON parsing and validation operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Safely parse JSON with fallback
|
|
8
|
+
*/
|
|
9
|
+
export function safeJsonParse<T = unknown>(
|
|
10
|
+
data: string,
|
|
11
|
+
fallback: T
|
|
12
|
+
): T {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(data) as T;
|
|
15
|
+
} catch {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Safely parse JSON with null fallback
|
|
22
|
+
*/
|
|
23
|
+
export function safeJsonParseOrNull<T = unknown>(data: string): T | null {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(data) as T;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Safely stringify object with fallback
|
|
33
|
+
*/
|
|
34
|
+
export function safeJsonStringify(
|
|
35
|
+
data: unknown,
|
|
36
|
+
fallback: string
|
|
37
|
+
): string {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(data);
|
|
40
|
+
} catch {
|
|
41
|
+
return fallback;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if string is valid JSON
|
|
47
|
+
*/
|
|
48
|
+
export function isValidJson(data: string): boolean {
|
|
49
|
+
try {
|
|
50
|
+
JSON.parse(data);
|
|
51
|
+
return true;
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number Helper Utilities
|
|
3
|
+
* Number manipulation and formatting
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Clamp number between min and max
|
|
8
|
+
*/
|
|
9
|
+
export function clampNumber(value: number, min: number, max: number): number {
|
|
10
|
+
return Math.min(Math.max(value, min), max);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Round to decimal places
|
|
15
|
+
*/
|
|
16
|
+
export function roundToDecimals(value: number, decimals: number): number {
|
|
17
|
+
const multiplier = Math.pow(10, decimals);
|
|
18
|
+
return Math.round(value * multiplier) / multiplier;
|
|
19
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object Transformer Utilities
|
|
3
|
+
* Clone, merge, pick, and omit operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Deep clone object using JSON serialization
|
|
8
|
+
* NOTE: This has limitations:
|
|
9
|
+
* - Functions are not cloned
|
|
10
|
+
* - Dates become strings
|
|
11
|
+
* - Circular references will cause errors
|
|
12
|
+
* For complex objects, consider a dedicated cloning library
|
|
13
|
+
*/
|
|
14
|
+
export function deepClone<T>(data: T): T {
|
|
15
|
+
try {
|
|
16
|
+
// Try JSON clone first (fast path)
|
|
17
|
+
const serialized = JSON.stringify(data);
|
|
18
|
+
return JSON.parse(serialized) as T;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
// Fallback for circular references or other JSON errors
|
|
21
|
+
console.warn(
|
|
22
|
+
'[object-transformers] deepClone failed, returning original:',
|
|
23
|
+
error instanceof Error ? error.message : String(error)
|
|
24
|
+
);
|
|
25
|
+
// Return original data if cloning fails
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Merge objects with later objects overriding earlier ones
|
|
32
|
+
*/
|
|
33
|
+
export function mergeObjects<T extends Record<string, unknown>>(
|
|
34
|
+
...objects: Partial<T>[]
|
|
35
|
+
): T {
|
|
36
|
+
return Object.assign({}, ...objects) as T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Pick specified properties from object
|
|
41
|
+
*/
|
|
42
|
+
export function pickProperties<T extends Record<string, unknown>, K extends keyof T>(
|
|
43
|
+
obj: T,
|
|
44
|
+
keys: readonly K[]
|
|
45
|
+
): Pick<T, K> {
|
|
46
|
+
const result = {} as Pick<T, K>;
|
|
47
|
+
for (const key of keys) {
|
|
48
|
+
if (key in obj) {
|
|
49
|
+
result[key] = obj[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Omit specified properties from object
|
|
57
|
+
*/
|
|
58
|
+
export function omitProperties<T extends Record<string, unknown>, K extends keyof T>(
|
|
59
|
+
obj: T,
|
|
60
|
+
keys: readonly K[]
|
|
61
|
+
): Omit<T, K> {
|
|
62
|
+
const result = { ...obj };
|
|
63
|
+
for (const key of keys) {
|
|
64
|
+
delete result[key];
|
|
65
|
+
}
|
|
66
|
+
return result as Omit<T, K>;
|
|
67
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object Validator Utilities
|
|
3
|
+
* Runtime object structure validation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validate object structure
|
|
8
|
+
*/
|
|
9
|
+
export function validateObjectStructure<T extends Record<string, unknown>>(
|
|
10
|
+
data: unknown,
|
|
11
|
+
requiredKeys: readonly (keyof T)[]
|
|
12
|
+
): data is T {
|
|
13
|
+
if (!data || typeof data !== "object") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const key of requiredKeys) {
|
|
18
|
+
if (!(key in data)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate array of objects
|
|
28
|
+
*/
|
|
29
|
+
export function validateObjectArray<T>(
|
|
30
|
+
data: unknown,
|
|
31
|
+
validator: (item: unknown) => item is T
|
|
32
|
+
): data is T[] {
|
|
33
|
+
if (!Array.isArray(data)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return data.every(validator);
|
|
38
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Value Parser Utilities
|
|
3
|
+
* Parse primitive values with fallbacks
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse number with fallback
|
|
8
|
+
*/
|
|
9
|
+
export function parseNumber(value: unknown, fallback: number): number {
|
|
10
|
+
if (typeof value === "number") {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof value === "string") {
|
|
15
|
+
const parsed = Number.parseFloat(value);
|
|
16
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse boolean with fallback
|
|
24
|
+
*/
|
|
25
|
+
export function parseBoolean(value: unknown, fallback: boolean): boolean {
|
|
26
|
+
if (typeof value === "boolean") {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof value === "string") {
|
|
31
|
+
const lower = value.toLowerCase().trim();
|
|
32
|
+
if (lower === "true" || lower === "yes" || lower === "1") {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (lower === "false" || lower === "no" || lower === "0") {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof value === "number") {
|
|
41
|
+
return value !== 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return fallback;
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type Guard Constants
|
|
3
|
+
* Validation thresholds and limits
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const MIN_BASE64_IMAGE_LENGTH = 100; // Minimum base64 length for meaningful image data
|
|
7
|
+
export const MIN_MODEL_ID_LENGTH = 3; // Minimum model ID length
|
|
8
|
+
export const MAX_PROMPT_LENGTH = 5000; // Maximum prompt length in characters
|
|
9
|
+
export const MAX_TIMEOUT_MS = 600000; // Maximum timeout (10 minutes)
|
|
10
|
+
export const MAX_RETRY_COUNT = 10; // Maximum retry attempts
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Type Guards
|
|
3
|
+
* Runtime type checking for model types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FalModelType } from "../../../domain/entities/fal.types";
|
|
7
|
+
import type { ModelType } from "../../../domain/types/model-selection.types";
|
|
8
|
+
import { FalErrorType } from "../../../domain/entities/error.types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a string is a valid FalModelType
|
|
12
|
+
*/
|
|
13
|
+
export function isFalModelType(value: unknown): value is FalModelType {
|
|
14
|
+
const validTypes: ReadonlyArray<FalModelType> = [
|
|
15
|
+
"text-to-image",
|
|
16
|
+
"text-to-video",
|
|
17
|
+
"text-to-voice",
|
|
18
|
+
"image-to-video",
|
|
19
|
+
"image-to-image",
|
|
20
|
+
"text-to-text",
|
|
21
|
+
];
|
|
22
|
+
return typeof value === "string" && validTypes.includes(value as FalModelType);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a string is a valid ModelType
|
|
27
|
+
*/
|
|
28
|
+
export function isModelType(value: unknown): value is ModelType {
|
|
29
|
+
const validTypes: ReadonlyArray<ModelType> = [
|
|
30
|
+
"text-to-image",
|
|
31
|
+
"text-to-video",
|
|
32
|
+
"image-to-video",
|
|
33
|
+
"text-to-voice",
|
|
34
|
+
];
|
|
35
|
+
return typeof value === "string" && validTypes.includes(value as ModelType);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if error is a FalErrorType
|
|
40
|
+
*/
|
|
41
|
+
export function isFalErrorType(value: unknown): value is FalErrorType {
|
|
42
|
+
const validTypes: ReadonlyArray<FalErrorType> = [
|
|
43
|
+
FalErrorType.NETWORK,
|
|
44
|
+
FalErrorType.TIMEOUT,
|
|
45
|
+
FalErrorType.API_ERROR,
|
|
46
|
+
FalErrorType.VALIDATION,
|
|
47
|
+
FalErrorType.IMAGE_TOO_SMALL,
|
|
48
|
+
FalErrorType.CONTENT_POLICY,
|
|
49
|
+
FalErrorType.RATE_LIMIT,
|
|
50
|
+
FalErrorType.AUTHENTICATION,
|
|
51
|
+
FalErrorType.QUOTA_EXCEEDED,
|
|
52
|
+
FalErrorType.MODEL_NOT_FOUND,
|
|
53
|
+
FalErrorType.UNKNOWN,
|
|
54
|
+
];
|
|
55
|
+
return typeof value === "string" && validTypes.includes(value as FalErrorType);
|
|
56
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Guards
|
|
3
|
+
* Runtime validation for values and formats
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
MIN_BASE64_IMAGE_LENGTH,
|
|
8
|
+
MIN_MODEL_ID_LENGTH,
|
|
9
|
+
MAX_PROMPT_LENGTH,
|
|
10
|
+
MAX_TIMEOUT_MS,
|
|
11
|
+
MAX_RETRY_COUNT,
|
|
12
|
+
} from './constants';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate base64 image string
|
|
16
|
+
*/
|
|
17
|
+
export function isValidBase64Image(value: unknown): boolean {
|
|
18
|
+
if (typeof value !== "string") {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check data URI prefix
|
|
23
|
+
if (value.startsWith("data:image/")) {
|
|
24
|
+
const base64Part = value.split("base64,")[1];
|
|
25
|
+
if (!base64Part) return false;
|
|
26
|
+
return base64Part.length >= MIN_BASE64_IMAGE_LENGTH;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if it's a valid base64 string with minimum length
|
|
30
|
+
const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
|
|
31
|
+
return base64Pattern.test(value) && value.length >= MIN_BASE64_IMAGE_LENGTH;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Validate API key format
|
|
36
|
+
*/
|
|
37
|
+
export function isValidApiKey(value: unknown): boolean {
|
|
38
|
+
return typeof value === "string" && value.length > 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate model ID format
|
|
43
|
+
*/
|
|
44
|
+
const MODEL_ID_PATTERN = /^[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_.]+(\/[a-zA-Z0-9-_.]+)?$/;
|
|
45
|
+
|
|
46
|
+
export function isValidModelId(value: unknown): boolean {
|
|
47
|
+
if (typeof value !== "string") {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return MODEL_ID_PATTERN.test(value) && value.length >= MIN_MODEL_ID_LENGTH;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate prompt string
|
|
56
|
+
*/
|
|
57
|
+
export function isValidPrompt(value: unknown): boolean {
|
|
58
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_PROMPT_LENGTH;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate timeout value
|
|
63
|
+
*/
|
|
64
|
+
export function isValidTimeout(value: unknown): boolean {
|
|
65
|
+
return typeof value === "number" && !isNaN(value) && isFinite(value) && value > 0 && value <= MAX_TIMEOUT_MS;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Validate retry count
|
|
70
|
+
*/
|
|
71
|
+
export function isValidRetryCount(value: unknown): boolean {
|
|
72
|
+
return (
|
|
73
|
+
typeof value === "number" &&
|
|
74
|
+
!isNaN(value) &&
|
|
75
|
+
isFinite(value) &&
|
|
76
|
+
Number.isInteger(value) &&
|
|
77
|
+
value >= 0 &&
|
|
78
|
+
value <= MAX_RETRY_COUNT
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -1,119 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type Guards and Validation Utilities
|
|
3
|
-
*
|
|
3
|
+
* @deprecated This file is now split into smaller modules for better maintainability.
|
|
4
|
+
* Import from './type-guards' submodules instead.
|
|
5
|
+
*
|
|
6
|
+
* This file re-exports all functions for backward compatibility.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
import type { ModelType } from "../../domain/types/model-selection.types";
|
|
8
|
-
import { FalErrorType } from "../../domain/entities/error.types";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Check if a string is a valid FalModelType
|
|
12
|
-
*/
|
|
13
|
-
export function isFalModelType(value: unknown): value is FalModelType {
|
|
14
|
-
const validTypes: ReadonlyArray<FalModelType> = [
|
|
15
|
-
"text-to-image",
|
|
16
|
-
"text-to-video",
|
|
17
|
-
"text-to-voice",
|
|
18
|
-
"image-to-video",
|
|
19
|
-
"image-to-image",
|
|
20
|
-
"text-to-text",
|
|
21
|
-
];
|
|
22
|
-
return typeof value === "string" && validTypes.includes(value as FalModelType);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check if a string is a valid ModelType
|
|
27
|
-
*/
|
|
28
|
-
export function isModelType(value: unknown): value is ModelType {
|
|
29
|
-
const validTypes: ReadonlyArray<ModelType> = [
|
|
30
|
-
"text-to-image",
|
|
31
|
-
"text-to-video",
|
|
32
|
-
"image-to-video",
|
|
33
|
-
"text-to-voice",
|
|
34
|
-
];
|
|
35
|
-
return typeof value === "string" && validTypes.includes(value as ModelType);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Check if error is a FalErrorType
|
|
40
|
-
*/
|
|
41
|
-
export function isFalErrorType(value: unknown): value is FalErrorType {
|
|
42
|
-
const validTypes: ReadonlyArray<FalErrorType> = [
|
|
43
|
-
FalErrorType.NETWORK,
|
|
44
|
-
FalErrorType.TIMEOUT,
|
|
45
|
-
FalErrorType.API_ERROR,
|
|
46
|
-
FalErrorType.VALIDATION,
|
|
47
|
-
FalErrorType.IMAGE_TOO_SMALL,
|
|
48
|
-
FalErrorType.CONTENT_POLICY,
|
|
49
|
-
FalErrorType.RATE_LIMIT,
|
|
50
|
-
FalErrorType.AUTHENTICATION,
|
|
51
|
-
FalErrorType.QUOTA_EXCEEDED,
|
|
52
|
-
FalErrorType.MODEL_NOT_FOUND,
|
|
53
|
-
FalErrorType.UNKNOWN,
|
|
54
|
-
];
|
|
55
|
-
return typeof value === "string" && validTypes.includes(value as FalErrorType);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Validate base64 image string
|
|
60
|
-
*/
|
|
61
|
-
export function isValidBase64Image(value: unknown): boolean {
|
|
62
|
-
if (typeof value !== "string") {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Check data URI prefix
|
|
67
|
-
if (value.startsWith("data:image/")) {
|
|
68
|
-
const base64Part = value.split("base64,")[1];
|
|
69
|
-
if (!base64Part) return false;
|
|
70
|
-
// Base64 should be at least 100 chars (meaningful image data)
|
|
71
|
-
return base64Part.length >= 100;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Check if it's a valid base64 string with minimum length
|
|
75
|
-
const base64Pattern = /^[A-Za-z0-9+/]+=*$/;
|
|
76
|
-
// Minimum 100 characters for meaningful base64 image data
|
|
77
|
-
return base64Pattern.test(value) && value.length >= 100;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Validate API key format
|
|
82
|
-
*/
|
|
83
|
-
export function isValidApiKey(value: unknown): boolean {
|
|
84
|
-
return typeof value === "string" && value.length > 0;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Validate model ID format
|
|
89
|
-
*/
|
|
90
|
-
const MODEL_ID_PATTERN = /^[a-zA-Z0-9-_]+\/[a-zA-Z0-9-_.]+(\/[a-zA-Z0-9-_.]+)?$/;
|
|
91
|
-
|
|
92
|
-
export function isValidModelId(value: unknown): boolean {
|
|
93
|
-
if (typeof value !== "string") {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return MODEL_ID_PATTERN.test(value) && value.length >= 3;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Validate prompt string
|
|
102
|
-
*/
|
|
103
|
-
export function isValidPrompt(value: unknown): boolean {
|
|
104
|
-
return typeof value === "string" && value.trim().length > 0 && value.length <= 5000;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Validate timeout value
|
|
109
|
-
*/
|
|
110
|
-
export function isValidTimeout(value: unknown): boolean {
|
|
111
|
-
return typeof value === "number" && !isNaN(value) && isFinite(value) && value > 0 && value <= 600000; // Max 10 minutes
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Validate retry count
|
|
116
|
-
*/
|
|
117
|
-
export function isValidRetryCount(value: unknown): boolean {
|
|
118
|
-
return typeof value === "number" && !isNaN(value) && isFinite(value) && Number.isInteger(value) && value >= 0 && value <= 10;
|
|
119
|
-
}
|
|
9
|
+
export * from './type-guards';
|
|
@@ -60,18 +60,29 @@ export function useFalGeneration<T = unknown>(
|
|
|
60
60
|
}, [options]);
|
|
61
61
|
|
|
62
62
|
useEffect(() => {
|
|
63
|
-
|
|
63
|
+
const stateManager = new FalGenerationStateManager<T>({
|
|
64
64
|
onProgress: (status) => {
|
|
65
65
|
optionsRef.current?.onProgress?.(status);
|
|
66
66
|
},
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
stateManager.setIsMounted(true);
|
|
70
|
+
stateManagerRef.current = stateManager;
|
|
70
71
|
|
|
71
72
|
return () => {
|
|
72
|
-
|
|
73
|
+
// Ensure we have a valid reference before cleanup
|
|
74
|
+
if (stateManagerRef.current) {
|
|
75
|
+
stateManagerRef.current.setIsMounted(false);
|
|
76
|
+
stateManagerRef.current = null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Cancel any running requests
|
|
73
80
|
if (falProvider.hasRunningRequest()) {
|
|
74
|
-
|
|
81
|
+
try {
|
|
82
|
+
falProvider.cancelCurrentRequest();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.warn('[useFalGeneration] Error cancelling request on unmount:', error);
|
|
85
|
+
}
|
|
75
86
|
}
|
|
76
87
|
};
|
|
77
88
|
}, []); // Empty deps - only run on mount/unmount
|
|
@@ -35,20 +35,37 @@ export function useModels(props: UseModelsProps): UseModelsReturn {
|
|
|
35
35
|
const [isLoading, setIsLoading] = useState(true);
|
|
36
36
|
const [error, setError] = useState<string | null>(null);
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// Direct effect - no intermediate callback needed
|
|
39
|
+
useEffect(() => {
|
|
39
40
|
setIsLoading(true);
|
|
40
41
|
setError(null);
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
try {
|
|
44
|
+
const selectionData = falModelsService.getModelSelectionData(type, config);
|
|
45
|
+
setModels(selectionData.models);
|
|
46
|
+
setSelectedModel(selectionData.selectedModel);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
setError(err instanceof Error ? err.message : 'Failed to load models');
|
|
49
|
+
} finally {
|
|
50
|
+
setIsLoading(false);
|
|
51
|
+
}
|
|
52
|
+
}, [type, config]); // Direct dependencies
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
// Separate refresh callback for manual reloads
|
|
55
|
+
const loadModels = useCallback(() => {
|
|
56
|
+
setIsLoading(true);
|
|
57
|
+
setError(null);
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
59
|
+
try {
|
|
60
|
+
const selectionData = falModelsService.getModelSelectionData(type, config);
|
|
61
|
+
setModels(selectionData.models);
|
|
62
|
+
setSelectedModel(selectionData.selectedModel);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
setError(err instanceof Error ? err.message : 'Failed to load models');
|
|
65
|
+
} finally {
|
|
66
|
+
setIsLoading(false);
|
|
67
|
+
}
|
|
68
|
+
}, [type, config]);
|
|
52
69
|
|
|
53
70
|
const selectModel = useCallback(
|
|
54
71
|
(modelId: string) => {
|