@umituz/react-native-ai-fal-provider 1.0.59 → 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.59",
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,
@@ -48,6 +50,20 @@ export {
48
50
  buildErrorMessage, isDefined, removeNullish, debounce, throttle,
49
51
  } from "./infrastructure/utils";
50
52
 
53
+ export {
54
+ createJobMetadata, updateJobMetadata, isJobCompleted, isJobRunning,
55
+ isJobStale, getJobDuration, formatJobDuration, calculateJobProgress,
56
+ serializeJobMetadata, deserializeJobMetadata, filterValidJobs,
57
+ sortJobsByCreation, getActiveJobs, getCompletedJobs,
58
+ } from "./infrastructure/utils";
59
+
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";
66
+
51
67
  export type {
52
68
  UpscaleOptions, PhotoRestoreOptions, FaceSwapOptions, ImageToImagePromptConfig,
53
69
  RemoveBackgroundOptions, RemoveObjectOptions, ReplaceBackgroundOptions,
@@ -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,8 +105,17 @@ 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;
118
+ let currentRequestId: string | null = null;
109
119
 
110
120
  if (typeof __DEV__ !== "undefined" && __DEV__) {
111
121
  console.log("[FalProvider] Subscribe started:", { model, timeoutMs });
@@ -119,24 +129,36 @@ export class FalProvider implements IAIProvider {
119
129
  input,
120
130
  logs: false,
121
131
  pollInterval: DEFAULT_FAL_CONFIG.pollInterval,
122
- onQueueUpdate: (update: { status: string; logs?: unknown[] }) => {
123
- const jobStatus = mapFalStatusToJobStatus(update as unknown as FalQueueStatus);
132
+ onQueueUpdate: (update: { status: string; logs?: unknown[]; request_id?: string }) => {
133
+ currentRequestId = update.request_id ?? null;
134
+ const jobStatus = mapFalStatusToJobStatus({
135
+ ...update as unknown as FalQueueStatus,
136
+ requestId: currentRequestId ?? "",
137
+ });
124
138
  if (jobStatus.status !== lastStatus) {
125
139
  lastStatus = jobStatus.status;
126
140
  if (typeof __DEV__ !== "undefined" && __DEV__) {
127
- console.log("[FalProvider] Status:", jobStatus.status);
141
+ console.log("[FalProvider] Status:", jobStatus.status, "RequestId:", currentRequestId);
128
142
  }
129
143
  }
130
144
  options?.onQueueUpdate?.(jobStatus);
131
145
  },
132
146
  }),
133
147
  new Promise<never>((_, reject) => {
134
- 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
+ });
135
157
  }),
136
158
  ]);
137
159
 
138
160
  if (typeof __DEV__ !== "undefined" && __DEV__) {
139
- console.log("[FalProvider] Subscribe completed:", { model });
161
+ console.log("[FalProvider] Subscribe completed:", { model, requestId: currentRequestId });
140
162
  }
141
163
 
142
164
  validateNSFWContent(result as Record<string, unknown>);
@@ -145,6 +167,7 @@ export class FalProvider implements IAIProvider {
145
167
  return result as T;
146
168
  } finally {
147
169
  if (timeoutId) clearTimeout(timeoutId);
170
+ this.currentAbortController = null;
148
171
  }
149
172
  }
150
173
 
@@ -171,11 +194,32 @@ export class FalProvider implements IAIProvider {
171
194
  }
172
195
 
173
196
  reset(): void {
197
+ this.cancelCurrentRequest();
174
198
  this.apiKey = null;
175
199
  this.config = null;
176
200
  this.initialized = false;
177
201
  }
178
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
+
179
223
  getImageFeatureModel(feature: ImageFeatureType): string {
180
224
  return FAL_IMAGE_FEATURE_MODELS[feature];
181
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
+ }
@@ -46,3 +46,35 @@ export {
46
46
  debounce,
47
47
  throttle,
48
48
  } from "./helpers.util";
49
+
50
+ export {
51
+ createJobMetadata,
52
+ updateJobMetadata,
53
+ isJobCompleted,
54
+ isJobRunning,
55
+ isJobStale,
56
+ getJobDuration,
57
+ formatJobDuration,
58
+ calculateJobProgress,
59
+ serializeJobMetadata,
60
+ deserializeJobMetadata,
61
+ filterValidJobs,
62
+ sortJobsByCreation,
63
+ getActiveJobs,
64
+ getCompletedJobs,
65
+ } from "./job-metadata.util";
66
+
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,176 @@
1
+ /**
2
+ * Job Metadata Utilities
3
+ * Helper functions for job metadata and management
4
+ */
5
+
6
+ import type { JobStatus } from "@umituz/react-native-ai-generation-content";
7
+
8
+ /**
9
+ * Job metadata for tracking and persistence
10
+ */
11
+ export interface FalJobMetadata {
12
+ readonly requestId: string;
13
+ readonly model: string;
14
+ readonly status: JobStatus["status"];
15
+ readonly createdAt: string;
16
+ readonly updatedAt: string;
17
+ readonly completedAt?: string;
18
+ readonly timeout?: number;
19
+ readonly error?: string;
20
+ }
21
+
22
+ /**
23
+ * Create job metadata
24
+ */
25
+ export function createJobMetadata(
26
+ requestId: string,
27
+ model: string,
28
+ timeout?: number
29
+ ): FalJobMetadata {
30
+ const now = new Date().toISOString();
31
+ return {
32
+ requestId,
33
+ model,
34
+ status: "IN_QUEUE",
35
+ createdAt: now,
36
+ updatedAt: now,
37
+ timeout,
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Update job metadata status
43
+ */
44
+ export function updateJobMetadata(
45
+ metadata: FalJobMetadata,
46
+ status: JobStatus["status"],
47
+ error?: string
48
+ ): FalJobMetadata {
49
+ return {
50
+ ...metadata,
51
+ status,
52
+ updatedAt: new Date().toISOString(),
53
+ ...(status === "COMPLETED" || status === "FAILED" ? { completedAt: new Date().toISOString() } : {}),
54
+ ...(error ? { error } : {}),
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Check if job is completed (success or failure)
60
+ */
61
+ export function isJobCompleted(metadata: FalJobMetadata): boolean {
62
+ return metadata.status === "COMPLETED" || metadata.status === "FAILED";
63
+ }
64
+
65
+ /**
66
+ * Check if job is running
67
+ */
68
+ export function isJobRunning(metadata: FalJobMetadata): boolean {
69
+ return metadata.status === "IN_QUEUE" || metadata.status === "IN_PROGRESS";
70
+ }
71
+
72
+ /**
73
+ * Check if job is stale (older than specified minutes)
74
+ */
75
+ export function isJobStale(metadata: FalJobMetadata, maxAgeMinutes: number = 60): boolean {
76
+ const age = Date.now() - new Date(metadata.createdAt).getTime();
77
+ const maxAgeMs = maxAgeMinutes * 60 * 1000;
78
+ return age > maxAgeMs;
79
+ }
80
+
81
+ /**
82
+ * Get job duration in milliseconds
83
+ */
84
+ export function getJobDuration(metadata: FalJobMetadata): number | null {
85
+ if (!metadata.completedAt) {
86
+ return null;
87
+ }
88
+ return new Date(metadata.completedAt).getTime() - new Date(metadata.createdAt).getTime();
89
+ }
90
+
91
+ /**
92
+ * Format job duration for display
93
+ */
94
+ export function formatJobDuration(metadata: FalJobMetadata): string {
95
+ const duration = getJobDuration(metadata);
96
+ if (!duration) {
97
+ return "In progress";
98
+ }
99
+
100
+ const seconds = Math.floor(duration / 1000);
101
+ if (seconds < 60) {
102
+ return `${seconds}s`;
103
+ }
104
+
105
+ const minutes = Math.floor(seconds / 60);
106
+ const remainingSeconds = seconds % 60;
107
+ return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
108
+ }
109
+
110
+ /**
111
+ * Calculate job progress percentage
112
+ */
113
+ export function calculateJobProgress(metadata: FalJobMetadata): number {
114
+ switch (metadata.status) {
115
+ case "IN_QUEUE":
116
+ return 10;
117
+ case "IN_PROGRESS":
118
+ return 50;
119
+ case "COMPLETED":
120
+ return 100;
121
+ case "FAILED":
122
+ return 0;
123
+ default:
124
+ return 0;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Serialize job metadata for storage
130
+ */
131
+ export function serializeJobMetadata(metadata: FalJobMetadata): string {
132
+ return JSON.stringify(metadata);
133
+ }
134
+
135
+ /**
136
+ * Deserialize job metadata from storage
137
+ */
138
+ export function deserializeJobMetadata(data: string): FalJobMetadata | null {
139
+ try {
140
+ return JSON.parse(data) as FalJobMetadata;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Filter valid job metadata from array
148
+ */
149
+ export function filterValidJobs(jobs: FalJobMetadata[]): FalJobMetadata[] {
150
+ return jobs.filter((job) => !isJobStale(job));
151
+ }
152
+
153
+ /**
154
+ * Sort jobs by creation time (newest first)
155
+ */
156
+ export function sortJobsByCreation(jobs: FalJobMetadata[]): FalJobMetadata[] {
157
+ return [...jobs].sort((a, b) => {
158
+ const timeA = new Date(a.createdAt).getTime();
159
+ const timeB = new Date(b.createdAt).getTime();
160
+ return timeB - timeA;
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Get active jobs (not completed and not stale)
166
+ */
167
+ export function getActiveJobs(jobs: FalJobMetadata[]): FalJobMetadata[] {
168
+ return jobs.filter((job) => isJobRunning(job) && !isJobStale(job));
169
+ }
170
+
171
+ /**
172
+ * Get completed jobs
173
+ */
174
+ export function getCompletedJobs(jobs: FalJobMetadata[]): FalJobMetadata[] {
175
+ return jobs.filter((job) => isJobCompleted(job));
176
+ }
@@ -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
+ }
@@ -20,8 +20,11 @@ export interface UseFalGenerationResult<T> {
20
20
  error: FalErrorInfo | null;
21
21
  isLoading: boolean;
22
22
  isRetryable: boolean;
23
+ requestId: string | null;
24
+ isCancelling: boolean;
23
25
  generate: (modelEndpoint: string, input: FalJobInput) => Promise<T | null>;
24
26
  retry: () => Promise<T | null>;
27
+ cancel: () => void;
25
28
  reset: () => void;
26
29
  }
27
30
 
@@ -31,8 +34,11 @@ export function useFalGeneration<T = unknown>(
31
34
  const [data, setData] = useState<T | null>(null);
32
35
  const [error, setError] = useState<FalErrorInfo | null>(null);
33
36
  const [isLoading, setIsLoading] = useState(false);
37
+ const [isCancelling, setIsCancelling] = useState(false);
34
38
 
35
39
  const lastRequestRef = useRef<{ endpoint: string; input: FalJobInput } | null>(null);
40
+ const currentRequestIdRef = useRef<string | null>(null);
41
+ const abortControllerRef = useRef<AbortController | null>(null);
36
42
 
37
43
  const generate = useCallback(
38
44
  async (modelEndpoint: string, input: FalJobInput): Promise<T | null> => {
@@ -40,15 +46,23 @@ export function useFalGeneration<T = unknown>(
40
46
  setIsLoading(true);
41
47
  setError(null);
42
48
  setData(null);
49
+ currentRequestIdRef.current = null;
50
+ setIsCancelling(false);
51
+
52
+ // Create abort controller for this request
53
+ abortControllerRef.current = new AbortController();
43
54
 
44
55
  try {
45
56
  const result = await falProvider.subscribe<T>(modelEndpoint, input, {
46
57
  timeoutMs: options?.timeoutMs,
47
58
  onQueueUpdate: (status) => {
59
+ // Note: requestId is tracked internally by falProvider subscribe
60
+ // and exposed via the requestId ref, not from status object
61
+ const currentRequestId = currentRequestIdRef.current ?? "";
48
62
  // Map JobStatus to FalQueueStatus for backward compatibility
49
63
  options?.onProgress?.({
50
64
  status: status.status,
51
- requestId: "",
65
+ requestId: currentRequestId,
52
66
  logs: status.logs?.map((log: FalLogEntry) => ({
53
67
  message: log.message,
54
68
  level: log.level,
@@ -68,6 +82,8 @@ export function useFalGeneration<T = unknown>(
68
82
  return null;
69
83
  } finally {
70
84
  setIsLoading(false);
85
+ setIsCancelling(false);
86
+ abortControllerRef.current = null;
71
87
  }
72
88
  },
73
89
  [options]
@@ -79,20 +95,36 @@ export function useFalGeneration<T = unknown>(
79
95
  return generate(endpoint, input);
80
96
  }, [generate]);
81
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
+
82
109
  const reset = useCallback(() => {
110
+ cancel();
83
111
  setData(null);
84
112
  setError(null);
85
113
  setIsLoading(false);
86
114
  lastRequestRef.current = null;
87
- }, []);
115
+ currentRequestIdRef.current = null;
116
+ }, [cancel]);
88
117
 
89
118
  return {
90
119
  data,
91
120
  error,
92
121
  isLoading,
93
122
  isRetryable: error ? isFalErrorRetryable(error.originalError) : false,
123
+ requestId: currentRequestIdRef.current,
124
+ isCancelling,
94
125
  generate,
95
126
  retry,
127
+ cancel,
96
128
  reset,
97
129
  };
98
130
  }