@umituz/react-native-ai-generation-content 1.65.6 → 1.65.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/access-control/hooks/useAIFeatureGate.ts +1 -1
- package/src/domains/background/infrastructure/executors/backgroundJobExecutor.ts +17 -6
- package/src/domains/background/infrastructure/services/job-poller.service.ts +81 -3
- package/src/domains/background/infrastructure/utils/polling-interval.util.ts +1 -4
- package/src/domains/background/presentation/hooks/use-background-generation.ts +17 -6
- package/src/domains/background/presentation/hooks/use-pending-jobs.ts +4 -0
- package/src/domains/creations/infrastructure/repositories/CreationsFetcher.ts +14 -2
- package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +0 -1
- package/src/domains/creations/infrastructure/repositories/creation-delete.operations.ts +65 -6
- package/src/domains/creations/presentation/hooks/creation-validators.ts +0 -1
- package/src/domains/generation/infrastructure/flow/use-flow-store.types.ts +0 -1
- package/src/domains/generation/infrastructure/flow/useFlow.ts +13 -25
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +5 -3
- package/src/domains/generation/wizard/infrastructure/strategies/video-generation.types.ts +84 -0
- package/src/domains/generation/wizard/presentation/hooks/generationExecutor.ts +6 -7
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +2 -2
- package/src/domains/image-to-video/domain/types/config.types.ts +3 -0
- package/src/domains/image-to-video/infrastructure/services/image-to-video-executor.ts +6 -3
- package/src/domains/image-to-video/presentation/hooks/imageToVideoStrategy.ts +10 -11
- package/src/domains/image-to-video/presentation/hooks/useFormState.ts +2 -2
- package/src/domains/image-to-video/presentation/hooks/useGeneration.ts +39 -6
- package/src/domains/image-to-video/presentation/hooks/useImageToVideoFeature.ts +19 -14
- package/src/domains/prompts/infrastructure/services/ImagePromptBuilder.ts +7 -1
- package/src/domains/text-to-image/presentation/hooks/useFormState.ts +1 -2
- package/src/domains/text-to-image/presentation/hooks/useGeneration.ts +2 -2
- package/src/domains/text-to-image/presentation/screens/TextToImageWizardFlow.tsx +0 -1
- package/src/domains/text-to-video/infrastructure/services/text-to-video-executor.ts +2 -2
- package/src/index.ts +3 -0
- package/src/infrastructure/executors/base-executor.ts +2 -2
- package/src/infrastructure/executors/index.ts +1 -1
- package/src/infrastructure/http/http-fetch-handler.ts +1 -1
- package/src/infrastructure/http/http-request-executor.ts +1 -1
- package/src/infrastructure/utils/domain-guards.ts +2 -1
- package/src/infrastructure/utils/index.ts +5 -1
- package/src/presentation/hooks/generation/orchestrator.ts +29 -2
- package/src/presentation/hooks/generation/useImageGeneration.ts +3 -0
- package/src/presentation/hooks/use-generation.ts +32 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.65.
|
|
3
|
+
"version": "1.65.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",
|
|
@@ -52,7 +52,7 @@ export function useAIFeatureGate(options: AIFeatureGateOptions): AIFeatureGateRe
|
|
|
52
52
|
hasSubscription: isPremium,
|
|
53
53
|
creditBalance,
|
|
54
54
|
requiredCredits: creditCost,
|
|
55
|
-
onShowPaywall: openPaywall,
|
|
55
|
+
onShowPaywall: () => openPaywall(),
|
|
56
56
|
isCreditsLoaded,
|
|
57
57
|
});
|
|
58
58
|
|
|
@@ -43,7 +43,8 @@ interface QueuedExecutionParams<TInput, TResult> {
|
|
|
43
43
|
input: TInput;
|
|
44
44
|
executor: JobExecutorConfig<TInput, TResult>;
|
|
45
45
|
updateJob: (params: { id: string; updates: Partial<BackgroundJob<TInput, TResult>> }) => void;
|
|
46
|
-
|
|
46
|
+
updateJobAsync: (params: { id: string; updates: Partial<BackgroundJob<TInput, TResult>> }) => Promise<{ id: string; updates: Partial<BackgroundJob<TInput, TResult>> }>;
|
|
47
|
+
removeJobAsync: (id: string) => Promise<string>;
|
|
47
48
|
getJob: (id: string) => BackgroundJob<TInput, TResult> | undefined;
|
|
48
49
|
activeJobsRef: React.MutableRefObject<Set<string>>;
|
|
49
50
|
onJobComplete?: (job: BackgroundJob<TInput, TResult>) => void;
|
|
@@ -59,7 +60,8 @@ export const executeQueuedJob = async <TInput, TResult>(
|
|
|
59
60
|
input,
|
|
60
61
|
executor,
|
|
61
62
|
updateJob,
|
|
62
|
-
|
|
63
|
+
updateJobAsync,
|
|
64
|
+
removeJobAsync,
|
|
63
65
|
getJob,
|
|
64
66
|
activeJobsRef,
|
|
65
67
|
onJobComplete,
|
|
@@ -68,13 +70,17 @@ export const executeQueuedJob = async <TInput, TResult>(
|
|
|
68
70
|
} = params;
|
|
69
71
|
|
|
70
72
|
try {
|
|
71
|
-
|
|
73
|
+
// Critical status update - await to ensure state consistency
|
|
74
|
+
await updateJobAsync({ id: jobId, updates: { status: "processing", progress: 10 } });
|
|
72
75
|
|
|
73
76
|
const result = await executor.execute(input, (p) => {
|
|
77
|
+
// Progress updates use non-async version for performance
|
|
78
|
+
// Progress updates are frequent and eventual consistency is acceptable
|
|
74
79
|
updateJob({ id: jobId, updates: { progress: p } });
|
|
75
80
|
});
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
// Critical status update - await to ensure state consistency
|
|
83
|
+
await updateJobAsync({
|
|
78
84
|
id: jobId,
|
|
79
85
|
updates: { status: "completed", progress: 100, result, completedAt: new Date() },
|
|
80
86
|
});
|
|
@@ -85,11 +91,13 @@ export const executeQueuedJob = async <TInput, TResult>(
|
|
|
85
91
|
onJobComplete?.(completedJob);
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
// Await removal to ensure cleanup happens before checking activeJobs
|
|
95
|
+
await removeJobAsync(jobId);
|
|
89
96
|
} catch (error) {
|
|
90
97
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
91
98
|
|
|
92
|
-
|
|
99
|
+
// Critical error status update - await for consistency
|
|
100
|
+
await updateJobAsync({ id: jobId, updates: { status: "failed", error: errorMsg, progress: 0 } });
|
|
93
101
|
|
|
94
102
|
const failedJob = getJob(jobId);
|
|
95
103
|
if (failedJob) {
|
|
@@ -97,7 +105,10 @@ export const executeQueuedJob = async <TInput, TResult>(
|
|
|
97
105
|
onJobError?.(failedJob);
|
|
98
106
|
}
|
|
99
107
|
} finally {
|
|
108
|
+
// Use atomic Set operation to prevent race conditions
|
|
100
109
|
activeJobsRef.current.delete(jobId);
|
|
110
|
+
|
|
111
|
+
// Check size after deletion to prevent multiple onAllComplete calls
|
|
101
112
|
if (activeJobsRef.current.size === 0) {
|
|
102
113
|
onAllComplete?.();
|
|
103
114
|
}
|
|
@@ -10,6 +10,56 @@ import { checkStatusForErrors, isJobComplete } from "../utils/status-checker.uti
|
|
|
10
10
|
import { validateResult } from "../utils/result-validator.util";
|
|
11
11
|
import type { PollJobOptions, PollJobResult } from "./job-poller.types";
|
|
12
12
|
|
|
13
|
+
declare const __DEV__: boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wraps a promise with abort signal support
|
|
17
|
+
* Rejects if signal is aborted before promise resolves
|
|
18
|
+
*/
|
|
19
|
+
function withAbortSignal<T>(
|
|
20
|
+
promise: Promise<T>,
|
|
21
|
+
signal: AbortSignal | undefined,
|
|
22
|
+
timeoutMs?: number,
|
|
23
|
+
): Promise<T> {
|
|
24
|
+
if (!signal && !timeoutMs) {
|
|
25
|
+
return promise;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return new Promise<T>((resolve, reject) => {
|
|
29
|
+
// Handle abort signal
|
|
30
|
+
if (signal?.aborted) {
|
|
31
|
+
reject(new Error("Operation aborted"));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const abortHandler = () => {
|
|
36
|
+
reject(new Error("Operation aborted"));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
signal?.addEventListener("abort", abortHandler, { once: true });
|
|
40
|
+
|
|
41
|
+
// Handle timeout
|
|
42
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
43
|
+
if (timeoutMs) {
|
|
44
|
+
timeoutId = setTimeout(() => {
|
|
45
|
+
reject(new Error(`Operation timeout after ${timeoutMs}ms`));
|
|
46
|
+
}, timeoutMs);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
promise
|
|
50
|
+
.then((result) => {
|
|
51
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
52
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
53
|
+
resolve(result);
|
|
54
|
+
})
|
|
55
|
+
.catch((error) => {
|
|
56
|
+
signal?.removeEventListener("abort", abortHandler);
|
|
57
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
58
|
+
reject(error);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
13
63
|
/**
|
|
14
64
|
* Poll job until completion with exponential backoff
|
|
15
65
|
* Only reports 100% on actual completion
|
|
@@ -60,7 +110,12 @@ export async function pollJob<T = unknown>(
|
|
|
60
110
|
}
|
|
61
111
|
|
|
62
112
|
try {
|
|
63
|
-
|
|
113
|
+
// Wrap provider calls with abort signal support and timeout (30s default)
|
|
114
|
+
const status = await withAbortSignal(
|
|
115
|
+
provider.getJobStatus(model, requestId),
|
|
116
|
+
signal,
|
|
117
|
+
30000,
|
|
118
|
+
);
|
|
64
119
|
onStatusChange?.(status);
|
|
65
120
|
|
|
66
121
|
const statusCheck = checkStatusForErrors(status);
|
|
@@ -77,7 +132,12 @@ export async function pollJob<T = unknown>(
|
|
|
77
132
|
consecutiveTransientErrors = 0;
|
|
78
133
|
|
|
79
134
|
if (isJobComplete(status)) {
|
|
80
|
-
|
|
135
|
+
// Wrap result retrieval with abort signal support and timeout (60s for larger results)
|
|
136
|
+
const result = await withAbortSignal(
|
|
137
|
+
provider.getJobResult<T>(model, requestId),
|
|
138
|
+
signal,
|
|
139
|
+
60000,
|
|
140
|
+
);
|
|
81
141
|
|
|
82
142
|
const validation = validateResult(result);
|
|
83
143
|
if (!validation.isValid) {
|
|
@@ -98,11 +158,29 @@ export async function pollJob<T = unknown>(
|
|
|
98
158
|
elapsedMs: Date.now() - startTime,
|
|
99
159
|
};
|
|
100
160
|
}
|
|
101
|
-
} catch {
|
|
161
|
+
} catch (error) {
|
|
102
162
|
consecutiveTransientErrors++;
|
|
103
163
|
|
|
164
|
+
if (__DEV__) {
|
|
165
|
+
console.warn("[JobPoller] Transient error during polling", {
|
|
166
|
+
attempt: attempt + 1,
|
|
167
|
+
requestId,
|
|
168
|
+
model,
|
|
169
|
+
consecutiveErrors: consecutiveTransientErrors,
|
|
170
|
+
error: error instanceof Error ? error.message : String(error),
|
|
171
|
+
code: (error as { code?: string })?.code,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
104
175
|
// Check if we've hit max consecutive transient errors
|
|
105
176
|
if (maxConsecutiveErrors && consecutiveTransientErrors >= maxConsecutiveErrors) {
|
|
177
|
+
if (__DEV__) {
|
|
178
|
+
console.error("[JobPoller] Max consecutive errors reached", {
|
|
179
|
+
maxConsecutiveErrors,
|
|
180
|
+
requestId,
|
|
181
|
+
model,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
106
184
|
return {
|
|
107
185
|
success: false,
|
|
108
186
|
error: new Error(`Too many consecutive errors (${consecutiveTransientErrors})`),
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
* Polling Interval Calculator
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
DEFAULT_POLLING_CONFIG,
|
|
7
|
-
type PollingConfig,
|
|
8
|
-
} from "../../../../domain/entities/polling.types";
|
|
5
|
+
import type { PollingConfig } from "../../../../domain/entities/polling.types";
|
|
9
6
|
|
|
10
7
|
export interface IntervalOptions {
|
|
11
8
|
attempt: number;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useRef, useState, useMemo } from "react";
|
|
2
2
|
import { usePendingJobs } from "./use-pending-jobs";
|
|
3
3
|
import { executeDirectGeneration, executeQueuedJob } from "../../infrastructure/executors/backgroundJobExecutor";
|
|
4
4
|
import { DEFAULT_QUEUE_CONFIG } from "../../domain/entities/job.types";
|
|
@@ -21,7 +21,7 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
|
|
|
21
21
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
22
22
|
const [progress, setProgress] = useState(0);
|
|
23
23
|
|
|
24
|
-
const { jobs, addJobAsync, updateJob, removeJob, getJob } = usePendingJobs<
|
|
24
|
+
const { jobs, addJobAsync, updateJob, updateJobAsync, removeJob, removeJobAsync, getJob } = usePendingJobs<
|
|
25
25
|
TInput,
|
|
26
26
|
TResult
|
|
27
27
|
>({
|
|
@@ -43,14 +43,15 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
|
|
|
43
43
|
input,
|
|
44
44
|
executor,
|
|
45
45
|
updateJob,
|
|
46
|
-
|
|
46
|
+
updateJobAsync,
|
|
47
|
+
removeJobAsync,
|
|
47
48
|
getJob,
|
|
48
49
|
activeJobsRef,
|
|
49
50
|
onJobComplete,
|
|
50
51
|
onJobError,
|
|
51
52
|
onAllComplete,
|
|
52
53
|
}),
|
|
53
|
-
[executor, onJobComplete, onJobError, onAllComplete, updateJob,
|
|
54
|
+
[executor, onJobComplete, onJobError, onAllComplete, updateJob, updateJobAsync, removeJobAsync, getJob],
|
|
54
55
|
);
|
|
55
56
|
|
|
56
57
|
const startJob = useCallback(
|
|
@@ -84,13 +85,23 @@ export function useBackgroundGeneration<TInput = unknown, TResult = unknown>(
|
|
|
84
85
|
[removeJob],
|
|
85
86
|
);
|
|
86
87
|
|
|
88
|
+
// Calculate active jobs from TanStack Query state (not ref) for reactivity
|
|
89
|
+
// Active jobs are those currently processing or queued
|
|
90
|
+
const activeJobs = useMemo(
|
|
91
|
+
() => jobs.filter((job) => job.status === "processing" || job.status === "queued"),
|
|
92
|
+
[jobs]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const activeJobCount = activeJobs.length;
|
|
96
|
+
const hasActiveJobs = activeJobCount > 0;
|
|
97
|
+
|
|
87
98
|
return {
|
|
88
99
|
startJob,
|
|
89
100
|
executeDirectly,
|
|
90
101
|
cancelJob,
|
|
91
102
|
pendingJobs: jobs,
|
|
92
|
-
activeJobCount
|
|
93
|
-
hasActiveJobs
|
|
103
|
+
activeJobCount,
|
|
104
|
+
hasActiveJobs,
|
|
94
105
|
isProcessing,
|
|
95
106
|
progress,
|
|
96
107
|
};
|
|
@@ -22,7 +22,9 @@ export interface UsePendingJobsReturn<TInput = unknown, TResult = unknown> {
|
|
|
22
22
|
readonly addJob: (input: AddJobInput<TInput>) => void;
|
|
23
23
|
readonly addJobAsync: (input: AddJobInput<TInput>) => Promise<BackgroundJob<TInput, TResult>>;
|
|
24
24
|
readonly updateJob: (input: UpdateJobInput) => void;
|
|
25
|
+
readonly updateJobAsync: (input: UpdateJobInput) => Promise<{ id: string; updates: Partial<BackgroundJob<TInput, TResult>> }>;
|
|
25
26
|
readonly removeJob: (id: string) => void;
|
|
27
|
+
readonly removeJobAsync: (id: string) => Promise<string>;
|
|
26
28
|
readonly clearCompleted: () => void;
|
|
27
29
|
readonly clearFailed: () => void;
|
|
28
30
|
readonly getJob: (id: string) => BackgroundJob<TInput, TResult> | undefined;
|
|
@@ -116,7 +118,9 @@ export function usePendingJobs<TInput = unknown, TResult = unknown>(
|
|
|
116
118
|
addJob: addJobMutation.mutate,
|
|
117
119
|
addJobAsync: addJobMutation.mutateAsync,
|
|
118
120
|
updateJob: updateJobMutation.mutate,
|
|
121
|
+
updateJobAsync: updateJobMutation.mutateAsync,
|
|
119
122
|
removeJob: removeJobMutation.mutate,
|
|
123
|
+
removeJobAsync: removeJobMutation.mutateAsync,
|
|
120
124
|
clearCompleted: clearCompletedMutation.mutate,
|
|
121
125
|
clearFailed: clearFailedMutation.mutate,
|
|
122
126
|
getJob,
|
|
@@ -95,8 +95,20 @@ export class CreationsFetcher {
|
|
|
95
95
|
): UnsubscribeFunction {
|
|
96
96
|
const userCollection = this.pathResolver.getUserCollection(userId);
|
|
97
97
|
if (!userCollection) {
|
|
98
|
+
const error = new Error(`[CreationsFetcher] Cannot subscribe: Invalid user collection for userId: ${userId}`);
|
|
99
|
+
if (__DEV__) {
|
|
100
|
+
console.error(error.message);
|
|
101
|
+
}
|
|
102
|
+
// Return empty array immediately
|
|
98
103
|
onData([]);
|
|
99
|
-
|
|
104
|
+
// Report error to callback
|
|
105
|
+
onError?.(error);
|
|
106
|
+
// Return no-op unsubscribe function (no listener was created)
|
|
107
|
+
return () => {
|
|
108
|
+
if (__DEV__) {
|
|
109
|
+
console.log("[CreationsFetcher] No-op unsubscribe called (no listener was created)");
|
|
110
|
+
}
|
|
111
|
+
};
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
// Optimized query with server-side filtering
|
|
@@ -132,7 +144,7 @@ export class CreationsFetcher {
|
|
|
132
144
|
if (__DEV__) {
|
|
133
145
|
console.error("[CreationsFetcher] Realtime subscription error:", {
|
|
134
146
|
error: error.message,
|
|
135
|
-
code: (error as
|
|
147
|
+
code: (error as { code?: string }).code,
|
|
136
148
|
userId,
|
|
137
149
|
});
|
|
138
150
|
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { updateDoc, deleteDoc } from "firebase/firestore";
|
|
6
6
|
import { type FirestorePathResolver } from "@umituz/react-native-firebase";
|
|
7
7
|
|
|
8
|
+
declare const __DEV__: boolean;
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Soft deletes a creation
|
|
10
12
|
*/
|
|
@@ -14,12 +16,31 @@ export async function deleteCreation(
|
|
|
14
16
|
creationId: string
|
|
15
17
|
): Promise<boolean> {
|
|
16
18
|
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
17
|
-
if (!docRef)
|
|
19
|
+
if (!docRef) {
|
|
20
|
+
if (__DEV__) {
|
|
21
|
+
console.error("[CreationDelete] Cannot delete: Invalid document reference", {
|
|
22
|
+
userId,
|
|
23
|
+
creationId,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
18
28
|
|
|
19
29
|
try {
|
|
20
30
|
await updateDoc(docRef, { deletedAt: new Date() });
|
|
31
|
+
if (__DEV__) {
|
|
32
|
+
console.log("[CreationDelete] Soft deleted successfully", { userId, creationId });
|
|
33
|
+
}
|
|
21
34
|
return true;
|
|
22
|
-
} catch {
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (__DEV__) {
|
|
37
|
+
console.error("[CreationDelete] Soft delete failed", {
|
|
38
|
+
userId,
|
|
39
|
+
creationId,
|
|
40
|
+
error: error instanceof Error ? error.message : String(error),
|
|
41
|
+
code: (error as { code?: string })?.code,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
23
44
|
return false;
|
|
24
45
|
}
|
|
25
46
|
}
|
|
@@ -33,12 +54,31 @@ export async function hardDeleteCreation(
|
|
|
33
54
|
creationId: string
|
|
34
55
|
): Promise<boolean> {
|
|
35
56
|
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
36
|
-
if (!docRef)
|
|
57
|
+
if (!docRef) {
|
|
58
|
+
if (__DEV__) {
|
|
59
|
+
console.error("[CreationDelete] Cannot hard delete: Invalid document reference", {
|
|
60
|
+
userId,
|
|
61
|
+
creationId,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
37
66
|
|
|
38
67
|
try {
|
|
39
68
|
await deleteDoc(docRef);
|
|
69
|
+
if (__DEV__) {
|
|
70
|
+
console.log("[CreationDelete] Hard deleted successfully", { userId, creationId });
|
|
71
|
+
}
|
|
40
72
|
return true;
|
|
41
|
-
} catch {
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (__DEV__) {
|
|
75
|
+
console.error("[CreationDelete] Hard delete failed", {
|
|
76
|
+
userId,
|
|
77
|
+
creationId,
|
|
78
|
+
error: error instanceof Error ? error.message : String(error),
|
|
79
|
+
code: (error as { code?: string })?.code,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
42
82
|
return false;
|
|
43
83
|
}
|
|
44
84
|
}
|
|
@@ -52,12 +92,31 @@ export async function restoreCreation(
|
|
|
52
92
|
creationId: string
|
|
53
93
|
): Promise<boolean> {
|
|
54
94
|
const docRef = pathResolver.getDocRef(userId, creationId);
|
|
55
|
-
if (!docRef)
|
|
95
|
+
if (!docRef) {
|
|
96
|
+
if (__DEV__) {
|
|
97
|
+
console.error("[CreationDelete] Cannot restore: Invalid document reference", {
|
|
98
|
+
userId,
|
|
99
|
+
creationId,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
56
104
|
|
|
57
105
|
try {
|
|
58
106
|
await updateDoc(docRef, { deletedAt: null });
|
|
107
|
+
if (__DEV__) {
|
|
108
|
+
console.log("[CreationDelete] Restored successfully", { userId, creationId });
|
|
109
|
+
}
|
|
59
110
|
return true;
|
|
60
|
-
} catch {
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (__DEV__) {
|
|
113
|
+
console.error("[CreationDelete] Restore failed", {
|
|
114
|
+
userId,
|
|
115
|
+
creationId,
|
|
116
|
+
error: error instanceof Error ? error.message : String(error),
|
|
117
|
+
code: (error as { code?: string })?.code,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
61
120
|
return false;
|
|
62
121
|
}
|
|
63
122
|
}
|
|
@@ -25,12 +25,9 @@ interface UseFlowReturn extends FlowState, FlowActions {
|
|
|
25
25
|
getPartnerName: (partnerId: string) => string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
let flowStoreInstance: FlowStoreType | null = null;
|
|
29
|
-
|
|
30
28
|
export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
|
|
31
29
|
const storeRef = useRef<FlowStoreType | null>(null);
|
|
32
30
|
const prevConfigRef = useRef<{ initialStepIndex?: number; initialStepId?: string; stepsCount: number } | undefined>(undefined);
|
|
33
|
-
const isResettingRef = useRef(false);
|
|
34
31
|
|
|
35
32
|
// Detect config changes (initialStepIndex, initialStepId, or steps changed)
|
|
36
33
|
const configChanged =
|
|
@@ -39,27 +36,19 @@ export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
|
|
|
39
36
|
prevConfigRef.current.initialStepId !== config.initialStepId ||
|
|
40
37
|
prevConfigRef.current.stepsCount !== config.steps.length);
|
|
41
38
|
|
|
42
|
-
// If config changed, reset and recreate store (
|
|
43
|
-
if (configChanged &&
|
|
44
|
-
|
|
45
|
-
if (flowStoreInstance) {
|
|
46
|
-
flowStoreInstance.getState().reset();
|
|
47
|
-
}
|
|
48
|
-
flowStoreInstance = null;
|
|
39
|
+
// If config changed, reset and recreate store (per-component instance)
|
|
40
|
+
if (configChanged && storeRef.current) {
|
|
41
|
+
storeRef.current.getState().reset();
|
|
49
42
|
storeRef.current = null;
|
|
50
|
-
isResettingRef.current = false;
|
|
51
43
|
}
|
|
52
44
|
|
|
53
|
-
// Initialize store if needed
|
|
54
|
-
if (!storeRef.current
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
storeRef.current = flowStoreInstance;
|
|
45
|
+
// Initialize store if needed (per-component instance)
|
|
46
|
+
if (!storeRef.current) {
|
|
47
|
+
storeRef.current = createFlowStore({
|
|
48
|
+
steps: config.steps,
|
|
49
|
+
initialStepId: config.initialStepId,
|
|
50
|
+
initialStepIndex: config.initialStepIndex,
|
|
51
|
+
});
|
|
63
52
|
}
|
|
64
53
|
|
|
65
54
|
// Store current config for next render comparison
|
|
@@ -115,9 +104,8 @@ export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
|
|
|
115
104
|
};
|
|
116
105
|
};
|
|
117
106
|
|
|
107
|
+
// Note: resetFlowStore is no longer needed as each component instance maintains its own store
|
|
108
|
+
// If you need to reset flow state, use the reset() action from the useFlow hook
|
|
118
109
|
export const resetFlowStore = () => {
|
|
119
|
-
|
|
120
|
-
flowStoreInstance.getState().reset();
|
|
121
|
-
}
|
|
122
|
-
flowStoreInstance = null;
|
|
110
|
+
console.warn('resetFlowStore is deprecated. Each component now maintains its own flow store instance.');
|
|
123
111
|
};
|
package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { extractPrompt, extractDuration, extractAspectRatio, extractResolution }
|
|
|
15
15
|
import { extractPhotosAsBase64 } from "./shared/photo-extraction.utils";
|
|
16
16
|
import { getVideoFeatureType } from "./video-generation.utils";
|
|
17
17
|
import type { WizardVideoInput, CreateVideoStrategyOptions } from "./video-generation.types";
|
|
18
|
-
import { validatePhotoCount } from "./video-generation.types";
|
|
18
|
+
import { validatePhotoCount, validateWizardVideoInput } from "./video-generation.types";
|
|
19
19
|
|
|
20
20
|
declare const __DEV__: boolean;
|
|
21
21
|
|
|
@@ -76,7 +76,8 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
76
76
|
|
|
77
77
|
return {
|
|
78
78
|
execute: async (input: unknown) => {
|
|
79
|
-
|
|
79
|
+
// Runtime validation with descriptive errors
|
|
80
|
+
const videoInput = validateWizardVideoInput(input);
|
|
80
81
|
|
|
81
82
|
const result = await executeVideoFeature(videoFeatureType, {
|
|
82
83
|
sourceImageBase64: videoInput.sourceImageBase64,
|
|
@@ -97,7 +98,8 @@ export function createVideoStrategy(options: CreateVideoStrategyOptions): Wizard
|
|
|
97
98
|
},
|
|
98
99
|
|
|
99
100
|
submitToQueue: async (input: unknown) => {
|
|
100
|
-
|
|
101
|
+
// Runtime validation with descriptive errors
|
|
102
|
+
const videoInput = validateWizardVideoInput(input);
|
|
101
103
|
|
|
102
104
|
const result = await submitVideoFeatureToQueue(videoFeatureType, {
|
|
103
105
|
sourceImageBase64: videoInput.sourceImageBase64,
|
|
@@ -59,3 +59,87 @@ export function validatePhotoCount(
|
|
|
59
59
|
|
|
60
60
|
return { isValid: true };
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Type guard for WizardVideoInput
|
|
65
|
+
* Validates runtime input and provides type safety
|
|
66
|
+
*/
|
|
67
|
+
export function isWizardVideoInput(input: unknown): input is WizardVideoInput {
|
|
68
|
+
if (!input || typeof input !== "object") {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const obj = input as Record<string, unknown>;
|
|
73
|
+
|
|
74
|
+
// prompt is required
|
|
75
|
+
if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Optional fields validation
|
|
80
|
+
if (obj.sourceImageBase64 !== undefined && typeof obj.sourceImageBase64 !== "string") {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (obj.targetImageBase64 !== undefined && typeof obj.targetImageBase64 !== "string") {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (obj.duration !== undefined && typeof obj.duration !== "number") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (obj.aspectRatio !== undefined && typeof obj.aspectRatio !== "string") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (obj.resolution !== undefined && typeof obj.resolution !== "string") {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validates and casts input to WizardVideoInput
|
|
105
|
+
* Throws descriptive error if validation fails
|
|
106
|
+
*/
|
|
107
|
+
export function validateWizardVideoInput(input: unknown): WizardVideoInput {
|
|
108
|
+
if (!isWizardVideoInput(input)) {
|
|
109
|
+
const errors: string[] = [];
|
|
110
|
+
|
|
111
|
+
if (!input || typeof input !== "object") {
|
|
112
|
+
throw new Error("Invalid input: expected object");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const obj = input as Record<string, unknown>;
|
|
116
|
+
|
|
117
|
+
if (typeof obj.prompt !== "string" || obj.prompt.length === 0) {
|
|
118
|
+
errors.push("prompt (string, required)");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (obj.sourceImageBase64 !== undefined && typeof obj.sourceImageBase64 !== "string") {
|
|
122
|
+
errors.push("sourceImageBase64 (string, optional)");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (obj.targetImageBase64 !== undefined && typeof obj.targetImageBase64 !== "string") {
|
|
126
|
+
errors.push("targetImageBase64 (string, optional)");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (obj.duration !== undefined && typeof obj.duration !== "number") {
|
|
130
|
+
errors.push("duration (number, optional)");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (obj.aspectRatio !== undefined && typeof obj.aspectRatio !== "string") {
|
|
134
|
+
errors.push("aspectRatio (string, optional)");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (obj.resolution !== undefined && typeof obj.resolution !== "string") {
|
|
138
|
+
errors.push("resolution (string, optional)");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
throw new Error(`Invalid WizardVideoInput: ${errors.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return input;
|
|
145
|
+
}
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { buildWizardInput } from "../../infrastructure/strategies";
|
|
2
2
|
import type { WizardScenarioData } from "./wizard-generation.types";
|
|
3
|
-
import type { Scenario } from "../../../../scenarios/domain/scenario.types";
|
|
4
3
|
import type { GenerationAction } from "./generationStateMachine";
|
|
5
4
|
|
|
6
5
|
declare const __DEV__: boolean;
|
|
7
6
|
|
|
8
7
|
interface ExecuteGenerationParams {
|
|
9
|
-
wizardData:
|
|
10
|
-
scenario:
|
|
8
|
+
wizardData: Record<string, unknown>;
|
|
9
|
+
scenario: WizardScenarioData;
|
|
11
10
|
isVideoMode: boolean;
|
|
12
11
|
isMountedRef: React.MutableRefObject<boolean>;
|
|
13
12
|
dispatch: React.Dispatch<GenerationAction>;
|
|
14
13
|
onError?: (error: string) => void;
|
|
15
|
-
videoGenerationFn: (input:
|
|
16
|
-
photoGenerationFn: (input:
|
|
14
|
+
videoGenerationFn: (input: unknown, prompt: string) => Promise<void>;
|
|
15
|
+
photoGenerationFn: (input: unknown, prompt: string) => Promise<void>;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export const executeWizardGeneration = async (params: ExecuteGenerationParams): Promise<void> => {
|
|
@@ -52,10 +51,10 @@ export const executeWizardGeneration = async (params: ExecuteGenerationParams):
|
|
|
52
51
|
if (isMountedRef.current) {
|
|
53
52
|
dispatch({ type: "COMPLETE" });
|
|
54
53
|
}
|
|
55
|
-
} catch (error:
|
|
54
|
+
} catch (error: unknown) {
|
|
56
55
|
if (!isMountedRef.current) return;
|
|
57
56
|
|
|
58
|
-
const errorMsg = error
|
|
57
|
+
const errorMsg = (error instanceof Error ? error.message : String(error)) || "error.generation.unknown";
|
|
59
58
|
if (__DEV__) {
|
|
60
59
|
console.error("[WizardGeneration] Error:", errorMsg, error);
|
|
61
60
|
}
|