@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 +1 -1
- package/src/domains/background/presentation/hooks/use-pending-jobs.ts +8 -1
- package/src/domains/creations/presentation/hooks/useCreationPersistence.ts +0 -3
- package/src/domains/creations/presentation/hooks/useCreations.ts +19 -19
- package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +37 -4
- package/src/domains/generation/infrastructure/flow/useFlowStore.ts +19 -9
- 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/src/infrastructure/validation/base-validator.ts +4 -0
- package/src/presentation/hooks/flow-state.utils.ts +6 -0
- package/src/presentation/hooks/use-generation.ts +3 -0
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
|
});
|
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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: { ...
|
|
97
|
+
set((state) => ({ partners: { ...state.partners, [partnerId]: image } })),
|
|
90
98
|
|
|
91
99
|
setPartnerName: (partnerId: string, name: string) =>
|
|
92
|
-
set({ partnerNames: { ...
|
|
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: { ...
|
|
106
|
+
set((state) => ({ selectedFeatures: { ...state.selectedFeatures, [featureType]: ids } })),
|
|
99
107
|
|
|
100
108
|
setCustomData: (key: string, value: unknown) =>
|
|
101
|
-
set({ customData: { ...
|
|
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:
|
|
109
|
-
generationStatus:
|
|
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 >=
|
|
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();
|
|
@@ -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
|
},
|