@umituz/react-native-ai-generation-content 1.72.5 → 1.72.7
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/infrastructure/services/job-poller.service.ts +20 -0
- package/src/domains/background/presentation/hooks/use-pending-jobs.ts +6 -3
- package/src/domains/creations/presentation/hooks/useCreationPersistence.ts +7 -1
- package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +39 -6
- package/src/domains/generation/infrastructure/flow/useFlowStore.ts +19 -9
- package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts +8 -5
- package/src/domains/generation/wizard/infrastructure/strategies/shared/photo-extraction.utils.ts +20 -2
- package/src/domains/generation/wizard/presentation/hooks/generationExecutor.ts +6 -2
- package/src/domains/generation/wizard/presentation/hooks/usePhotoUploadState.ts +21 -7
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +22 -4
- package/src/domains/generation/wizard/presentation/hooks/useWizardGeneration.ts +9 -1
- package/src/domains/generation/wizard/presentation/hooks/videoQueuePoller.ts +6 -3
- package/src/domains/image-to-video/presentation/hooks/imageToVideoStrategy.ts +7 -1
- package/src/domains/result-preview/presentation/hooks/useResultActions.ts +6 -1
- package/src/domains/text-to-video/presentation/hooks/textToVideoStrategy.ts +7 -1
- package/src/infrastructure/services/generation-orchestrator.service.ts +5 -0
- package/src/infrastructure/services/image-feature-executor.service.ts +6 -1
- package/src/infrastructure/validation/base-validator.ts +4 -0
- package/src/presentation/hooks/flow-state.utils.ts +6 -0
- package/src/presentation/hooks/generation/orchestrator.ts +19 -5
- package/src/presentation/hooks/generation-flow-navigation.ts +13 -2
- package/src/presentation/hooks/use-generation.ts +4 -1
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.7",
|
|
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",
|
|
@@ -77,6 +77,26 @@ export async function pollJob<T = unknown>(
|
|
|
77
77
|
signal,
|
|
78
78
|
} = options;
|
|
79
79
|
|
|
80
|
+
// Validate requestId early
|
|
81
|
+
if (!requestId || typeof requestId !== "string" || requestId.trim() === "") {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
error: new Error("Invalid requestId provided"),
|
|
85
|
+
attempts: 0,
|
|
86
|
+
elapsedMs: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Validate model early
|
|
91
|
+
if (!model || typeof model !== "string" || model.trim() === "") {
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
error: new Error("Invalid model provided"),
|
|
95
|
+
attempts: 0,
|
|
96
|
+
elapsedMs: 0,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
80
100
|
const pollingConfig = { ...DEFAULT_POLLING_CONFIG, ...config };
|
|
81
101
|
const { maxAttempts, maxTotalTimeMs, maxConsecutiveErrors } = pollingConfig;
|
|
82
102
|
|
|
@@ -85,7 +85,8 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
|
|
|
85
85
|
onSuccess: (id: string) => {
|
|
86
86
|
queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
|
|
87
87
|
queryKey,
|
|
88
|
-
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
88
|
+
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
89
|
+
old?.filter((job: BackgroundJob<TInput, TResult>) => job && job.id !== id) ?? [],
|
|
89
90
|
);
|
|
90
91
|
},
|
|
91
92
|
});
|
|
@@ -95,7 +96,8 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
|
|
|
95
96
|
onSuccess: () => {
|
|
96
97
|
queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
|
|
97
98
|
queryKey,
|
|
98
|
-
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
99
|
+
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
100
|
+
old?.filter((job: BackgroundJob<TInput, TResult>) => job && job.status !== "completed") ?? [],
|
|
99
101
|
);
|
|
100
102
|
},
|
|
101
103
|
});
|
|
@@ -105,7 +107,8 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
|
|
|
105
107
|
onSuccess: () => {
|
|
106
108
|
queryClient.setQueryData<BackgroundJob<TInput, TResult>[]>(
|
|
107
109
|
queryKey,
|
|
108
|
-
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
110
|
+
(old: BackgroundJob<TInput, TResult>[] | undefined) =>
|
|
111
|
+
old?.filter((job: BackgroundJob<TInput, TResult>) => job && job.status !== "failed") ?? [],
|
|
109
112
|
);
|
|
110
113
|
},
|
|
111
114
|
});
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useCallback, useMemo } from "react";
|
|
6
|
+
|
|
7
|
+
declare const __DEV__: boolean;
|
|
6
8
|
import { useAuth } from "@umituz/react-native-auth";
|
|
7
9
|
import { createCreationsRepository } from "../../infrastructure/adapters";
|
|
8
10
|
import type { Creation } from "../../domain/entities/Creation";
|
|
@@ -68,7 +70,11 @@ export function useCreationPersistence(
|
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
if (creditCost && onCreditDeduct) {
|
|
71
|
-
onCreditDeduct(creditCost).catch(() => {
|
|
73
|
+
onCreditDeduct(creditCost).catch((error) => {
|
|
74
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
75
|
+
console.error("[CreationPersistence] Credit deduction failed:", error);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
72
78
|
}
|
|
73
79
|
},
|
|
74
80
|
[userId, repository, creditCost, onCreditDeduct]
|
|
@@ -42,6 +42,14 @@ export function useProcessingJobsPoller(
|
|
|
42
42
|
const pollingRef = useRef<Set<string>>(new Set());
|
|
43
43
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
44
44
|
|
|
45
|
+
// Convert to IDs to prevent re-creating array on every render
|
|
46
|
+
const processingJobIds = useMemo(
|
|
47
|
+
() => creations
|
|
48
|
+
.filter((c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model)
|
|
49
|
+
.map((c) => c.id),
|
|
50
|
+
[creations],
|
|
51
|
+
);
|
|
52
|
+
|
|
45
53
|
const processingJobs = useMemo(
|
|
46
54
|
() => creations.filter(
|
|
47
55
|
(c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
|
|
@@ -81,6 +89,19 @@ export function useProcessingJobsPoller(
|
|
|
81
89
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[ProcessingJobsPoller] Completed:", creation.id, urls);
|
|
82
90
|
|
|
83
91
|
const uri = urls.videoUrl || urls.imageUrl || "";
|
|
92
|
+
|
|
93
|
+
// Validate that we have a valid URI before marking as completed
|
|
94
|
+
if (!uri || uri.trim() === "") {
|
|
95
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
96
|
+
console.error("[ProcessingJobsPoller] No valid URI in result:", creation.id);
|
|
97
|
+
}
|
|
98
|
+
await repository.update(userId, creation.id, {
|
|
99
|
+
status: CREATION_STATUS.FAILED,
|
|
100
|
+
metadata: { error: "No valid result URL received" },
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
84
105
|
await repository.update(userId, creation.id, {
|
|
85
106
|
status: CREATION_STATUS.COMPLETED,
|
|
86
107
|
uri,
|
|
@@ -103,8 +124,14 @@ export function useProcessingJobsPoller(
|
|
|
103
124
|
}
|
|
104
125
|
};
|
|
105
126
|
|
|
127
|
+
// Use ref to always get latest creations
|
|
128
|
+
const creationsRef = useRef(creations);
|
|
106
129
|
useEffect(() => {
|
|
107
|
-
|
|
130
|
+
creationsRef.current = creations;
|
|
131
|
+
}, [creations]);
|
|
132
|
+
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!enabled || !userId || processingJobIds.length === 0) {
|
|
108
135
|
if (intervalRef.current) {
|
|
109
136
|
clearInterval(intervalRef.current);
|
|
110
137
|
intervalRef.current = null;
|
|
@@ -112,13 +139,19 @@ export function useProcessingJobsPoller(
|
|
|
112
139
|
return;
|
|
113
140
|
}
|
|
114
141
|
|
|
142
|
+
// Get current jobs at poll time from ref to avoid stale closures
|
|
143
|
+
const pollCurrentJobs = () => {
|
|
144
|
+
const currentJobs = creationsRef.current.filter(
|
|
145
|
+
(c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
|
|
146
|
+
);
|
|
147
|
+
currentJobs.forEach((job) => pollJobRef.current?.(job));
|
|
148
|
+
};
|
|
149
|
+
|
|
115
150
|
// Initial poll
|
|
116
|
-
|
|
151
|
+
pollCurrentJobs();
|
|
117
152
|
|
|
118
153
|
// Set up interval polling
|
|
119
|
-
intervalRef.current = setInterval(
|
|
120
|
-
processingJobs.forEach((job) => pollJobRef.current?.(job));
|
|
121
|
-
}, DEFAULT_POLL_INTERVAL_MS);
|
|
154
|
+
intervalRef.current = setInterval(pollCurrentJobs, DEFAULT_POLL_INTERVAL_MS);
|
|
122
155
|
|
|
123
156
|
return () => {
|
|
124
157
|
// Clear polling set first to prevent new operations
|
|
@@ -130,7 +163,7 @@ export function useProcessingJobsPoller(
|
|
|
130
163
|
intervalRef.current = null;
|
|
131
164
|
}
|
|
132
165
|
};
|
|
133
|
-
}, [enabled, userId,
|
|
166
|
+
}, [enabled, userId, processingJobIds]);
|
|
134
167
|
|
|
135
168
|
return {
|
|
136
169
|
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 }),
|
package/src/domains/generation/wizard/infrastructure/strategies/image-generation.strategy.ts
CHANGED
|
@@ -83,14 +83,17 @@ function applyStyleEnhancements(prompt: string, wizardData: Record<string, unkno
|
|
|
83
83
|
export function createImageStrategy(options: CreateImageStrategyOptions): WizardStrategy {
|
|
84
84
|
const { scenario, creditCost } = options;
|
|
85
85
|
|
|
86
|
+
// Validate model early - fail fast
|
|
87
|
+
if (!scenario.model) {
|
|
88
|
+
throw new Error("Model is required for image generation");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const model = scenario.model;
|
|
92
|
+
|
|
86
93
|
return {
|
|
87
94
|
execute: async (input: unknown) => {
|
|
88
95
|
const imageInput = input as WizardImageInput;
|
|
89
|
-
|
|
90
|
-
throw new Error("Model is required for image generation");
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const result = await executeImageGeneration(imageInput, scenario.model);
|
|
96
|
+
const result = await executeImageGeneration(imageInput, model);
|
|
94
97
|
|
|
95
98
|
if (!result.success || !result.imageUrl) {
|
|
96
99
|
throw new Error(result.error || "Image generation failed");
|
package/src/domains/generation/wizard/infrastructure/strategies/shared/photo-extraction.utils.ts
CHANGED
|
@@ -55,13 +55,31 @@ export async function extractPhotosAsBase64(
|
|
|
55
55
|
return [];
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
const
|
|
58
|
+
// Use Promise.allSettled to handle individual failures gracefully
|
|
59
|
+
const results = await Promise.allSettled(
|
|
60
|
+
photoUris.map(async (uri, index) => {
|
|
61
|
+
try {
|
|
62
|
+
return await readFileAsBase64(uri);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
65
|
+
console.error(`[PhotoExtraction] Failed to read photo ${index}:`, error);
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Extract successful results only
|
|
73
|
+
const validPhotos = results
|
|
74
|
+
.map((result) => (result.status === "fulfilled" ? result.value : null))
|
|
75
|
+
.filter((photo): photo is string => typeof photo === "string" && photo.length > 0);
|
|
60
76
|
|
|
61
77
|
if (enableDebugLogs && typeof __DEV__ !== "undefined" && __DEV__) {
|
|
78
|
+
const failedCount = results.filter((r) => r.status === "rejected").length;
|
|
62
79
|
console.log("[PhotoExtraction] Converted photos", {
|
|
63
80
|
total: photoUris.length,
|
|
64
81
|
valid: validPhotos.length,
|
|
82
|
+
failed: failedCount,
|
|
65
83
|
sizes: validPhotos.map((p) => `${(p.length / 1024).toFixed(1)}KB`),
|
|
66
84
|
});
|
|
67
85
|
}
|
|
@@ -44,9 +44,13 @@ export const executeWizardGeneration = async (params: ExecuteGenerationParams):
|
|
|
44
44
|
console.log("[WizardGeneration] GENERATING -", isVideoMode ? "VIDEO" : "PHOTO");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
// Safely extract prompt with type guard
|
|
48
|
+
const prompt = typeof input === "object" && input !== null && "prompt" in input && typeof input.prompt === "string"
|
|
49
|
+
? input.prompt
|
|
50
|
+
: "";
|
|
51
|
+
|
|
48
52
|
const generationFn = isVideoMode ? videoGenerationFn : photoGenerationFn;
|
|
49
|
-
await generationFn(input,
|
|
53
|
+
await generationFn(input, prompt);
|
|
50
54
|
|
|
51
55
|
if (isMountedRef.current) {
|
|
52
56
|
dispatch({ type: "COMPLETE" });
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { useState, useCallback, useEffect, useRef } from "react";
|
|
8
|
+
|
|
9
|
+
declare const __DEV__: boolean;
|
|
8
10
|
import { useMedia, MediaQuality, MediaValidationError, MEDIA_CONSTANTS } from "@umituz/react-native-design-system";
|
|
9
11
|
import type { UploadedImage } from "../../../../../presentation/hooks/generation/useAIGenerateState";
|
|
10
12
|
|
|
@@ -51,6 +53,15 @@ export const usePhotoUploadState = ({
|
|
|
51
53
|
const { pickImage, isLoading } = useMedia();
|
|
52
54
|
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
53
55
|
|
|
56
|
+
// Use refs to avoid effect re-runs on callback changes
|
|
57
|
+
const onErrorRef = useRef(onError);
|
|
58
|
+
const translationsRef = useRef(translations);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
onErrorRef.current = onError;
|
|
62
|
+
translationsRef.current = translations;
|
|
63
|
+
}, [onError, translations]);
|
|
64
|
+
|
|
54
65
|
const maxFileSizeMB = config?.maxFileSizeMB ?? MEDIA_CONSTANTS.MAX_IMAGE_SIZE_MB;
|
|
55
66
|
|
|
56
67
|
useEffect(() => {
|
|
@@ -61,28 +72,31 @@ export const usePhotoUploadState = ({
|
|
|
61
72
|
}, [stepId, initialImage]);
|
|
62
73
|
|
|
63
74
|
useEffect(() => {
|
|
75
|
+
// Clear any existing timeout first
|
|
76
|
+
if (timeoutRef.current) {
|
|
77
|
+
clearTimeout(timeoutRef.current);
|
|
78
|
+
timeoutRef.current = undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
64
81
|
if (isLoading) {
|
|
65
82
|
timeoutRef.current = setTimeout(() => {
|
|
66
83
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
67
84
|
console.warn("[usePhotoUploadState] Image picker timeout - possible stuck state");
|
|
68
85
|
}
|
|
69
|
-
|
|
70
|
-
title:
|
|
86
|
+
onErrorRef.current?.({
|
|
87
|
+
title: translationsRef.current.error,
|
|
71
88
|
message: "Image selection is taking too long. Please try again.",
|
|
72
89
|
});
|
|
73
90
|
}, 30000);
|
|
74
|
-
} else {
|
|
75
|
-
if (timeoutRef.current) {
|
|
76
|
-
clearTimeout(timeoutRef.current);
|
|
77
|
-
}
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
return () => {
|
|
81
94
|
if (timeoutRef.current) {
|
|
82
95
|
clearTimeout(timeoutRef.current);
|
|
96
|
+
timeoutRef.current = undefined;
|
|
83
97
|
}
|
|
84
98
|
};
|
|
85
|
-
}, [isLoading
|
|
99
|
+
}, [isLoading]);
|
|
86
100
|
|
|
87
101
|
const clearImage = useCallback(() => {
|
|
88
102
|
setImage(null);
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { useEffect, useRef, useCallback, useState } from "react";
|
|
6
|
+
|
|
7
|
+
declare const __DEV__: boolean;
|
|
6
8
|
import { pollQueueStatus } from "./videoQueuePoller";
|
|
7
9
|
import { DEFAULT_POLL_INTERVAL_MS } from "../../../../../infrastructure/constants/polling.constants";
|
|
8
10
|
import type { GenerationUrls } from "./generation-result.utils";
|
|
@@ -50,7 +52,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
50
52
|
imageUrl: urls.imageUrl,
|
|
51
53
|
videoUrl: urls.videoUrl,
|
|
52
54
|
});
|
|
53
|
-
} catch {
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
57
|
+
console.error("[VideoQueue] Failed to update completion status:", error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
resetRefs();
|
|
56
62
|
onSuccess?.(urls);
|
|
@@ -64,7 +70,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
64
70
|
if (creationId && userId) {
|
|
65
71
|
try {
|
|
66
72
|
await persistence.updateToFailed(userId, creationId, errorMsg);
|
|
67
|
-
} catch {
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
75
|
+
console.error("[VideoQueue] Failed to update error status:", error);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
68
78
|
}
|
|
69
79
|
resetRefs();
|
|
70
80
|
onError?.(errorMsg);
|
|
@@ -107,7 +117,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
107
117
|
prompt,
|
|
108
118
|
});
|
|
109
119
|
creationIdRef.current = creationId;
|
|
110
|
-
} catch {
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
122
|
+
console.error("[VideoQueue] Failed to save processing creation:", error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
const queueResult = await strategy.submitToQueue(input);
|
|
@@ -126,7 +140,11 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
126
140
|
if (creationId && userId && queueResult.requestId && queueResult.model) {
|
|
127
141
|
try {
|
|
128
142
|
await persistence.updateRequestId(userId, creationId, queueResult.requestId, queueResult.model);
|
|
129
|
-
} catch {
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
145
|
+
console.error("[VideoQueue] Failed to update request ID:", error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
pollingRef.current = setInterval(() => void pollStatus(), DEFAULT_POLL_INTERVAL_MS);
|
|
@@ -72,7 +72,8 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
|
|
|
72
72
|
if (isGeneratingStep && state.status === "IDLE" && !isAlreadyGenerating) {
|
|
73
73
|
dispatch({ type: "START_PREPARATION" });
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
// Execute generation and handle errors properly
|
|
76
|
+
void executeWizardGeneration({
|
|
76
77
|
wizardData,
|
|
77
78
|
scenario,
|
|
78
79
|
isVideoMode,
|
|
@@ -81,6 +82,13 @@ export const useWizardGeneration = (props: UseWizardGenerationProps): UseWizardG
|
|
|
81
82
|
onError,
|
|
82
83
|
videoGenerationFn: videoGeneration.startGeneration,
|
|
83
84
|
photoGenerationFn: photoGeneration.startGeneration,
|
|
85
|
+
}).catch((error) => {
|
|
86
|
+
// Catch any unhandled errors from executeWizardGeneration
|
|
87
|
+
if (isMountedRef.current) {
|
|
88
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
89
|
+
dispatch({ type: "ERROR", error: errorMsg });
|
|
90
|
+
onError?.(errorMsg);
|
|
91
|
+
}
|
|
84
92
|
});
|
|
85
93
|
}
|
|
86
94
|
|
|
@@ -16,12 +16,15 @@ 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
|
|
19
20
|
if (isPollingRef.current) return;
|
|
21
|
+
isPollingRef.current = true;
|
|
20
22
|
|
|
21
23
|
const provider = providerRegistry.getActiveProvider();
|
|
22
|
-
if (!provider)
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
if (!provider) {
|
|
25
|
+
isPollingRef.current = false;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
25
28
|
try {
|
|
26
29
|
const status = await provider.getJobStatus(model, requestId);
|
|
27
30
|
if (__DEV__) console.log("[VideoQueueGeneration] Poll:", status.status);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { executeImageToVideo } from "../../infrastructure/services";
|
|
2
|
+
|
|
3
|
+
declare const __DEV__: boolean;
|
|
2
4
|
import type { GenerationStrategy } from "../../../../presentation/hooks/generation";
|
|
3
5
|
import type {
|
|
4
6
|
ImageToVideoFeatureConfig,
|
|
@@ -41,7 +43,11 @@ export const createImageToVideoStrategy = (
|
|
|
41
43
|
type: "image-to-video",
|
|
42
44
|
imageUri: input.imageUrl,
|
|
43
45
|
metadata: input.options as Record<string, unknown> | undefined,
|
|
44
|
-
}).catch(() => {
|
|
46
|
+
}).catch((error) => {
|
|
47
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
48
|
+
console.error("[ImageToVideo] onGenerationStart callback failed:", error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
45
51
|
|
|
46
52
|
const result = await executeImageToVideo(
|
|
47
53
|
{ imageUri: input.imageUrl, userId, motionPrompt: input.prompt, options: input.options },
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useCallback } from "react";
|
|
7
|
+
|
|
8
|
+
declare const __DEV__: boolean;
|
|
7
9
|
import * as MediaLibrary from "expo-media-library";
|
|
8
10
|
import * as Sharing from "expo-sharing";
|
|
9
11
|
import {
|
|
@@ -88,7 +90,10 @@ export const useResultActions = (options: UseResultActionsOptions = {}): UseResu
|
|
|
88
90
|
onShareEnd?.(true);
|
|
89
91
|
}
|
|
90
92
|
} catch (error: unknown) {
|
|
91
|
-
if (__DEV__
|
|
93
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
94
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
95
|
+
console.log("[ResultActions] Share cancelled or failed:", errorMsg);
|
|
96
|
+
}
|
|
92
97
|
onShareEnd?.(true);
|
|
93
98
|
} finally {
|
|
94
99
|
setIsSharing(false);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { executeTextToVideo } from "../../infrastructure/services";
|
|
2
|
+
|
|
3
|
+
declare const __DEV__: boolean;
|
|
2
4
|
import type { GenerationStrategy } from "../../../../presentation/hooks/generation";
|
|
3
5
|
import type {
|
|
4
6
|
TextToVideoConfig,
|
|
@@ -40,7 +42,11 @@ export const createTextToVideoStrategy = (
|
|
|
40
42
|
type: "text-to-video",
|
|
41
43
|
prompt: input.prompt,
|
|
42
44
|
metadata: input.options as Record<string, unknown> | undefined,
|
|
43
|
-
}).catch(() => {
|
|
45
|
+
}).catch((error) => {
|
|
46
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
47
|
+
console.error("[TextToVideo] onGenerationStart callback failed:", error);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
44
50
|
|
|
45
51
|
const result = await executeTextToVideo(
|
|
46
52
|
{ prompt: input.prompt, userId, options: input.options },
|
|
@@ -86,6 +86,11 @@ class GenerationOrchestratorService {
|
|
|
86
86
|
throw pollResult.error ?? new Error("Polling failed");
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Validate result exists before type assertion
|
|
90
|
+
if (!pollResult.data) {
|
|
91
|
+
throw new Error("Polling succeeded but no data returned");
|
|
92
|
+
}
|
|
93
|
+
|
|
89
94
|
const result = pollResult.data as T;
|
|
90
95
|
const duration = Date.now() - startTime;
|
|
91
96
|
|
|
@@ -72,10 +72,15 @@ export async function executeImageFeature(
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Safely extract requestId with type guard
|
|
76
|
+
const requestId = typeof result === "object" && result !== null && "requestId" in result && typeof result.requestId === "string"
|
|
77
|
+
? result.requestId
|
|
78
|
+
: undefined;
|
|
79
|
+
|
|
75
80
|
return {
|
|
76
81
|
success: true,
|
|
77
82
|
imageUrl,
|
|
78
|
-
requestId
|
|
83
|
+
requestId,
|
|
79
84
|
};
|
|
80
85
|
} catch (error) {
|
|
81
86
|
const message = extractErrorMessage(error, "Processing failed", `Image:${featureType}`);
|
|
@@ -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],
|
|
@@ -34,6 +34,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
34
34
|
const isGeneratingRef = useRef(false);
|
|
35
35
|
const isMountedRef = useRef(true);
|
|
36
36
|
const abortControllerRef = useRef<AbortController | null>(null);
|
|
37
|
+
const cleanupTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
37
38
|
|
|
38
39
|
const offlineStore = useOfflineStore();
|
|
39
40
|
const { showError, showSuccess } = useAlert();
|
|
@@ -44,18 +45,31 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
44
45
|
return () => {
|
|
45
46
|
isMountedRef.current = false;
|
|
46
47
|
abortControllerRef.current?.abort();
|
|
48
|
+
// Clear any pending lifecycle complete timeout
|
|
49
|
+
if (cleanupTimeoutRef.current) {
|
|
50
|
+
clearTimeout(cleanupTimeoutRef.current);
|
|
51
|
+
cleanupTimeoutRef.current = null;
|
|
52
|
+
}
|
|
47
53
|
};
|
|
48
54
|
}, []);
|
|
49
55
|
|
|
50
56
|
const handleLifecycleComplete = useCallback(
|
|
51
57
|
(status: "success" | "error", result?: TResult, error?: GenerationError) => {
|
|
52
58
|
if (!lifecycle?.onComplete) return;
|
|
59
|
+
|
|
60
|
+
// Clear any existing timeout first
|
|
61
|
+
if (cleanupTimeoutRef.current) {
|
|
62
|
+
clearTimeout(cleanupTimeoutRef.current);
|
|
63
|
+
cleanupTimeoutRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
const delay = lifecycle.completeDelay ?? 500;
|
|
54
|
-
|
|
55
|
-
if (isMountedRef.current)
|
|
67
|
+
cleanupTimeoutRef.current = setTimeout(() => {
|
|
68
|
+
if (isMountedRef.current) {
|
|
69
|
+
lifecycle.onComplete?.(status, result, error);
|
|
70
|
+
}
|
|
71
|
+
cleanupTimeoutRef.current = null;
|
|
56
72
|
}, delay);
|
|
57
|
-
// Store timeout ID for cleanup (if component unmounts during delay)
|
|
58
|
-
return () => clearTimeout(timeoutId);
|
|
59
73
|
},
|
|
60
74
|
[lifecycle],
|
|
61
75
|
);
|
|
@@ -237,7 +251,7 @@ export const useGenerationOrchestrator = <TInput, TResult>(
|
|
|
237
251
|
abortControllerRef.current = null;
|
|
238
252
|
}
|
|
239
253
|
},
|
|
240
|
-
[offlineStore
|
|
254
|
+
[offlineStore, moderation, alertMessages, strategy, checkCredits, onCreditsExhausted, executeGeneration, showError, onError, handleLifecycleComplete],
|
|
241
255
|
);
|
|
242
256
|
|
|
243
257
|
const reset = useCallback(() => {
|
|
@@ -24,7 +24,11 @@ export function createGoNextHandler(
|
|
|
24
24
|
onComplete?.(newState);
|
|
25
25
|
} else {
|
|
26
26
|
setState({ ...state, currentStepIndex: nextIndex });
|
|
27
|
-
|
|
27
|
+
// Bounds check before accessing array
|
|
28
|
+
const nextStep = config.photoSteps[nextIndex];
|
|
29
|
+
if (nextStep) {
|
|
30
|
+
onStepChange?.(nextIndex, nextStep);
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
};
|
|
30
34
|
}
|
|
@@ -40,8 +44,15 @@ export function createGoBackHandler(
|
|
|
40
44
|
if (!canGoBack) return;
|
|
41
45
|
|
|
42
46
|
const prevIndex = currentStepIndex - 1;
|
|
47
|
+
// Validate index is within bounds
|
|
48
|
+
if (prevIndex < 0) return;
|
|
49
|
+
|
|
43
50
|
setState((prev) => ({ ...prev, currentStepIndex: prevIndex, isComplete: false }));
|
|
44
|
-
|
|
51
|
+
// Bounds check before accessing array
|
|
52
|
+
const prevStep = config.photoSteps[prevIndex];
|
|
53
|
+
if (prevStep) {
|
|
54
|
+
onStepChange?.(prevIndex, prevStep);
|
|
55
|
+
}
|
|
45
56
|
};
|
|
46
57
|
}
|
|
47
58
|
|
|
@@ -88,7 +88,7 @@ export function useGeneration<T = unknown>(
|
|
|
88
88
|
|
|
89
89
|
const genResult = await generationOrchestrator.generate<T>(request);
|
|
90
90
|
|
|
91
|
-
if (abortRef.current || abortControllerRef.current
|
|
91
|
+
if (abortRef.current || abortControllerRef.current?.signal.aborted) return;
|
|
92
92
|
|
|
93
93
|
setResult(genResult);
|
|
94
94
|
|
|
@@ -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
|
},
|