@umituz/react-native-ai-generation-content 1.82.8 → 1.83.0

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-generation-content",
3
- "version": "1.82.8",
3
+ "version": "1.83.0",
4
4
  "description": "Provider-agnostic AI generation orchestration for React Native with result preview components",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -104,6 +104,13 @@ export const executeQueuedJob = async <TInput, TResult>(
104
104
  await executor.onError?.(failedJob, error instanceof Error ? error : new Error(errorMsg));
105
105
  onJobError?.(failedJob);
106
106
  }
107
+
108
+ // Remove failed job from cache to prevent accumulation
109
+ try {
110
+ await removeJobAsync(jobId);
111
+ } catch {
112
+ // Best effort cleanup
113
+ }
107
114
  } finally {
108
115
  // Use atomic Set operation to prevent race conditions
109
116
  activeJobsRef.current.delete(jobId);
@@ -81,8 +81,13 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
81
81
  activeJobsRef.current.delete(id);
82
82
  jobInputsRef.current.delete(id);
83
83
  removeJob(id);
84
+
85
+ // Trigger onAllComplete if no more active jobs after cancel
86
+ if (activeJobsRef.current.size === 0) {
87
+ onAllComplete?.();
88
+ }
84
89
  },
85
- [removeJob],
90
+ [removeJob, onAllComplete],
86
91
  );
87
92
 
88
93
  // Calculate active jobs from TanStack Query state (not ref) for reactivity
@@ -121,14 +121,14 @@ export const usePhotoUploadState = ({
121
121
  }
122
122
 
123
123
  if (result.error === MediaValidationError.FILE_TOO_LARGE) {
124
- onError?.({
125
- title: translations.fileTooLarge,
126
- message: translations.maxFileSize.replace("{size}", maxFileSizeMB.toString()),
124
+ onErrorRef.current?.({
125
+ title: translationsRef.current.fileTooLarge,
126
+ message: translationsRef.current.maxFileSize.replace("{size}", maxFileSizeMB.toString()),
127
127
  });
128
128
  } else if (result.error === MediaValidationError.PERMISSION_DENIED) {
129
- onError?.({
130
- title: translations.error,
131
- message: translations.permissionDenied ?? "Permission to access media library is required",
129
+ onErrorRef.current?.({
130
+ title: translationsRef.current.error,
131
+ message: translationsRef.current.permissionDenied ?? "Permission to access media library is required",
132
132
  });
133
133
  }
134
134
  return;
@@ -168,12 +168,12 @@ export const usePhotoUploadState = ({
168
168
  if (typeof __DEV__ !== "undefined" && __DEV__) {
169
169
  console.error("[usePhotoUploadState] Error picking image", error);
170
170
  }
171
- onError?.({
172
- title: translations.error,
173
- message: translations.uploadFailed,
171
+ onErrorRef.current?.({
172
+ title: translationsRef.current.error,
173
+ message: translationsRef.current.uploadFailed,
174
174
  });
175
175
  }
176
- }, [pickImage, maxFileSizeMB, translations, onError]);
176
+ }, [pickImage, maxFileSizeMB]);
177
177
 
178
178
  const canContinue = image !== null && !isLoading;
179
179
 
@@ -6,7 +6,10 @@ import { useEffect, useRef, useCallback, useState } from "react";
6
6
 
7
7
  declare const __DEV__: boolean;
8
8
  import { pollQueueStatus } from "./videoQueuePoller";
9
- import { DEFAULT_POLL_INTERVAL_MS } from "../../../../../infrastructure/constants/polling.constants";
9
+ import {
10
+ DEFAULT_POLL_INTERVAL_MS,
11
+ DEFAULT_MAX_POLL_TIME_MS,
12
+ } from "../../../../../infrastructure/constants/polling.constants";
10
13
  import type { GenerationUrls } from "./generation-result.utils";
11
14
  import type {
12
15
  UseVideoQueueGenerationProps,
@@ -23,6 +26,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
23
26
  const isGeneratingRef = useRef(false);
24
27
  const isPollingRef = useRef(false);
25
28
  const consecutiveErrorsRef = useRef(0);
29
+ const pollStartTimeRef = useRef<number | null>(null);
26
30
  const [isGenerating, setIsGenerating] = useState(false);
27
31
 
28
32
  const clearPolling = useCallback(() => {
@@ -38,6 +42,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
38
42
  isGeneratingRef.current = false;
39
43
  isPollingRef.current = false;
40
44
  consecutiveErrorsRef.current = 0;
45
+ pollStartTimeRef.current = null;
41
46
  creationIdRef.current = null;
42
47
  requestIdRef.current = null;
43
48
  modelRef.current = null;
@@ -53,12 +58,12 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
53
58
  isGeneratingRef.current = false;
54
59
  isPollingRef.current = false;
55
60
  consecutiveErrorsRef.current = 0;
61
+ pollStartTimeRef.current = null;
56
62
  setIsGenerating(false);
57
63
  }, [clearPolling]);
58
64
 
59
65
  const handleComplete = useCallback(
60
66
  async (urls: GenerationUrls) => {
61
- // Stop polling immediately on completion
62
67
  clearPolling();
63
68
 
64
69
  const creationId = creationIdRef.current;
@@ -74,7 +79,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
74
79
  });
75
80
  }
76
81
 
77
- // Validate non-empty URI
78
82
  if (!creationId || !userId || !uri || uri.trim() === "") {
79
83
  if (typeof __DEV__ !== "undefined" && __DEV__) {
80
84
  console.error("[VideoQueue] ❌ Invalid completion data:", { creationId, userId, uri });
@@ -106,8 +110,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
106
110
 
107
111
  resetRefs();
108
112
 
109
- // Still call onSuccess even if persistence failed - the generation itself succeeded
110
- // The video/image URL is valid, user should still see the result
111
113
  if (typeof __DEV__ !== "undefined" && __DEV__) {
112
114
  console.log("[VideoQueue] 🎯 Calling onSuccess callback now...", { persistenceSucceeded });
113
115
  }
@@ -139,11 +141,30 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
139
141
  [userId, persistence, onError, resetRefs, clearPolling],
140
142
  );
141
143
 
144
+ // Use a ref to hold the latest handleComplete/handleError to avoid stale closures
145
+ // in the setInterval callback
146
+ const handleCompleteRef = useRef(handleComplete);
147
+ const handleErrorRef = useRef(handleError);
148
+ useEffect(() => { handleCompleteRef.current = handleComplete; }, [handleComplete]);
149
+ useEffect(() => { handleErrorRef.current = handleError; }, [handleError]);
150
+
142
151
  const pollStatus = useCallback(async () => {
143
152
  const requestId = requestIdRef.current;
144
153
  const model = modelRef.current;
145
154
  if (!requestId || !model) return;
146
155
 
156
+ // Check max poll time
157
+ if (pollStartTimeRef.current !== null) {
158
+ const elapsed = Date.now() - pollStartTimeRef.current;
159
+ if (elapsed >= DEFAULT_MAX_POLL_TIME_MS) {
160
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
161
+ console.warn("[VideoQueue] ⏰ Max poll time exceeded, aborting");
162
+ }
163
+ await handleErrorRef.current("Generation timed out. Please try again.");
164
+ return;
165
+ }
166
+ }
167
+
147
168
  try {
148
169
  await pollQueueStatus({
149
170
  requestId,
@@ -151,15 +172,19 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
151
172
  isPollingRef,
152
173
  pollingRef,
153
174
  consecutiveErrorsRef,
154
- onComplete: handleComplete,
155
- onError: handleError,
175
+ onComplete: handleCompleteRef.current,
176
+ onError: handleErrorRef.current,
156
177
  });
157
178
  } catch (error) {
158
179
  if (typeof __DEV__ !== "undefined" && __DEV__) {
159
180
  console.error("[VideoQueue] Unexpected poll error:", error);
160
181
  }
161
182
  }
162
- }, [handleComplete, handleError]);
183
+ }, []);
184
+
185
+ // Keep a stable ref to pollStatus for the setInterval closure
186
+ const pollStatusRef = useRef(pollStatus);
187
+ useEffect(() => { pollStatusRef.current = pollStatus; }, [pollStatus]);
163
188
 
164
189
  const startGeneration = useCallback(
165
190
  async (input: unknown, prompt: string) => {
@@ -175,7 +200,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
175
200
  let creationId: string | null = null;
176
201
  if (userId && prompt) {
177
202
  try {
178
- // Extract generation parameters from input
179
203
  const inputData = input as Record<string, unknown>;
180
204
  const duration = typeof inputData?.duration === "number" ? inputData.duration : undefined;
181
205
  const resolution = typeof inputData?.resolution === "string" ? inputData.resolution : undefined;
@@ -197,8 +221,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
197
221
  if (typeof __DEV__ !== "undefined" && __DEV__) {
198
222
  console.error("[VideoQueue] Failed to save processing creation:", error);
199
223
  }
200
- // Continue without creation tracking - generation can still proceed
201
- // The video will be generated but won't appear in gallery history
202
224
  }
203
225
  }
204
226
 
@@ -206,7 +228,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
206
228
  try {
207
229
  queueResult = await strategy.submitToQueue(input);
208
230
  } catch (error) {
209
- // Queue submission threw - reset state and report error
210
231
  if (creationId && userId) {
211
232
  try {
212
233
  await persistence.updateToFailed(userId, creationId, error instanceof Error ? error.message : "Queue submission failed");
@@ -243,10 +264,12 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
243
264
  }
244
265
  }
245
266
 
246
- pollingRef.current = setInterval(() => void pollStatus(), DEFAULT_POLL_INTERVAL_MS);
247
- void pollStatus();
267
+ // Start polling: record start time, then poll immediately + on interval
268
+ pollStartTimeRef.current = Date.now();
269
+ pollingRef.current = setInterval(() => void pollStatusRef.current(), DEFAULT_POLL_INTERVAL_MS);
270
+ void pollStatusRef.current();
248
271
  },
249
- [userId, scenario, persistence, strategy, creditCost, pollStatus, onError],
272
+ [userId, scenario, persistence, strategy, creditCost, onError],
250
273
  );
251
274
 
252
275
  return { isGenerating, startGeneration };
@@ -1,12 +1,10 @@
1
1
  import { providerRegistry } from "../../../../../infrastructure/services/provider-registry.service";
2
2
  import { extractResultUrl, type GenerationUrls, type GenerationResult } from "./generation-result.utils";
3
3
  import { QUEUE_STATUS } from "../../../../../domain/constants/queue-status.constants";
4
+ import { DEFAULT_MAX_CONSECUTIVE_ERRORS } from "../../../../../infrastructure/constants/polling.constants";
4
5
 
5
6
  declare const __DEV__: boolean;
6
7
 
7
- /** Max consecutive transient errors before aborting */
8
- const MAX_CONSECUTIVE_ERRORS = 5;
9
-
10
8
  /**
11
9
  * Extract meaningful error message from various error formats.
12
10
  * Fal AI client throws ValidationError with empty .message but details in .body/.detail
@@ -14,12 +12,8 @@ const MAX_CONSECUTIVE_ERRORS = 5;
14
12
  function extractErrorMessage(err: unknown): string {
15
13
  if (!err) return "Generation failed";
16
14
 
17
- // Standard Error with message
18
- if (err instanceof Error && err.message && err.message.length > 0) {
19
- return err.message;
20
- }
21
-
22
- // Fal AI ValidationError - has .body.detail array
15
+ // Fal AI ValidationError - has .body.detail array (check before instanceof Error
16
+ // because ValidationError may extend Error with empty .message)
23
17
  const errObj = err as Record<string, unknown>;
24
18
  if (errObj.body && typeof errObj.body === "object") {
25
19
  const body = errObj.body as Record<string, unknown>;
@@ -35,6 +29,11 @@ function extractErrorMessage(err: unknown): string {
35
29
  if (first?.msg) return first.msg;
36
30
  }
37
31
 
32
+ // Standard Error with message
33
+ if (err instanceof Error && err.message && err.message.length > 0) {
34
+ return err.message;
35
+ }
36
+
38
37
  // Fallback to string conversion
39
38
  const str = String(err);
40
39
  return str.length > 0 && str !== "[object Object]" ? str : "Generation failed";
@@ -96,15 +95,21 @@ export const pollQueueStatus = async (params: PollParams): Promise<void> => {
96
95
  await onError(errorMessage);
97
96
  }
98
97
  } else {
99
- await onError("Generation failed");
98
+ // Try to extract error from FAL job logs (error-level log takes priority)
99
+ const logs = status.logs ?? [];
100
+ const errorLog = logs.findLast?.((l) => l.level === "error") ?? logs[logs.length - 1];
101
+ const failMessage =
102
+ errorLog?.message && errorLog.message !== "[object Object]"
103
+ ? errorLog.message
104
+ : "Generation failed";
105
+ await onError(failMessage);
100
106
  }
101
107
  }
102
108
  } catch (err) {
103
109
  consecutiveErrorsRef.current += 1;
104
- const errorMessage = err instanceof Error ? err.message : "Generation failed";
110
+ const errorMessage = extractErrorMessage(err);
105
111
 
106
- if (consecutiveErrorsRef.current >= MAX_CONSECUTIVE_ERRORS) {
107
- // Too many consecutive errors - abort
112
+ if (consecutiveErrorsRef.current >= DEFAULT_MAX_CONSECUTIVE_ERRORS) {
108
113
  if (pollingRef.current) {
109
114
  clearInterval(pollingRef.current);
110
115
  pollingRef.current = null;
@@ -112,10 +117,9 @@ export const pollQueueStatus = async (params: PollParams): Promise<void> => {
112
117
  if (__DEV__) console.error("[VideoQueueGeneration] Max consecutive errors reached, aborting:", errorMessage);
113
118
  await onError(errorMessage);
114
119
  } else {
115
- // Transient error - continue polling
116
120
  if (__DEV__) {
117
121
  console.warn(
118
- `[VideoQueueGeneration] Transient poll error (${consecutiveErrorsRef.current}/${MAX_CONSECUTIVE_ERRORS}):`,
122
+ `[VideoQueueGeneration] Transient poll error (${consecutiveErrorsRef.current}/${DEFAULT_MAX_CONSECUTIVE_ERRORS}):`,
119
123
  errorMessage,
120
124
  );
121
125
  }