@umituz/react-native-ai-generation-content 1.72.7 → 1.72.9

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.9",
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",
@@ -71,7 +71,7 @@
71
71
  "@typescript-eslint/parser": "^8.54.0",
72
72
  "@umituz/react-native-design-system": "latest",
73
73
  "@umituz/react-native-firebase": "latest",
74
- "@umituz/react-native-subscription": "latest",
74
+ "@umituz/react-native-subscription": "^2.35.3",
75
75
  "eslint": "^9.39.2",
76
76
  "eslint-plugin-react": "^7.37.5",
77
77
  "eslint-plugin-react-hooks": "^7.0.1",
@@ -79,7 +79,7 @@ export function useAIFeatureGate(options: AIFeatureGateOptions): AIFeatureGateRe
79
79
  );
80
80
 
81
81
  const requireFeature = useCallback(
82
- (action: () => void | Promise<void>): void => {
82
+ (action: () => void | Promise<void>): boolean => {
83
83
  if (typeof __DEV__ !== "undefined" && __DEV__) {
84
84
  console.log("[AIFeatureGate] requireFeature called:", {
85
85
  isOffline,
@@ -97,26 +97,28 @@ export function useAIFeatureGate(options: AIFeatureGateOptions): AIFeatureGateRe
97
97
  console.log("[AIFeatureGate] BLOCKED: User is offline");
98
98
  }
99
99
  onNetworkError?.();
100
- return;
100
+ return false;
101
101
  }
102
102
 
103
103
  if (isAuthenticated && !isCreditsLoaded) {
104
104
  if (typeof __DEV__ !== "undefined" && __DEV__) {
105
105
  console.log("[AIFeatureGate] BLOCKED: User authenticated but credits not loaded yet");
106
106
  }
107
- return;
107
+ return false;
108
108
  }
109
109
 
110
110
  if (typeof __DEV__ !== "undefined" && __DEV__) {
111
111
  console.log("[AIFeatureGate] Calling requireFeatureFromPackage");
112
112
  }
113
113
 
114
- requireFeatureFromPackage(() => {
114
+ const executed = requireFeatureFromPackage(() => {
115
115
  if (typeof __DEV__ !== "undefined" && __DEV__) {
116
116
  console.log("[AIFeatureGate] Inside requireFeatureFromPackage callback - executing action");
117
117
  }
118
118
  handlePromiseResult(action(), onSuccess, onError);
119
119
  });
120
+
121
+ return executed;
120
122
  },
121
123
  [isOffline, isAuthenticated, isCreditsLoaded, isPremium, creditBalance, creditCost, hasCredits, onNetworkError, requireFeatureFromPackage, onSuccess, onError],
122
124
  );
@@ -34,8 +34,9 @@ export interface AIFeatureGateReturn {
34
34
  /**
35
35
  * Function to execute protected feature
36
36
  * Handles all access control checks (auth, premium, credits, paywall)
37
+ * @returns true if action was executed, false if blocked by gate
37
38
  */
38
- requireFeature: (action: () => void | Promise<void>) => void;
39
+ requireFeature: (action: () => void | Promise<void>) => boolean;
39
40
 
40
41
  /**
41
42
  * Whether user can access this feature (all checks passed)
@@ -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();