@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.72.7",
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) => [newJob, ...(old ?? [])],
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 >= queuedDuration) {
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
- }, [queuedDuration]);
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
- if (creationId && userId && (urls.videoUrl || urls.imageUrl)) {
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: (urls.videoUrl || urls.imageUrl) ?? "",
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: videoGeneration.startGeneration,
84
- photoGenerationFn: photoGeneration.startGeneration,
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
- // Atomic check-and-set to prevent race condition
20
- if (isPollingRef.current) return;
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();