@umituz/react-native-ai-generation-content 1.72.6 → 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.6",
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
  });
@@ -71,12 +71,9 @@ export function useCreationPersistence(
71
71
 
72
72
  if (creditCost && onCreditDeduct) {
73
73
  onCreditDeduct(creditCost).catch((error) => {
74
- // Log credit deduction errors for debugging
75
74
  if (typeof __DEV__ !== "undefined" && __DEV__) {
76
75
  console.error("[CreationPersistence] Credit deduction failed:", error);
77
76
  }
78
- // Don't throw - credit deduction failure shouldn't block completion
79
- // But we should notify user or retry in production
80
77
  });
81
78
  }
82
79
  },
@@ -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,7 +98,22 @@ 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 || "";
104
+
105
+ // Validate that we have a valid URI before marking as completed
106
+ if (!uri || uri.trim() === "") {
107
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
108
+ console.error("[ProcessingJobsPoller] No valid URI in result:", creation.id);
109
+ }
110
+ await repository.update(userId, creation.id, {
111
+ status: CREATION_STATUS.FAILED,
112
+ metadata: { error: "No valid result URL received" },
113
+ });
114
+ return;
115
+ }
116
+
92
117
  await repository.update(userId, creation.id, {
93
118
  status: CREATION_STATUS.COMPLETED,
94
119
  uri,
@@ -97,6 +122,8 @@ export function useProcessingJobsPoller(
97
122
  } else if (status.status === QUEUE_STATUS.FAILED) {
98
123
  if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[ProcessingJobsPoller] Failed:", creation.id);
99
124
 
125
+ if (!isMountedRef.current) return;
126
+
100
127
  await repository.update(userId, creation.id, {
101
128
  status: CREATION_STATUS.FAILED,
102
129
  metadata: { error: "Generation failed" },
@@ -111,6 +138,12 @@ export function useProcessingJobsPoller(
111
138
  }
112
139
  };
113
140
 
141
+ // Use ref to always get latest creations
142
+ const creationsRef = useRef(creations);
143
+ useEffect(() => {
144
+ creationsRef.current = creations;
145
+ }, [creations]);
146
+
114
147
  useEffect(() => {
115
148
  if (!enabled || !userId || processingJobIds.length === 0) {
116
149
  if (intervalRef.current) {
@@ -120,9 +153,9 @@ export function useProcessingJobsPoller(
120
153
  return;
121
154
  }
122
155
 
123
- // Get current jobs at poll time to avoid stale closures
156
+ // Get current jobs at poll time from ref to avoid stale closures
124
157
  const pollCurrentJobs = () => {
125
- const currentJobs = creations.filter(
158
+ const currentJobs = creationsRef.current.filter(
126
159
  (c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
127
160
  );
128
161
  currentJobs.forEach((job) => pollJobRef.current?.(job));
@@ -144,7 +177,7 @@ export function useProcessingJobsPoller(
144
177
  intervalRef.current = null;
145
178
  }
146
179
  };
147
- }, [enabled, userId, processingJobIds, creations]);
180
+ }, [enabled, userId, processingJobIds]);
148
181
 
149
182
  return {
150
183
  processingCount: processingJobs.length,
@@ -3,6 +3,8 @@
3
3
  */
4
4
 
5
5
  import { createStore } from "@umituz/react-native-design-system";
6
+
7
+ declare const __DEV__: boolean;
6
8
  import type {
7
9
  FlowState,
8
10
  FlowActions,
@@ -34,7 +36,13 @@ export const createFlowStore = (config: FlowStoreConfig) => {
34
36
  if (config.initialStepIndex !== undefined) {
35
37
  initialIndex = Math.max(0, Math.min(config.initialStepIndex, config.steps.length - 1));
36
38
  } else if (config.initialStepId) {
37
- initialIndex = Math.max(0, config.steps.findIndex((s) => s.id === config.initialStepId));
39
+ const foundIndex = config.steps.findIndex((s) => s.id === config.initialStepId);
40
+ if (foundIndex === -1) {
41
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
42
+ console.warn(`[FlowStore] Step "${config.initialStepId}" not found, using first step`);
43
+ }
44
+ }
45
+ initialIndex = Math.max(0, foundIndex);
38
46
  }
39
47
 
40
48
  const initialStepId = config.steps[initialIndex]?.id ?? "";
@@ -86,28 +94,30 @@ export const createFlowStore = (config: FlowStoreConfig) => {
86
94
  setScenario: (scenario: unknown) => set({ selectedScenario: scenario }),
87
95
 
88
96
  setPartnerImage: (partnerId: string, image: FlowUploadedImageData | undefined) =>
89
- set({ partners: { ...get().partners, [partnerId]: image } }),
97
+ set((state) => ({ partners: { ...state.partners, [partnerId]: image } })),
90
98
 
91
99
  setPartnerName: (partnerId: string, name: string) =>
92
- set({ partnerNames: { ...get().partnerNames, [partnerId]: name } }),
100
+ set((state) => ({ partnerNames: { ...state.partnerNames, [partnerId]: name } })),
93
101
 
94
102
  setTextInput: (text: string) => set({ textInput: text }),
95
103
  setVisualStyle: (styleId: string) => set({ visualStyle: styleId }),
96
104
 
97
105
  setSelectedFeatures: (featureType: string, ids: readonly string[]) =>
98
- set({ selectedFeatures: { ...get().selectedFeatures, [featureType]: ids } }),
106
+ set((state) => ({ selectedFeatures: { ...state.selectedFeatures, [featureType]: ids } })),
99
107
 
100
108
  setCustomData: (key: string, value: unknown) =>
101
- set({ customData: { ...get().customData, [key]: value } }),
109
+ set((state) => ({ customData: { ...state.customData, [key]: value } })),
102
110
 
103
111
  startGeneration: () =>
104
112
  set({ generationStatus: "preparing", generationProgress: 0, generationError: undefined }),
105
113
 
106
- updateProgress: (progress: number) =>
114
+ updateProgress: (progress: number) => {
115
+ const validProgress = Math.max(0, Math.min(100, progress));
107
116
  set({
108
- generationProgress: progress,
109
- generationStatus: progress >= 100 ? "completed" : progress > 0 ? "generating" : "preparing",
110
- }),
117
+ generationProgress: validProgress,
118
+ generationStatus: validProgress >= 100 ? "completed" : validProgress > 0 ? "generating" : "preparing",
119
+ });
120
+ },
111
121
 
112
122
  setResult: (result: unknown) =>
113
123
  set({ generationResult: result, generationStatus: "completed", generationProgress: 100 }),
@@ -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();
@@ -136,6 +136,10 @@ export function validateBase64(input: unknown): ValidationResult {
136
136
  return { isValid: false, errors: ["Input must be a string"] };
137
137
  }
138
138
 
139
+ if (input.length === 0) {
140
+ return { isValid: false, errors: ["Base64 string cannot be empty"] };
141
+ }
142
+
139
143
  const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
140
144
  if (!base64Regex.test(input)) {
141
145
  return { isValid: false, errors: ["Invalid base64 format"] };
@@ -48,6 +48,12 @@ export function updatePhotoStep(
48
48
  stepIndex: number,
49
49
  updates: Partial<PhotoStepData>,
50
50
  ): GenerationFlowState {
51
+ // Validate bounds to prevent sparse arrays and undefined access
52
+ if (stepIndex < 0 || stepIndex >= state.photoSteps.length) {
53
+ console.warn(`[FlowState] Invalid stepIndex ${stepIndex}, ignoring update`);
54
+ return state;
55
+ }
56
+
51
57
  const newPhotoSteps = [...state.photoSteps];
52
58
  newPhotoSteps[stepIndex] = {
53
59
  ...newPhotoSteps[stepIndex],
@@ -109,6 +109,9 @@ export function useGeneration<T = unknown>(
109
109
  if (!abortRef.current && !abortControllerRef.current?.signal.aborted) {
110
110
  setIsGenerating(false);
111
111
  }
112
+ if (abortControllerRef.current && !abortControllerRef.current.signal.aborted) {
113
+ abortControllerRef.current.abort();
114
+ }
112
115
  abortControllerRef.current = null;
113
116
  }
114
117
  },