@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.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/domains/access-control/hooks/useAIFeatureGate.ts +1 -1
  3. package/src/domains/background/infrastructure/executors/backgroundJobExecutor.ts +17 -6
  4. package/src/domains/background/infrastructure/services/job-poller.service.ts +81 -3
  5. package/src/domains/background/infrastructure/utils/polling-interval.util.ts +1 -4
  6. package/src/domains/background/presentation/hooks/use-background-generation.ts +17 -6
  7. package/src/domains/background/presentation/hooks/use-pending-jobs.ts +4 -0
  8. package/src/domains/creations/infrastructure/repositories/CreationsFetcher.ts +14 -2
  9. package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +0 -1
  10. package/src/domains/creations/infrastructure/repositories/creation-delete.operations.ts +65 -6
  11. package/src/domains/creations/presentation/hooks/creation-validators.ts +0 -1
  12. package/src/domains/generation/infrastructure/flow/use-flow-store.types.ts +0 -1
  13. package/src/domains/generation/infrastructure/flow/useFlow.ts +13 -25
  14. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.strategy.ts +5 -3
  15. package/src/domains/generation/wizard/infrastructure/strategies/video-generation.types.ts +84 -0
  16. package/src/domains/generation/wizard/presentation/hooks/generationExecutor.ts +6 -7
  17. package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +2 -2
  18. package/src/domains/image-to-video/domain/types/config.types.ts +3 -0
  19. package/src/domains/image-to-video/infrastructure/services/image-to-video-executor.ts +6 -3
  20. package/src/domains/image-to-video/presentation/hooks/imageToVideoStrategy.ts +10 -11
  21. package/src/domains/image-to-video/presentation/hooks/useFormState.ts +2 -2
  22. package/src/domains/image-to-video/presentation/hooks/useGeneration.ts +39 -6
  23. package/src/domains/image-to-video/presentation/hooks/useImageToVideoFeature.ts +19 -14
  24. package/src/domains/prompts/infrastructure/services/ImagePromptBuilder.ts +7 -1
  25. package/src/domains/text-to-image/presentation/hooks/useFormState.ts +1 -2
  26. package/src/domains/text-to-image/presentation/hooks/useGeneration.ts +2 -2
  27. package/src/domains/text-to-image/presentation/screens/TextToImageWizardFlow.tsx +0 -1
  28. package/src/domains/text-to-video/infrastructure/services/text-to-video-executor.ts +2 -2
  29. package/src/index.ts +3 -0
  30. package/src/infrastructure/executors/base-executor.ts +2 -2
  31. package/src/infrastructure/executors/index.ts +1 -1
  32. package/src/infrastructure/http/http-fetch-handler.ts +1 -1
  33. package/src/infrastructure/http/http-request-executor.ts +1 -1
  34. package/src/infrastructure/utils/domain-guards.ts +2 -1
  35. package/src/infrastructure/utils/index.ts +5 -1
  36. package/src/presentation/hooks/generation/orchestrator.ts +29 -2
  37. package/src/presentation/hooks/generation/useImageGeneration.ts +3 -0
  38. 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.6",
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
- removeJob: (id: string) => void;
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
- removeJob,
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
- updateJob({ id: jobId, updates: { status: "processing", progress: 10 } });
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
- updateJob({
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
- removeJob(jobId);
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
- updateJob({ id: jobId, updates: { status: "failed", error: errorMsg, progress: 0 } });
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
- const status = await provider.getJobStatus(model, requestId);
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
- const result = await provider.getJobResult<T>(model, requestId);
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
- removeJob,
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, removeJob, getJob],
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: activeJobsRef.current.size,
93
- hasActiveJobs: activeJobsRef.current.size > 0,
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
- return () => {};
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 any).code,
147
+ code: (error as { code?: string }).code,
136
148
  userId,
137
149
  });
138
150
  }
@@ -1,4 +1,3 @@
1
- declare const __DEV__: boolean;
2
1
 
3
2
  import { BaseRepository, FirestorePathResolver } from "@umituz/react-native-firebase";
4
3
  import type {
@@ -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) return false;
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) return false;
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) return false;
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
  }
@@ -14,7 +14,6 @@ import {
14
14
  } from "../../domain/constants";
15
15
  import type { ICreationsRepository } from "../../domain/repositories/ICreationsRepository";
16
16
 
17
- declare const __DEV__: boolean;
18
17
 
19
18
  export interface ValidationResult {
20
19
  isValid: boolean;
@@ -4,7 +4,6 @@
4
4
 
5
5
  import type {
6
6
  FlowState,
7
- FlowActions,
8
7
  StepDefinition,
9
8
  } from "../../../../domain/entities/flow-config.types";
10
9
 
@@ -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 (with guard against multiple resets)
43
- if (configChanged && !isResettingRef.current) {
44
- isResettingRef.current = true;
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 && !isResettingRef.current) {
55
- if (!flowStoreInstance) {
56
- flowStoreInstance = createFlowStore({
57
- steps: config.steps,
58
- initialStepId: config.initialStepId,
59
- initialStepIndex: config.initialStepIndex,
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
- if (flowStoreInstance) {
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
  };
@@ -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
- const videoInput = input as WizardVideoInput;
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
- const videoInput = input as WizardVideoInput;
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: WizardScenarioData;
10
- scenario: 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: any, prompt: string) => Promise<void>;
16
- photoGenerationFn: (input: any, prompt: string) => Promise<void>;
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: any) {
54
+ } catch (error: unknown) {
56
55
  if (!isMountedRef.current) return;
57
56
 
58
- const errorMsg = error?.message || "error.generation.unknown";
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
  }