@umituz/react-native-ai-fal-provider 1.0.60 → 1.0.61

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": "1.0.60",
3
+ "version": "1.0.61",
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",
package/src/index.ts CHANGED
@@ -26,7 +26,9 @@ export type { FeatureModelConfig } from "./domain/constants/feature-models.const
26
26
  export {
27
27
  FalProvider, falProvider, falModelsService,
28
28
  getImageFeatureModel, getVideoFeatureModel, NSFWContentError,
29
+ cancelCurrentFalRequest, hasRunningFalRequest,
29
30
  } from "./infrastructure/services";
31
+ export type { FalProviderType } from "./infrastructure/services";
30
32
 
31
33
  export {
32
34
  categorizeFalError, falErrorMapper, mapFalError, isFalErrorRetryable,
@@ -55,7 +57,12 @@ export {
55
57
  sortJobsByCreation, getActiveJobs, getCompletedJobs,
56
58
  } from "./infrastructure/utils";
57
59
 
58
- export type { FalJobMetadata } from "./infrastructure/utils";
60
+ export {
61
+ saveJobMetadata, loadJobMetadata, deleteJobMetadata, loadAllJobs,
62
+ cleanupOldJobs, getJobsByModel, getJobsByStatus, updateJobStatus,
63
+ } from "./infrastructure/utils";
64
+
65
+ export type { FalJobMetadata, IJobStorage, InMemoryJobStorage } from "./infrastructure/utils";
59
66
 
60
67
  export type {
61
68
  UpscaleOptions, PhotoRestoreOptions, FaceSwapOptions, ImageToImagePromptConfig,
@@ -33,6 +33,7 @@ export class FalProvider implements IAIProvider {
33
33
  private apiKey: string | null = null;
34
34
  private config: AIProviderConfig | null = null;
35
35
  private initialized = false;
36
+ private currentAbortController: AbortController | null = null;
36
37
 
37
38
  initialize(configData: AIProviderConfig): void {
38
39
  this.apiKey = configData.apiKey;
@@ -104,6 +105,14 @@ export class FalProvider implements IAIProvider {
104
105
  options?: SubscribeOptions<T>,
105
106
  ): Promise<T> {
106
107
  this.validateInitialization();
108
+
109
+ // Cancel previous request if exists
110
+ this.cancelCurrentRequest();
111
+
112
+ // Create new abort controller
113
+ this.currentAbortController = new AbortController();
114
+ const { signal } = this.currentAbortController;
115
+
107
116
  const timeoutMs = options?.timeoutMs ?? this.config?.defaultTimeoutMs ?? DEFAULT_FAL_CONFIG.defaultTimeoutMs;
108
117
  let timeoutId: ReturnType<typeof setTimeout> | null = null;
109
118
  let currentRequestId: string | null = null;
@@ -136,7 +145,15 @@ export class FalProvider implements IAIProvider {
136
145
  },
137
146
  }),
138
147
  new Promise<never>((_, reject) => {
139
- timeoutId = setTimeout(() => reject(new Error("FAL subscription timeout")), timeoutMs);
148
+ timeoutId = setTimeout(() => {
149
+ reject(new Error("FAL subscription timeout"));
150
+ }, timeoutMs);
151
+ }),
152
+ // Abort promise
153
+ new Promise<never>((_, reject) => {
154
+ signal.addEventListener("abort", () => {
155
+ reject(new Error("Request cancelled by user"));
156
+ });
140
157
  }),
141
158
  ]);
142
159
 
@@ -150,6 +167,7 @@ export class FalProvider implements IAIProvider {
150
167
  return result as T;
151
168
  } finally {
152
169
  if (timeoutId) clearTimeout(timeoutId);
170
+ this.currentAbortController = null;
153
171
  }
154
172
  }
155
173
 
@@ -176,11 +194,32 @@ export class FalProvider implements IAIProvider {
176
194
  }
177
195
 
178
196
  reset(): void {
197
+ this.cancelCurrentRequest();
179
198
  this.apiKey = null;
180
199
  this.config = null;
181
200
  this.initialized = false;
182
201
  }
183
202
 
203
+ /**
204
+ * Cancel the current running request
205
+ */
206
+ cancelCurrentRequest(): void {
207
+ if (this.currentAbortController) {
208
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
209
+ console.log("[FalProvider] Cancelling current request");
210
+ }
211
+ this.currentAbortController.abort();
212
+ this.currentAbortController = null;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Check if there's a running request
218
+ */
219
+ hasRunningRequest(): boolean {
220
+ return this.currentAbortController !== null;
221
+ }
222
+
184
223
  getImageFeatureModel(feature: ImageFeatureType): string {
185
224
  return FAL_IMAGE_FEATURE_MODELS[feature];
186
225
  }
@@ -4,8 +4,10 @@
4
4
 
5
5
  import { FAL_IMAGE_FEATURE_MODELS, FAL_VIDEO_FEATURE_MODELS } from "../../domain/constants/feature-models.constants";
6
6
  import type { ImageFeatureType, VideoFeatureType } from "@umituz/react-native-ai-generation-content";
7
+ import { falProvider } from "./fal-provider";
7
8
 
8
9
  export { FalProvider, falProvider } from "./fal-provider";
10
+ export type { FalProvider as FalProviderType } from "./fal-provider";
9
11
  export { falModelsService, type FalModelConfig } from "./fal-models.service";
10
12
  export { NSFWContentError } from "./nsfw-content-error";
11
13
 
@@ -16,3 +18,17 @@ export function getImageFeatureModel(feature: ImageFeatureType): string {
16
18
  export function getVideoFeatureModel(feature: VideoFeatureType): string {
17
19
  return FAL_VIDEO_FEATURE_MODELS[feature];
18
20
  }
21
+
22
+ /**
23
+ * Cancel the current running FAL request
24
+ */
25
+ export function cancelCurrentFalRequest(): void {
26
+ falProvider.cancelCurrentRequest();
27
+ }
28
+
29
+ /**
30
+ * Check if there's a running FAL request
31
+ */
32
+ export function hasRunningFalRequest(): boolean {
33
+ return falProvider.hasRunningRequest();
34
+ }
@@ -65,3 +65,16 @@ export {
65
65
  } from "./job-metadata.util";
66
66
 
67
67
  export type { FalJobMetadata } from "./job-metadata.util";
68
+
69
+ export {
70
+ saveJobMetadata,
71
+ loadJobMetadata,
72
+ deleteJobMetadata,
73
+ loadAllJobs,
74
+ cleanupOldJobs,
75
+ getJobsByModel,
76
+ getJobsByStatus,
77
+ updateJobStatus,
78
+ } from "./job-storage.util";
79
+
80
+ export type { IJobStorage, InMemoryJobStorage } from "./job-storage.util";
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Job Storage Utilities
3
+ * Helper functions for job persistence and storage integration
4
+ */
5
+
6
+ import type { FalJobMetadata } from "./job-metadata.util";
7
+
8
+ /**
9
+ * Generic storage interface for job persistence
10
+ * Implement this interface with your preferred storage backend
11
+ */
12
+ export interface IJobStorage {
13
+ setItem(key: string, value: string): Promise<void>;
14
+ getItem(key: string): Promise<string | null>;
15
+ removeItem(key: string): Promise<void>;
16
+ getAllKeys?(): Promise<readonly string[]>;
17
+ }
18
+
19
+ /**
20
+ * Save job metadata to storage
21
+ */
22
+ export async function saveJobMetadata(
23
+ storage: IJobStorage,
24
+ metadata: FalJobMetadata
25
+ ): Promise<void> {
26
+ const key = `fal_job:${metadata.requestId}`;
27
+ const value = JSON.stringify(metadata);
28
+ await storage.setItem(key, value);
29
+ }
30
+
31
+ /**
32
+ * Load job metadata from storage
33
+ */
34
+ export async function loadJobMetadata(
35
+ storage: IJobStorage,
36
+ requestId: string
37
+ ): Promise<FalJobMetadata | null> {
38
+ const key = `fal_job:${requestId}`;
39
+ const value = await storage.getItem(key);
40
+ if (!value) return null;
41
+
42
+ try {
43
+ return JSON.parse(value) as FalJobMetadata;
44
+ } catch {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Delete job metadata from storage
51
+ */
52
+ export async function deleteJobMetadata(
53
+ storage: IJobStorage,
54
+ requestId: string
55
+ ): Promise<void> {
56
+ const key = `fal_job:${requestId}`;
57
+ await storage.removeItem(key);
58
+ }
59
+
60
+ /**
61
+ * Load all jobs from storage
62
+ */
63
+ export async function loadAllJobs(
64
+ storage: IJobStorage
65
+ ): Promise<FalJobMetadata[]> {
66
+ if (!storage.getAllKeys) {
67
+ return [];
68
+ }
69
+
70
+ const keys = await storage.getAllKeys();
71
+ const jobKeys = keys.filter((key) => key.startsWith("fal_job:"));
72
+
73
+ const jobs: FalJobMetadata[] = [];
74
+ for (const key of jobKeys) {
75
+ const value = await storage.getItem(key);
76
+ if (value) {
77
+ try {
78
+ const metadata = JSON.parse(value) as FalJobMetadata;
79
+ jobs.push(metadata);
80
+ } catch {
81
+ // Skip invalid entries
82
+ continue;
83
+ }
84
+ }
85
+ }
86
+
87
+ return jobs;
88
+ }
89
+
90
+ /**
91
+ * Clean up old jobs from storage
92
+ */
93
+ export async function cleanupOldJobs(
94
+ storage: IJobStorage,
95
+ maxAgeMinutes: number = 60
96
+ ): Promise<number> {
97
+ const jobs = await loadAllJobs(storage);
98
+ const now = Date.now();
99
+ const maxAgeMs = maxAgeMinutes * 60 * 1000;
100
+ let cleanedCount = 0;
101
+
102
+ for (const job of jobs) {
103
+ const jobAge = now - new Date(job.createdAt).getTime();
104
+ if (jobAge > maxAgeMs) {
105
+ await deleteJobMetadata(storage, job.requestId);
106
+ cleanedCount++;
107
+ }
108
+ }
109
+
110
+ return cleanedCount;
111
+ }
112
+
113
+ /**
114
+ * Get jobs by model
115
+ */
116
+ export async function getJobsByModel(
117
+ storage: IJobStorage,
118
+ model: string
119
+ ): Promise<FalJobMetadata[]> {
120
+ const jobs = await loadAllJobs(storage);
121
+ return jobs.filter((job) => job.model === model);
122
+ }
123
+
124
+ /**
125
+ * Get jobs by status
126
+ */
127
+ export async function getJobsByStatus(
128
+ storage: IJobStorage,
129
+ status: FalJobMetadata["status"]
130
+ ): Promise<FalJobMetadata[]> {
131
+ const jobs = await loadAllJobs(storage);
132
+ return jobs.filter((job) => job.status === status);
133
+ }
134
+
135
+ /**
136
+ * Update job status in storage
137
+ */
138
+ export async function updateJobStatus(
139
+ storage: IJobStorage,
140
+ requestId: string,
141
+ status: FalJobMetadata["status"],
142
+ error?: string
143
+ ): Promise<void> {
144
+ const metadata = await loadJobMetadata(storage, requestId);
145
+ if (!metadata) {
146
+ throw new Error(`Job not found: ${requestId}`);
147
+ }
148
+
149
+ const updated: FalJobMetadata = {
150
+ ...metadata,
151
+ status,
152
+ updatedAt: new Date().toISOString(),
153
+ ...(status === "COMPLETED" || status === "FAILED" ? { completedAt: new Date().toISOString() } : {}),
154
+ ...(error ? { error } : {}),
155
+ };
156
+
157
+ await saveJobMetadata(storage, updated);
158
+ }
159
+
160
+ /**
161
+ * Create a simple in-memory storage for testing
162
+ */
163
+ export class InMemoryJobStorage implements IJobStorage {
164
+ private store = new Map<string, string>();
165
+
166
+ setItem(key: string, value: string): Promise<void> {
167
+ this.store.set(key, value);
168
+ return Promise.resolve();
169
+ }
170
+
171
+ getItem(key: string): Promise<string | null> {
172
+ return Promise.resolve(this.store.get(key) ?? null);
173
+ }
174
+
175
+ removeItem(key: string): Promise<void> {
176
+ this.store.delete(key);
177
+ return Promise.resolve();
178
+ }
179
+
180
+ getAllKeys(): Promise<readonly string[]> {
181
+ return Promise.resolve(Array.from(this.store.keys()));
182
+ }
183
+
184
+ clear(): void {
185
+ this.store.clear();
186
+ }
187
+ }
@@ -21,8 +21,10 @@ export interface UseFalGenerationResult<T> {
21
21
  isLoading: boolean;
22
22
  isRetryable: boolean;
23
23
  requestId: string | null;
24
+ isCancelling: boolean;
24
25
  generate: (modelEndpoint: string, input: FalJobInput) => Promise<T | null>;
25
26
  retry: () => Promise<T | null>;
27
+ cancel: () => void;
26
28
  reset: () => void;
27
29
  }
28
30
 
@@ -32,9 +34,11 @@ export function useFalGeneration<T = unknown>(
32
34
  const [data, setData] = useState<T | null>(null);
33
35
  const [error, setError] = useState<FalErrorInfo | null>(null);
34
36
  const [isLoading, setIsLoading] = useState(false);
37
+ const [isCancelling, setIsCancelling] = useState(false);
35
38
 
36
39
  const lastRequestRef = useRef<{ endpoint: string; input: FalJobInput } | null>(null);
37
40
  const currentRequestIdRef = useRef<string | null>(null);
41
+ const abortControllerRef = useRef<AbortController | null>(null);
38
42
 
39
43
  const generate = useCallback(
40
44
  async (modelEndpoint: string, input: FalJobInput): Promise<T | null> => {
@@ -43,6 +47,10 @@ export function useFalGeneration<T = unknown>(
43
47
  setError(null);
44
48
  setData(null);
45
49
  currentRequestIdRef.current = null;
50
+ setIsCancelling(false);
51
+
52
+ // Create abort controller for this request
53
+ abortControllerRef.current = new AbortController();
46
54
 
47
55
  try {
48
56
  const result = await falProvider.subscribe<T>(modelEndpoint, input, {
@@ -50,10 +58,11 @@ export function useFalGeneration<T = unknown>(
50
58
  onQueueUpdate: (status) => {
51
59
  // Note: requestId is tracked internally by falProvider subscribe
52
60
  // and exposed via the requestId ref, not from status object
61
+ const currentRequestId = currentRequestIdRef.current ?? "";
53
62
  // Map JobStatus to FalQueueStatus for backward compatibility
54
63
  options?.onProgress?.({
55
64
  status: status.status,
56
- requestId: currentRequestIdRef.current ?? "",
65
+ requestId: currentRequestId,
57
66
  logs: status.logs?.map((log: FalLogEntry) => ({
58
67
  message: log.message,
59
68
  level: log.level,
@@ -73,6 +82,8 @@ export function useFalGeneration<T = unknown>(
73
82
  return null;
74
83
  } finally {
75
84
  setIsLoading(false);
85
+ setIsCancelling(false);
86
+ abortControllerRef.current = null;
76
87
  }
77
88
  },
78
89
  [options]
@@ -84,13 +95,25 @@ export function useFalGeneration<T = unknown>(
84
95
  return generate(endpoint, input);
85
96
  }, [generate]);
86
97
 
98
+ const cancel = useCallback(() => {
99
+ if (abortControllerRef.current) {
100
+ setIsCancelling(true);
101
+ abortControllerRef.current.abort();
102
+ abortControllerRef.current = null;
103
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
104
+ console.log("[useFalGeneration] Request cancelled");
105
+ }
106
+ }
107
+ }, []);
108
+
87
109
  const reset = useCallback(() => {
110
+ cancel();
88
111
  setData(null);
89
112
  setError(null);
90
113
  setIsLoading(false);
91
114
  lastRequestRef.current = null;
92
115
  currentRequestIdRef.current = null;
93
- }, []);
116
+ }, [cancel]);
94
117
 
95
118
  return {
96
119
  data,
@@ -98,8 +121,10 @@ export function useFalGeneration<T = unknown>(
98
121
  isLoading,
99
122
  isRetryable: error ? isFalErrorRetryable(error.originalError) : false,
100
123
  requestId: currentRequestIdRef.current,
124
+ isCancelling,
101
125
  generate,
102
126
  retry,
127
+ cancel,
103
128
  reset,
104
129
  };
105
130
  }