@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 +1 -1
- package/src/index.ts +8 -1
- package/src/infrastructure/services/fal-provider.ts +40 -1
- package/src/infrastructure/services/index.ts +16 -0
- package/src/infrastructure/utils/index.ts +13 -0
- package/src/infrastructure/utils/job-storage.util.ts +187 -0
- package/src/presentation/hooks/use-fal-generation.ts +27 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-fal-provider",
|
|
3
|
-
"version": "1.0.
|
|
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
|
|
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(() =>
|
|
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:
|
|
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
|
}
|