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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/infrastructure/services/fal-models.service.ts +4 -1
  3. package/src/infrastructure/services/fal-provider-subscription.ts +17 -7
  4. package/src/infrastructure/services/fal-provider.ts +11 -1
  5. package/src/infrastructure/services/fal-queue-operations.ts +10 -0
  6. package/src/infrastructure/services/request-store.ts +53 -12
  7. package/src/infrastructure/utils/collection-filters.util.ts +5 -167
  8. package/src/infrastructure/utils/collections/array-filters.util.ts +52 -0
  9. package/src/infrastructure/utils/collections/array-reducers.util.ts +67 -0
  10. package/src/infrastructure/utils/collections/array-sorters.util.ts +60 -0
  11. package/src/infrastructure/utils/collections/index.ts +8 -0
  12. package/src/infrastructure/utils/cost-tracker.ts +6 -1
  13. package/src/infrastructure/utils/data-parsers.util.ts +5 -187
  14. package/src/infrastructure/utils/error-mapper.ts +10 -0
  15. package/src/infrastructure/utils/fal-error-handler.util.ts +21 -18
  16. package/src/infrastructure/utils/fal-generation-state-manager.util.ts +9 -2
  17. package/src/infrastructure/utils/fal-storage.util.ts +7 -2
  18. package/src/infrastructure/utils/general-helpers.util.ts +5 -146
  19. package/src/infrastructure/utils/helpers/function-helpers.util.ts +25 -0
  20. package/src/infrastructure/utils/helpers/index.ts +8 -0
  21. package/src/infrastructure/utils/helpers/object-helpers.util.ts +44 -0
  22. package/src/infrastructure/utils/helpers/timing-helpers.util.ts +89 -0
  23. package/src/infrastructure/utils/input-preprocessor.util.ts +12 -6
  24. package/src/infrastructure/utils/input-validator.util.ts +34 -3
  25. package/src/infrastructure/utils/parsers/index.ts +10 -0
  26. package/src/infrastructure/utils/parsers/json-parsers.util.ts +55 -0
  27. package/src/infrastructure/utils/parsers/number-helpers.util.ts +19 -0
  28. package/src/infrastructure/utils/parsers/object-transformers.util.ts +67 -0
  29. package/src/infrastructure/utils/parsers/object-validators.util.ts +38 -0
  30. package/src/infrastructure/utils/parsers/value-parsers.util.ts +45 -0
  31. package/src/infrastructure/utils/type-guards/constants.ts +10 -0
  32. package/src/infrastructure/utils/type-guards/index.ts +8 -0
  33. package/src/infrastructure/utils/type-guards/model-type-guards.util.ts +56 -0
  34. package/src/infrastructure/utils/type-guards/validation-guards.util.ts +80 -0
  35. package/src/infrastructure/utils/type-guards.util.ts +5 -115
  36. package/src/presentation/hooks/use-fal-generation.ts +15 -4
  37. package/src/presentation/hooks/use-models.ts +26 -9
@@ -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 > 0) {
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,8 @@
1
+ /**
2
+ * Type Guards - Centralized Exports
3
+ * Re-exports all type guard utilities from submodules
4
+ */
5
+
6
+ export * from './constants';
7
+ export * from './model-type-guards.util';
8
+ export * from './validation-guards.util';
@@ -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
- * Runtime type checking and validation helpers
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
- 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
- }
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
- stateManagerRef.current = new FalGenerationStateManager<T>({
63
+ const stateManager = new FalGenerationStateManager<T>({
64
64
  onProgress: (status) => {
65
65
  optionsRef.current?.onProgress?.(status);
66
66
  },
67
67
  });
68
68
 
69
- stateManagerRef.current.setIsMounted(true);
69
+ stateManager.setIsMounted(true);
70
+ stateManagerRef.current = stateManager;
70
71
 
71
72
  return () => {
72
- stateManagerRef.current?.setIsMounted(false);
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
- falProvider.cancelCurrentRequest();
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
- const loadModels = useCallback(() => {
38
+ // Direct effect - no intermediate callback needed
39
+ useEffect(() => {
39
40
  setIsLoading(true);
40
41
  setError(null);
41
42
 
42
- const selectionData = falModelsService.getModelSelectionData(type, config);
43
- setModels(selectionData.models);
44
- setSelectedModel(selectionData.selectedModel);
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
- setIsLoading(false);
47
- }, [type, config]);
54
+ // Separate refresh callback for manual reloads
55
+ const loadModels = useCallback(() => {
56
+ setIsLoading(true);
57
+ setError(null);
48
58
 
49
- useEffect(() => {
50
- loadModels();
51
- }, [loadModels]);
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) => {