@umituz/react-native-ai-generation-content 1.72.7 → 1.72.8
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/presentation/hooks/use-pending-jobs.ts +8 -1
- package/src/domains/creations/presentation/hooks/useCreations.ts +19 -19
- package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +15 -1
- package/src/domains/generation/wizard/presentation/hooks/useGenerationPhase.ts +8 -2
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +19 -2
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +11 -4
- package/src/domains/generation/wizard/presentation/hooks/videoQueuePoller.ts +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.72.
|
|
3
|
+
"version": "1.72.8",
|
|
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",
|
|
@@ -59,7 +59,14 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
|
|
|
59
59
|
onSuccess: (newJob: BackgroundJob<TInput, TResult>) => {
|
|
60
60
|
queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
|
|
61
61
|
queryKey,
|
|
62
|
-
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
62
|
+
(old: BackgroundJob<TInput, TResult>[] | undefined) => {
|
|
63
|
+
// Check if job already exists to prevent duplicates
|
|
64
|
+
const exists = old?.some((job) => job.id === newJob.id);
|
|
65
|
+
if (exists) {
|
|
66
|
+
return old ?? [];
|
|
67
|
+
}
|
|
68
|
+
return [newJob, ...(old ?? [])];
|
|
69
|
+
},
|
|
63
70
|
);
|
|
64
71
|
},
|
|
65
72
|
});
|
|
@@ -38,6 +38,23 @@ export function useCreations({
|
|
|
38
38
|
}
|
|
39
39
|
}, []);
|
|
40
40
|
|
|
41
|
+
const onDataCallback = useCallback((creations: Creation[]) => {
|
|
42
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
43
|
+
console.log("[useCreations] Realtime update:", creations.length);
|
|
44
|
+
}
|
|
45
|
+
setData(creations);
|
|
46
|
+
setIsLoading(false);
|
|
47
|
+
setError(null);
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const onErrorCallback = useCallback((err: Error) => {
|
|
51
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
52
|
+
console.error("[useCreations] Realtime listener error:", err);
|
|
53
|
+
}
|
|
54
|
+
setError(err);
|
|
55
|
+
setIsLoading(false);
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
41
58
|
useEffect(() => {
|
|
42
59
|
if (!userId || !enabled) {
|
|
43
60
|
setData([]);
|
|
@@ -52,24 +69,7 @@ export function useCreations({
|
|
|
52
69
|
setIsLoading(true);
|
|
53
70
|
setError(null);
|
|
54
71
|
|
|
55
|
-
const unsubscribe = repository.subscribeToAll(
|
|
56
|
-
userId,
|
|
57
|
-
(creations) => {
|
|
58
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
59
|
-
console.log("[useCreations] Realtime update:", creations.length);
|
|
60
|
-
}
|
|
61
|
-
setData(creations);
|
|
62
|
-
setIsLoading(false);
|
|
63
|
-
setError(null);
|
|
64
|
-
},
|
|
65
|
-
(err) => {
|
|
66
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
67
|
-
console.error("[useCreations] Realtime listener error:", err);
|
|
68
|
-
}
|
|
69
|
-
setError(err);
|
|
70
|
-
setIsLoading(false);
|
|
71
|
-
},
|
|
72
|
-
);
|
|
72
|
+
const unsubscribe = repository.subscribeToAll(userId, onDataCallback, onErrorCallback);
|
|
73
73
|
|
|
74
74
|
return () => {
|
|
75
75
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -77,7 +77,7 @@ export function useCreations({
|
|
|
77
77
|
}
|
|
78
78
|
unsubscribe();
|
|
79
79
|
};
|
|
80
|
-
}, [userId, repository, enabled]);
|
|
80
|
+
}, [userId, repository, enabled, onDataCallback, onErrorCallback]);
|
|
81
81
|
|
|
82
82
|
return { data, isLoading, error, refetch };
|
|
83
83
|
}
|
|
@@ -60,8 +60,18 @@ export function useProcessingJobsPoller(
|
|
|
60
60
|
// Use ref for stable function reference to prevent effect re-runs
|
|
61
61
|
const pollJobRef = useRef<((creation: Creation) => Promise<void>) | undefined>(undefined);
|
|
62
62
|
|
|
63
|
+
// Use mounted ref to prevent operations after unmount
|
|
64
|
+
const isMountedRef = useRef(true);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
isMountedRef.current = true;
|
|
68
|
+
return () => {
|
|
69
|
+
isMountedRef.current = false;
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
63
73
|
pollJobRef.current = async (creation: Creation) => {
|
|
64
|
-
if (!userId || !creation.requestId || !creation.model) return;
|
|
74
|
+
if (!isMountedRef.current || !userId || !creation.requestId || !creation.model) return;
|
|
65
75
|
|
|
66
76
|
if (pollingRef.current.has(creation.id)) return;
|
|
67
77
|
pollingRef.current.add(creation.id);
|
|
@@ -88,6 +98,8 @@ export function useProcessingJobsPoller(
|
|
|
88
98
|
const urls = extractResultUrl(result);
|
|
89
99
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[ProcessingJobsPoller] Completed:", creation.id, urls);
|
|
90
100
|
|
|
101
|
+
if (!isMountedRef.current) return;
|
|
102
|
+
|
|
91
103
|
const uri = urls.videoUrl || urls.imageUrl || "";
|
|
92
104
|
|
|
93
105
|
// Validate that we have a valid URI before marking as completed
|
|
@@ -110,6 +122,8 @@ export function useProcessingJobsPoller(
|
|
|
110
122
|
} else if (status.status === QUEUE_STATUS.FAILED) {
|
|
111
123
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[ProcessingJobsPoller] Failed:", creation.id);
|
|
112
124
|
|
|
125
|
+
if (!isMountedRef.current) return;
|
|
126
|
+
|
|
113
127
|
await repository.update(userId, creation.id, {
|
|
114
128
|
status: CREATION_STATUS.FAILED,
|
|
115
129
|
metadata: { error: "Generation failed" },
|
|
@@ -28,6 +28,12 @@ export function useGenerationPhase(options?: UseGenerationPhaseOptions): Generat
|
|
|
28
28
|
|
|
29
29
|
const [phase, setPhase] = useState<GenerationPhase>("queued");
|
|
30
30
|
const startTimeRef = useRef<number>(Date.now());
|
|
31
|
+
const queuedDurationRef = useRef(queuedDuration);
|
|
32
|
+
|
|
33
|
+
// Only reset if duration changes significantly
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
queuedDurationRef.current = queuedDuration;
|
|
36
|
+
}, [queuedDuration]);
|
|
31
37
|
|
|
32
38
|
useEffect(() => {
|
|
33
39
|
startTimeRef.current = Date.now();
|
|
@@ -36,14 +42,14 @@ export function useGenerationPhase(options?: UseGenerationPhaseOptions): Generat
|
|
|
36
42
|
const interval = setInterval(() => {
|
|
37
43
|
const elapsed = Date.now() - startTimeRef.current;
|
|
38
44
|
|
|
39
|
-
if (elapsed >=
|
|
45
|
+
if (elapsed >= queuedDurationRef.current) {
|
|
40
46
|
setPhase("processing");
|
|
41
47
|
clearInterval(interval);
|
|
42
48
|
}
|
|
43
49
|
}, 1000);
|
|
44
50
|
|
|
45
51
|
return () => clearInterval(interval);
|
|
46
|
-
}, [
|
|
52
|
+
}, []); // Empty deps - only run once on mount
|
|
47
53
|
|
|
48
54
|
return phase;
|
|
49
55
|
}
|
|
@@ -30,6 +30,13 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
30
30
|
clearInterval(pollingRef.current);
|
|
31
31
|
pollingRef.current = null;
|
|
32
32
|
}
|
|
33
|
+
// Reset all refs on unmount
|
|
34
|
+
isGeneratingRef.current = false;
|
|
35
|
+
isPollingRef.current = false;
|
|
36
|
+
creationIdRef.current = null;
|
|
37
|
+
requestIdRef.current = null;
|
|
38
|
+
modelRef.current = null;
|
|
39
|
+
setIsGenerating(false);
|
|
33
40
|
};
|
|
34
41
|
}, []);
|
|
35
42
|
|
|
@@ -45,10 +52,20 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
45
52
|
const handleComplete = useCallback(
|
|
46
53
|
async (urls: GenerationUrls) => {
|
|
47
54
|
const creationId = creationIdRef.current;
|
|
48
|
-
|
|
55
|
+
const uri = (urls.videoUrl || urls.imageUrl) ?? "";
|
|
56
|
+
|
|
57
|
+
// Validate non-empty URI
|
|
58
|
+
if (!creationId || !userId || !uri || uri.trim() === "") {
|
|
59
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
60
|
+
console.error("[VideoQueue] Invalid completion data:", { creationId, userId, uri });
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (creationId && userId) {
|
|
49
66
|
try {
|
|
50
67
|
await persistence.updateToCompleted(userId, creationId, {
|
|
51
|
-
uri
|
|
68
|
+
uri,
|
|
52
69
|
imageUrl: urls.imageUrl,
|
|
53
70
|
videoUrl: urls.videoUrl,
|
|
54
71
|
});
|
|
@@ -66,6 +66,15 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
|
|
|
66
66
|
onCreditsExhausted,
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
+
// Use refs for functions to avoid effect re-runs
|
|
70
|
+
const videoStartGenerationRef = useRef(videoGeneration.startGeneration);
|
|
71
|
+
const photoStartGenerationRef = useRef(photoGeneration.startGeneration);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
videoStartGenerationRef.current = videoGeneration.startGeneration;
|
|
75
|
+
photoStartGenerationRef.current = photoGeneration.startGeneration;
|
|
76
|
+
}, [videoGeneration.startGeneration, photoGeneration.startGeneration]);
|
|
77
|
+
|
|
69
78
|
useEffect(() => {
|
|
70
79
|
const isAlreadyGenerating = videoGeneration.isGenerating || photoGeneration.isGenerating;
|
|
71
80
|
|
|
@@ -80,8 +89,8 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
|
|
|
80
89
|
isMountedRef,
|
|
81
90
|
dispatch,
|
|
82
91
|
onError,
|
|
83
|
-
videoGenerationFn:
|
|
84
|
-
photoGenerationFn:
|
|
92
|
+
videoGenerationFn: videoStartGenerationRef.current,
|
|
93
|
+
photoGenerationFn: photoStartGenerationRef.current,
|
|
85
94
|
}).catch((error) => {
|
|
86
95
|
// Catch any unhandled errors from executeWizardGeneration
|
|
87
96
|
if (isMountedRef.current) {
|
|
@@ -102,9 +111,7 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
|
|
|
102
111
|
wizardData,
|
|
103
112
|
isVideoMode,
|
|
104
113
|
videoGeneration.isGenerating,
|
|
105
|
-
videoGeneration.startGeneration,
|
|
106
114
|
photoGeneration.isGenerating,
|
|
107
|
-
photoGeneration.startGeneration,
|
|
108
115
|
onError,
|
|
109
116
|
]);
|
|
110
117
|
|
|
@@ -16,8 +16,12 @@ interface PollParams {
|
|
|
16
16
|
export const pollQueueStatus = async (params: PollParams): Promise<void> => {
|
|
17
17
|
const { requestId, model, isPollingRef, pollingRef, onComplete, onError } = params;
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
19
|
+
// Check-and-set - while not truly atomic in JS, this is best we can do
|
|
20
|
+
// The ref prevents most race conditions in practice
|
|
21
|
+
if (isPollingRef.current) {
|
|
22
|
+
if (__DEV__) console.log("[VideoQueuePoller] Already polling, skipping");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
21
25
|
isPollingRef.current = true;
|
|
22
26
|
|
|
23
27
|
const provider = providerRegistry.getActiveProvider();
|