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

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.
@@ -0,0 +1,73 @@
1
+ /**
2
+ * String Formatting Utilities
3
+ * Functions for formatting and manipulating strings
4
+ */
5
+
6
+ /**
7
+ * Truncate text with ellipsis
8
+ */
9
+ export function truncateText(text: string, maxLength: number): string {
10
+ if (text.length <= maxLength) {
11
+ return text;
12
+ }
13
+ return text.slice(0, maxLength - 3) + "...";
14
+ }
15
+
16
+ /**
17
+ * Capitalize first letter of string
18
+ */
19
+ export function capitalize(text: string): string {
20
+ if (!text) return text;
21
+ return text.charAt(0).toUpperCase() + text.slice(1);
22
+ }
23
+
24
+ /**
25
+ * Convert string to title case
26
+ */
27
+ export function toTitleCase(text: string): string {
28
+ return text
29
+ .toLowerCase()
30
+ .split(" ")
31
+ .map((word) => capitalize(word))
32
+ .join(" ");
33
+ }
34
+
35
+ /**
36
+ * Convert string to slug
37
+ */
38
+ export function toSlug(text: string): string {
39
+ return text
40
+ .toLowerCase()
41
+ .trim()
42
+ .replace(/[^\w\s-]/g, "")
43
+ .replace(/[\s_-]+/g, "-")
44
+ .replace(/^-+|-+$/g, "");
45
+ }
46
+
47
+ /**
48
+ * Format list of items with conjunction
49
+ */
50
+ export function formatList(items: readonly string[], conjunction: string = "and"): string {
51
+ if (items.length === 0) return "";
52
+ if (items.length === 1) return items[0] ?? "";
53
+ if (items.length === 2) return items.join(` ${conjunction} `);
54
+
55
+ const allButLast = items.slice(0, -1);
56
+ const last = items[items.length - 1];
57
+ return `${allButLast.join(", ")}, ${conjunction} ${last}`;
58
+ }
59
+
60
+ /**
61
+ * Pluralize word based on count
62
+ */
63
+ export function pluralize(word: string, count: number): string {
64
+ if (count === 1) return word;
65
+ return `${word}s`;
66
+ }
67
+
68
+ /**
69
+ * Format count with plural word
70
+ */
71
+ export function formatCount(word: string, count: number): string {
72
+ return `${count} ${pluralize(word, count)}`;
73
+ }
@@ -6,8 +6,10 @@
6
6
  import { useState, useCallback, useRef, useEffect } from "react";
7
7
  import { falProvider } from "../../infrastructure/services/fal-provider";
8
8
  import { mapFalError } from "../../infrastructure/utils/error-mapper";
9
- import type { FalJobInput, FalQueueStatus, FalLogEntry } from "../../domain/entities/fal.types";
9
+ import { FalGenerationStateManager } from "../../infrastructure/utils/fal-generation-state-manager.util";
10
+ import type { FalJobInput, FalQueueStatus } from "../../domain/entities/fal.types";
10
11
  import type { FalErrorInfo } from "../../domain/entities/error.types";
12
+ import type { JobStatus } from "../../domain/types";
11
13
 
12
14
  export interface UseFalGenerationOptions {
13
15
  timeoutMs?: number;
@@ -28,6 +30,19 @@ export interface UseFalGenerationResult<T> {
28
30
  reset: () => void;
29
31
  }
30
32
 
33
+ function convertJobStatusToFalQueueStatus(status: JobStatus, currentRequestId: string | null): FalQueueStatus {
34
+ return {
35
+ status: status.status as FalQueueStatus["status"],
36
+ requestId: status.requestId ?? currentRequestId ?? "",
37
+ logs: status.logs?.map((log) => ({
38
+ message: log.message,
39
+ level: log.level,
40
+ timestamp: log.timestamp,
41
+ })),
42
+ queuePosition: status.queuePosition,
43
+ };
44
+ }
45
+
31
46
  export function useFalGeneration<T = unknown>(
32
47
  options?: UseFalGenerationOptions
33
48
  ): UseFalGenerationResult<T> {
@@ -36,76 +51,82 @@ export function useFalGeneration<T = unknown>(
36
51
  const [isLoading, setIsLoading] = useState(false);
37
52
  const [isCancelling, setIsCancelling] = useState(false);
38
53
 
39
- const lastRequestRef = useRef<{ endpoint: string; input: FalJobInput } | null>(null);
40
- const currentRequestIdRef = useRef<string | null>(null);
41
- const isMountedRef = useRef(true);
54
+ const stateManagerRef = useRef<FalGenerationStateManager<T> | null>(null);
55
+ const optionsRef = useRef(options);
56
+
57
+ // Keep optionsRef updated without causing effect re-runs
58
+ useEffect(() => {
59
+ optionsRef.current = options;
60
+ }, [options]);
42
61
 
43
- // Cleanup on unmount
44
62
  useEffect(() => {
45
- isMountedRef.current = true;
63
+ stateManagerRef.current = new FalGenerationStateManager<T>({
64
+ onProgress: (status) => {
65
+ optionsRef.current?.onProgress?.(status);
66
+ },
67
+ });
68
+
69
+ stateManagerRef.current.setIsMounted(true);
70
+
46
71
  return () => {
47
- isMountedRef.current = false;
72
+ stateManagerRef.current?.setIsMounted(false);
48
73
  if (falProvider.hasRunningRequest()) {
49
74
  falProvider.cancelCurrentRequest();
50
75
  }
51
76
  };
52
- }, []);
77
+ }, []); // Empty deps - only run on mount/unmount
53
78
 
54
79
  const generate = useCallback(
55
80
  async (modelEndpoint: string, input: FalJobInput): Promise<T | null> => {
56
- if (!isMountedRef.current) return null;
81
+ const stateManager = stateManagerRef.current;
82
+ if (!stateManager || !stateManager.checkMounted()) return null;
57
83
 
58
- lastRequestRef.current = { endpoint: modelEndpoint, input };
84
+ stateManager.setLastRequest(modelEndpoint, input);
59
85
  setIsLoading(true);
60
86
  setError(null);
61
87
  setData(null);
62
- currentRequestIdRef.current = null;
88
+ stateManager.setCurrentRequestId(null);
63
89
  setIsCancelling(false);
64
90
 
65
91
  try {
66
92
  const result = await falProvider.subscribe<T>(modelEndpoint, input, {
67
- timeoutMs: options?.timeoutMs,
68
- onQueueUpdate: (status) => {
69
- if (!isMountedRef.current) return;
70
- if (status.requestId) {
71
- currentRequestIdRef.current = status.requestId;
72
- }
73
- options?.onProgress?.({
74
- status: status.status,
75
- requestId: status.requestId ?? currentRequestIdRef.current ?? "",
76
- logs: status.logs?.map((log: FalLogEntry) => ({
77
- message: log.message,
78
- level: log.level,
79
- timestamp: log.timestamp,
80
- })),
81
- queuePosition: status.queuePosition,
82
- });
93
+ timeoutMs: optionsRef.current?.timeoutMs,
94
+ onQueueUpdate: (status: JobStatus) => {
95
+ const falStatus = convertJobStatusToFalQueueStatus(
96
+ status,
97
+ stateManager.getCurrentRequestId()
98
+ );
99
+ stateManager.handleQueueUpdate(falStatus);
83
100
  },
84
101
  });
85
102
 
86
- if (!isMountedRef.current) return null;
103
+ if (!stateManager.checkMounted()) return null;
87
104
  setData(result);
88
105
  return result;
89
106
  } catch (err) {
90
- if (!isMountedRef.current) return null;
107
+ if (!stateManager.checkMounted()) return null;
91
108
  const errorInfo = mapFalError(err);
92
109
  setError(errorInfo);
93
- options?.onError?.(errorInfo);
110
+ optionsRef.current?.onError?.(errorInfo);
94
111
  return null;
95
112
  } finally {
96
- if (isMountedRef.current) {
113
+ if (stateManager.checkMounted()) {
97
114
  setIsLoading(false);
98
115
  setIsCancelling(false);
99
116
  }
100
117
  }
101
118
  },
102
- [options]
119
+ [] // No deps - we use optionsRef.current inside
103
120
  );
104
121
 
105
122
  const retry = useCallback(async (): Promise<T | null> => {
106
- if (!lastRequestRef.current) return null;
107
- const { endpoint, input } = lastRequestRef.current;
108
- return generate(endpoint, input);
123
+ const stateManager = stateManagerRef.current;
124
+ if (!stateManager) return null;
125
+
126
+ const lastRequest = stateManager.getLastRequest();
127
+ if (!lastRequest) return null;
128
+
129
+ return generate(lastRequest.endpoint, lastRequest.input);
109
130
  }, [generate]);
110
131
 
111
132
  const cancel = useCallback(() => {
@@ -121,16 +142,17 @@ export function useFalGeneration<T = unknown>(
121
142
  setError(null);
122
143
  setIsLoading(false);
123
144
  setIsCancelling(false);
124
- lastRequestRef.current = null;
125
- currentRequestIdRef.current = null;
145
+ stateManagerRef.current?.clearLastRequest();
126
146
  }, [cancel]);
127
147
 
148
+ const requestId = stateManagerRef.current?.getCurrentRequestId() ?? null;
149
+
128
150
  return {
129
151
  data,
130
152
  error,
131
153
  isLoading,
132
154
  isRetryable: error?.retryable ?? false,
133
- requestId: currentRequestIdRef.current,
155
+ requestId,
134
156
  isCancelling,
135
157
  generate,
136
158
  retry,
@@ -12,7 +12,6 @@
12
12
  import { useState, useEffect, useCallback, useMemo } from "react";
13
13
  import { falModelsService } from "../../infrastructure/services/fal-models.service";
14
14
  import type { FalModelConfig } from "../../domain/constants/default-models.constants";
15
- import { DEFAULT_CREDIT_COSTS, DEFAULT_MODEL_IDS } from "../../domain/constants/default-models.constants";
16
15
  import type {
17
16
  ModelType,
18
17
  ModelSelectionConfig,
@@ -36,28 +35,16 @@ export function useModels(props: UseModelsProps): UseModelsReturn {
36
35
  const [isLoading, setIsLoading] = useState(true);
37
36
  const [error, setError] = useState<string | null>(null);
38
37
 
39
- const defaultCreditCost = config?.defaultCreditCost ?? DEFAULT_CREDIT_COSTS[type];
40
- const defaultModelId = config?.defaultModelId ?? DEFAULT_MODEL_IDS[type];
41
-
42
38
  const loadModels = useCallback(() => {
43
39
  setIsLoading(true);
44
40
  setError(null);
45
41
 
46
- const fetchedModels = falModelsService.getModels(type);
47
- setModels(fetchedModels);
48
-
49
- const targetId = config?.initialModelId ?? defaultModelId;
50
- const initial =
51
- fetchedModels.find((m) => m.id === targetId) ||
52
- fetchedModels.find((m) => m.isDefault) ||
53
- fetchedModels[0];
54
-
55
- if (initial) {
56
- setSelectedModel(initial);
57
- }
42
+ const selectionData = falModelsService.getModelSelectionData(type, config);
43
+ setModels(selectionData.models);
44
+ setSelectedModel(selectionData.selectedModel);
58
45
 
59
46
  setIsLoading(false);
60
- }, [type, config?.initialModelId, defaultModelId]);
47
+ }, [type, config]);
61
48
 
62
49
  useEffect(() => {
63
50
  loadModels();
@@ -68,24 +55,21 @@ export function useModels(props: UseModelsProps): UseModelsReturn {
68
55
  const model = models.find((m) => m.id === modelId);
69
56
  if (model) {
70
57
  setSelectedModel(model);
71
- } else {
72
- // eslint-disable-next-line no-console
73
- console.warn(`Model not found: ${modelId}. Available models:`, models.map(m => m.id));
74
58
  }
75
59
  },
76
60
  [models],
77
61
  );
78
62
 
79
63
  const creditCost = useMemo(() => {
80
- if (selectedModel?.pricing?.freeUserCost) {
81
- return selectedModel.pricing.freeUserCost;
82
- }
83
- return defaultCreditCost;
84
- }, [selectedModel, defaultCreditCost]);
64
+ return falModelsService.getModelCreditCost(
65
+ selectedModel?.id ?? falModelsService.getDefaultModelId(type),
66
+ type
67
+ );
68
+ }, [selectedModel, type]);
85
69
 
86
70
  const modelId = useMemo(() => {
87
- return selectedModel?.id ?? defaultModelId;
88
- }, [selectedModel, defaultModelId]);
71
+ return selectedModel?.id ?? falModelsService.getDefaultModelId(type);
72
+ }, [selectedModel, type]);
89
73
 
90
74
  return {
91
75
  models,