@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 +1 -1
- package/src/domains/background/infrastructure/executors/backgroundJobExecutor.ts +7 -0
- package/src/domains/background/presentation/hooks/use-background-generation.ts +6 -1
- package/src/domains/generation/wizard/presentation/hooks/usePhotoUploadState.ts +10 -10
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +38 -15
- package/src/domains/generation/wizard/presentation/hooks/videoQueuePoller.ts +19 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
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
|
-
|
|
125
|
-
title:
|
|
126
|
-
message:
|
|
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
|
-
|
|
130
|
-
title:
|
|
131
|
-
message:
|
|
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
|
-
|
|
172
|
-
title:
|
|
173
|
-
message:
|
|
171
|
+
onErrorRef.current?.({
|
|
172
|
+
title: translationsRef.current.error,
|
|
173
|
+
message: translationsRef.current.uploadFailed,
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
|
-
}, [pickImage, maxFileSizeMB
|
|
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 {
|
|
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:
|
|
155
|
-
onError:
|
|
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
|
-
}, [
|
|
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
|
-
|
|
247
|
-
|
|
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,
|
|
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
|
-
//
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
110
|
+
const errorMessage = extractErrorMessage(err);
|
|
105
111
|
|
|
106
|
-
if (consecutiveErrorsRef.current >=
|
|
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}/${
|
|
122
|
+
`[VideoQueueGeneration] Transient poll error (${consecutiveErrorsRef.current}/${DEFAULT_MAX_CONSECUTIVE_ERRORS}):`,
|
|
119
123
|
errorMessage,
|
|
120
124
|
);
|
|
121
125
|
}
|