@umituz/react-native-ai-generation-content 1.61.32 → 1.61.34

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 (32) hide show
  1. package/package.json +1 -1
  2. package/src/domain/entities/error.types.ts +0 -1
  3. package/src/domain/entities/job.types.ts +0 -4
  4. package/src/domain/entities/polling.types.ts +1 -3
  5. package/src/domain/interfaces/app-services.interface.ts +20 -2
  6. package/src/domains/content-moderation/infrastructure/services/content-moderation.service.ts +2 -1
  7. package/src/domains/content-moderation/infrastructure/services/moderators/text.moderator.ts +84 -4
  8. package/src/domains/content-moderation/infrastructure/services/pattern-matcher.service.ts +85 -2
  9. package/src/domains/creations/infrastructure/repositories/CreationsWriter.ts +102 -19
  10. package/src/domains/creations/presentation/hooks/useAdvancedFilter.ts +13 -4
  11. package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +10 -9
  12. package/src/domains/face-detection/presentation/hooks/useFaceDetection.ts +1 -1
  13. package/src/domains/generation/infrastructure/flow/useFlow.ts +11 -6
  14. package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +2 -3
  15. package/src/exports/infrastructure.ts +24 -1
  16. package/src/exports/presentation.ts +1 -1
  17. package/src/features/image-to-video/presentation/components/index.ts +0 -4
  18. package/src/index.ts +0 -4
  19. package/src/infrastructure/constants/index.ts +14 -2
  20. package/src/infrastructure/constants/polling.constants.ts +34 -0
  21. package/src/infrastructure/constants/storage.constants.ts +25 -0
  22. package/src/infrastructure/constants/validation.constants.ts +40 -0
  23. package/src/infrastructure/logging/logger.ts +185 -0
  24. package/src/infrastructure/services/job-poller.service.ts +13 -28
  25. package/src/infrastructure/utils/status-checker.util.ts +27 -7
  26. package/src/infrastructure/validation/input-validator.ts +406 -0
  27. package/src/presentation/components/AIGenerationForm.tsx +0 -11
  28. package/src/presentation/components/ErrorBoundary.tsx +141 -0
  29. package/src/presentation/components/index.ts +1 -0
  30. package/src/presentation/hooks/use-background-generation.ts +0 -12
  31. package/src/presentation/hooks/useAIFeatureCallbacks.ts +3 -4
  32. package/src/presentation/types/result-config.types.ts +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-ai-generation-content",
3
- "version": "1.61.32",
3
+ "version": "1.61.34",
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",
@@ -17,7 +17,6 @@ export enum AIErrorType {
17
17
  export interface AIErrorInfo {
18
18
  type: AIErrorType;
19
19
  messageKey: string;
20
- retryable: boolean;
21
20
  originalError?: unknown;
22
21
  statusCode?: number;
23
22
  }
@@ -47,15 +47,11 @@ export type GenerationMode = "direct" | "queued";
47
47
  export interface BackgroundQueueConfig {
48
48
  readonly mode?: GenerationMode;
49
49
  readonly maxConcurrent?: number;
50
- readonly retryCount?: number;
51
- readonly retryDelayMs?: number;
52
50
  readonly queryKey?: readonly string[];
53
51
  }
54
52
 
55
53
  export const DEFAULT_QUEUE_CONFIG: Required<BackgroundQueueConfig> = {
56
54
  mode: "queued",
57
55
  maxConcurrent: 1,
58
- retryCount: 2,
59
- retryDelayMs: 2000,
60
56
  queryKey: ["ai", "background-jobs"],
61
57
  };
@@ -8,7 +8,7 @@ export interface PollingConfig {
8
8
  initialIntervalMs: number;
9
9
  maxIntervalMs: number;
10
10
  backoffMultiplier: number;
11
- maxConsecutiveErrors: number;
11
+ maxTotalTimeMs?: number;
12
12
  }
13
13
 
14
14
  export const DEFAULT_POLLING_CONFIG: PollingConfig = {
@@ -16,13 +16,11 @@ export const DEFAULT_POLLING_CONFIG: PollingConfig = {
16
16
  initialIntervalMs: 1000,
17
17
  maxIntervalMs: 3000,
18
18
  backoffMultiplier: 1.2,
19
- maxConsecutiveErrors: 5,
20
19
  };
21
20
 
22
21
  export interface PollingState {
23
22
  attempt: number;
24
23
  lastProgress: number;
25
- consecutiveErrors: number;
26
24
  startTime: number;
27
25
  }
28
26
 
@@ -4,6 +4,17 @@
4
4
  * Apps implement these interfaces to provide their specific logic
5
5
  */
6
6
 
7
+ /**
8
+ * Metadata types for credit cost calculation
9
+ */
10
+ export interface CreditCostMetadata {
11
+ readonly model?: string;
12
+ readonly duration?: number;
13
+ readonly resolution?: string;
14
+ readonly quality?: string;
15
+ readonly [key: string]: string | number | boolean | undefined;
16
+ }
17
+
7
18
  /**
8
19
  * Network service interface
9
20
  * Handles network availability checks
@@ -52,7 +63,7 @@ export interface ICreditService {
52
63
  */
53
64
  calculateCost: (
54
65
  capability: string,
55
- metadata?: Record<string, unknown>,
66
+ metadata?: CreditCostMetadata,
56
67
  ) => number;
57
68
  }
58
69
 
@@ -91,6 +102,13 @@ export interface IAuthService {
91
102
  requireAuth: () => string;
92
103
  }
93
104
 
105
+ /**
106
+ * Analytics event data
107
+ */
108
+ export interface AnalyticsEventData {
109
+ readonly [key: string]: string | number | boolean | null | undefined;
110
+ }
111
+
94
112
  /**
95
113
  * Analytics service interface (optional)
96
114
  * Tracks events for analytics
@@ -101,7 +119,7 @@ export interface IAnalyticsService {
101
119
  * @param event - Event name
102
120
  * @param data - Event data
103
121
  */
104
- track: (event: string, data: Record<string, unknown>) => void;
122
+ track: (event: string, data: AnalyticsEventData) => void;
105
123
  }
106
124
 
107
125
  /**
@@ -103,7 +103,8 @@ class ContentModerationService {
103
103
  0
104
104
  );
105
105
 
106
- return Math.min(1.0, score / 2);
106
+ // Only divide by 2 if we have violations, otherwise return 1.0
107
+ return violations.length > 0 ? Math.min(1.0, score / Math.max(1, violations.length)) : 1.0;
107
108
  }
108
109
 
109
110
  private determineAction(
@@ -7,11 +7,91 @@ import type { Violation } from "../../../domain/entities/moderation.types";
7
7
  import { patternMatcherService } from "../pattern-matcher.service";
8
8
  import { rulesRegistry } from "../../rules/rules-registry";
9
9
  import { BaseModerator, type ModerationResult } from "./base.moderator";
10
+ import { DEFAULT_MAX_TEXT_LENGTH } from "../../../../infrastructure/constants/content.constants";
10
11
 
11
12
  declare const __DEV__: boolean;
12
13
 
13
- const DEFAULT_MAX_LENGTH = 10000;
14
+ /**
15
+ * HTML entity encoding detection
16
+ * More reliable than regex for detecting encoded malicious content
17
+ */
18
+ function containsHTMLEntities(content: string): boolean {
19
+ const htmlEntities = [
20
+ /&lt;/gi, /&gt;/gi, /&quot;/gi, /&amp;/gi, /&apos;/gi,
21
+ /&#\d+;/gi, /&#x[0-9a-fA-F]+;/gi,
22
+ ];
23
+ return htmlEntities.some(entity => entity.test(content));
24
+ }
25
+
26
+ /**
27
+ * Safe string matching for malicious code detection
28
+ * Uses string operations instead of regex where possible
29
+ */
30
+ function containsMaliciousPatterns(content: string): boolean {
31
+ const lowerContent = content.toLowerCase();
32
+
33
+ // Check for script tags (case-insensitive)
34
+ const scriptPatterns = ["<script", "</script>", "javascript:", "onclick=", "onerror=", "onload="];
35
+ for (const pattern of scriptPatterns) {
36
+ if (lowerContent.includes(pattern)) {
37
+ return true;
38
+ }
39
+ }
40
+
41
+ // Check for HTML entities (potential evasion)
42
+ if (containsHTMLEntities(content)) {
43
+ return true;
44
+ }
45
+
46
+ return false;
47
+ }
48
+
49
+ /**
50
+ * Multi-layered prompt injection detection
51
+ * Combines regex with string matching for better security
52
+ */
53
+ function containsPromptInjection(content: string): boolean {
54
+ const lowerContent = content.toLowerCase();
55
+
56
+ // Critical injection patterns (string-based for safety)
57
+ const criticalPatterns = [
58
+ "ignore all instructions",
59
+ "ignore previous instructions",
60
+ "disregard all instructions",
61
+ "forget all instructions",
62
+ "you are now a",
63
+ "jailbreak",
64
+ "dan mode",
65
+ "developer mode",
66
+ "system:",
67
+ "[system]",
68
+ "<<system>>",
69
+ ];
70
+
71
+ for (const pattern of criticalPatterns) {
72
+ if (lowerContent.includes(pattern)) {
73
+ return true;
74
+ }
75
+ }
76
+
77
+ // Additional regex patterns for more complex matching
78
+ const regexPatterns = [
79
+ /act\s+as\s+(if|though)\s+you/gi,
80
+ /pretend\s+(you\s+are|to\s+be)/gi,
81
+ /bypass\s+(your\s+)?(safety|content|moderation)/gi,
82
+ /override\s+(your\s+)?(restrictions?|limitations?|rules?)/gi,
83
+ ];
84
+
85
+ return regexPatterns.some(pattern => {
86
+ try {
87
+ return pattern.test(content);
88
+ } catch {
89
+ return false;
90
+ }
91
+ });
92
+ }
14
93
 
94
+ // Kept for reference but no longer used directly - using safer functions above
15
95
  const MALICIOUS_CODE_PATTERNS = [
16
96
  /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
17
97
  /javascript:/gi,
@@ -36,7 +116,7 @@ const PROMPT_INJECTION_PATTERNS = [
36
116
  ];
37
117
 
38
118
  class TextModerator extends BaseModerator {
39
- private maxLength = DEFAULT_MAX_LENGTH;
119
+ private maxLength = DEFAULT_MAX_TEXT_LENGTH;
40
120
 
41
121
  setMaxLength(length: number): void {
42
122
  this.maxLength = length;
@@ -106,11 +186,11 @@ class TextModerator extends BaseModerator {
106
186
  }
107
187
 
108
188
  private containsMaliciousCode(content: string): boolean {
109
- return MALICIOUS_CODE_PATTERNS.some((pattern) => pattern.test(content));
189
+ return containsMaliciousPatterns(content);
110
190
  }
111
191
 
112
192
  private containsPromptInjection(content: string): boolean {
113
- return PROMPT_INJECTION_PATTERNS.some((pattern) => pattern.test(content));
193
+ return containsPromptInjection(content);
114
194
  }
115
195
 
116
196
  private evaluateRules(content: string): Violation[] {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Pattern Matcher Service
3
- * Utility service for regex pattern matching
3
+ * Utility service for regex pattern matching with security validations
4
4
  */
5
5
 
6
6
  export interface PatternMatch {
@@ -10,8 +10,70 @@ export interface PatternMatch {
10
10
  position?: number;
11
11
  }
12
12
 
13
+ /**
14
+ * Validates that a regex pattern is safe to use
15
+ * Prevents ReDoS (Regular Expression Denial of Service) attacks
16
+ */
17
+ function isValidRegexPattern(pattern: string): boolean {
18
+ if (!pattern || typeof pattern !== "string") {
19
+ return false;
20
+ }
21
+
22
+ // Check for dangerous patterns that could cause ReDoS
23
+ const dangerousPatterns = [
24
+ /\([^)]*\+\([^)]*\+\)/, // Nested repeated groups
25
+ /\([^)]*\*[^)]*\*\)/, // Multiple nested stars
26
+ /\.\*\.*/, // Multiple wildcards
27
+ /\.\+\.+/, // Multiple repeated wildcards
28
+ ];
29
+
30
+ for (const dangerous of dangerousPatterns) {
31
+ if (dangerous.test(pattern)) {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ // Limit pattern length to prevent potential attacks
37
+ const MAX_PATTERN_LENGTH = 1000;
38
+ if (pattern.length > MAX_PATTERN_LENGTH) {
39
+ return false;
40
+ }
41
+
42
+ try {
43
+ // Test if pattern compiles without errors
44
+ new RegExp(pattern);
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Safely escapes special regex characters in user input
53
+ */
54
+ function escapeRegExp(str: string): string {
55
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
56
+ }
57
+
13
58
  class PatternMatcherService {
14
59
  matchPattern(content: string, pattern: string): PatternMatch {
60
+ // Validate inputs
61
+ if (!content || typeof content !== "string") {
62
+ return { pattern, matched: false };
63
+ }
64
+
65
+ if (!pattern || typeof pattern !== "string") {
66
+ return { pattern, matched: false };
67
+ }
68
+
69
+ // Validate pattern safety before using it
70
+ if (!isValidRegexPattern(pattern)) {
71
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
72
+ console.warn("[PatternMatcher] Invalid or unsafe pattern rejected:", pattern);
73
+ }
74
+ return { pattern, matched: false };
75
+ }
76
+
15
77
  try {
16
78
  const regex = new RegExp(pattern, "gi");
17
79
  const match = regex.exec(content);
@@ -26,11 +88,32 @@ class PatternMatcherService {
26
88
  }
27
89
 
28
90
  return { pattern, matched: false };
29
- } catch {
91
+ } catch (error) {
92
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
93
+ console.warn("[PatternMatcher] Regex error:", error);
94
+ }
30
95
  return { pattern, matched: false };
31
96
  }
32
97
  }
33
98
 
99
+ /**
100
+ * Safe string matching without regex (for user-provided search terms)
101
+ */
102
+ safeStringMatch(content: string, searchTerm: string): boolean {
103
+ if (!content || !searchTerm) {
104
+ return false;
105
+ }
106
+
107
+ try {
108
+ const escaped = escapeRegExp(searchTerm);
109
+ const regex = new RegExp(escaped, "gi");
110
+ return regex.test(content);
111
+ } catch {
112
+ // Fallback to simple includes if regex fails
113
+ return content.toLowerCase().includes(searchTerm.toLowerCase());
114
+ }
115
+ }
116
+
34
117
  matchAnyPattern(content: string, patterns: string[]): PatternMatch[] {
35
118
  return patterns.map((pattern) => this.matchPattern(content, pattern));
36
119
  }
@@ -43,8 +43,15 @@ export class CreationsWriter {
43
43
  await setDoc(docRef, data);
44
44
  if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[CreationsWriter] create() success");
45
45
  } catch (error) {
46
- if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[CreationsWriter] create() error", error);
47
- throw error;
46
+ const errorMessage = error instanceof Error ? error.message : String(error);
47
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
48
+ console.error("[CreationsWriter] create() error", {
49
+ userId,
50
+ creationId: creation.id,
51
+ error: errorMessage,
52
+ });
53
+ }
54
+ throw new Error(`Failed to create creation ${creation.id}: ${errorMessage}`);
48
55
  }
49
56
  }
50
57
 
@@ -52,7 +59,12 @@ export class CreationsWriter {
52
59
  if (typeof __DEV__ !== "undefined" && __DEV__) console.log("[CreationsWriter] update()", { userId, id });
53
60
 
54
61
  const docRef = this.pathResolver.getDocRef(userId, id);
55
- if (!docRef) return false;
62
+ if (!docRef) {
63
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
64
+ console.error("[CreationsWriter] update() - Firestore not initialized", { userId, id });
65
+ }
66
+ return false;
67
+ }
56
68
 
57
69
  try {
58
70
  const updateData: Record<string, unknown> = {};
@@ -62,7 +74,14 @@ export class CreationsWriter {
62
74
  await updateDoc(docRef, updateData);
63
75
  return true;
64
76
  } catch (error) {
65
- if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[CreationsWriter] update() error", error);
77
+ const errorMessage = error instanceof Error ? error.message : String(error);
78
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
79
+ console.error("[CreationsWriter] update() error", {
80
+ userId,
81
+ creationId: id,
82
+ error: errorMessage,
83
+ });
84
+ }
66
85
  return false;
67
86
  }
68
87
  }
@@ -73,7 +92,15 @@ export class CreationsWriter {
73
92
  try {
74
93
  await updateDoc(docRef, { deletedAt: new Date() });
75
94
  return true;
76
- } catch {
95
+ } catch (error) {
96
+ const errorMessage = error instanceof Error ? error.message : String(error);
97
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
98
+ console.error("[CreationsWriter] delete() error", {
99
+ userId,
100
+ creationId,
101
+ error: errorMessage,
102
+ });
103
+ }
77
104
  return false;
78
105
  }
79
106
  }
@@ -84,7 +111,15 @@ export class CreationsWriter {
84
111
  try {
85
112
  await deleteDoc(docRef);
86
113
  return true;
87
- } catch {
114
+ } catch (error) {
115
+ const errorMessage = error instanceof Error ? error.message : String(error);
116
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
117
+ console.error("[CreationsWriter] hardDelete() error", {
118
+ userId,
119
+ creationId,
120
+ error: errorMessage,
121
+ });
122
+ }
88
123
  return false;
89
124
  }
90
125
  }
@@ -95,7 +130,15 @@ export class CreationsWriter {
95
130
  try {
96
131
  await updateDoc(docRef, { deletedAt: null });
97
132
  return true;
98
- } catch {
133
+ } catch (error) {
134
+ const errorMessage = error instanceof Error ? error.message : String(error);
135
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
136
+ console.error("[CreationsWriter] restore() error", {
137
+ userId,
138
+ creationId,
139
+ error: errorMessage,
140
+ });
141
+ }
99
142
  return false;
100
143
  }
101
144
  }
@@ -106,7 +149,16 @@ export class CreationsWriter {
106
149
  try {
107
150
  await updateDoc(docRef, { isShared });
108
151
  return true;
109
- } catch {
152
+ } catch (error) {
153
+ const errorMessage = error instanceof Error ? error.message : String(error);
154
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
155
+ console.error("[CreationsWriter] updateShared() error", {
156
+ userId,
157
+ creationId,
158
+ isShared,
159
+ error: errorMessage,
160
+ });
161
+ }
110
162
  return false;
111
163
  }
112
164
  }
@@ -118,7 +170,10 @@ export class CreationsWriter {
118
170
  const docRef = this.pathResolver.getDocRef(userId, creationId);
119
171
  if (!docRef) {
120
172
  if (typeof __DEV__ !== "undefined" && __DEV__) {
121
- console.log("[CreationsWriter] updateFavorite() - no docRef");
173
+ console.warn("[CreationsWriter] updateFavorite() - Firestore not initialized", {
174
+ userId,
175
+ creationId,
176
+ });
122
177
  }
123
178
  return false;
124
179
  }
@@ -129,8 +184,14 @@ export class CreationsWriter {
129
184
  }
130
185
  return true;
131
186
  } catch (error) {
187
+ const errorMessage = error instanceof Error ? error.message : String(error);
132
188
  if (typeof __DEV__ !== "undefined" && __DEV__) {
133
- console.error("[CreationsWriter] updateFavorite() error", error);
189
+ console.error("[CreationsWriter] updateFavorite() error", {
190
+ userId,
191
+ creationId,
192
+ isFavorite,
193
+ error: errorMessage,
194
+ });
134
195
  }
135
196
  return false;
136
197
  }
@@ -143,19 +204,41 @@ export class CreationsWriter {
143
204
  try {
144
205
  await updateDoc(docRef, { rating, ratedAt: new Date() });
145
206
  if (description || rating) {
146
- await submitFeedback({
207
+ try {
208
+ await submitFeedback({
209
+ userId,
210
+ userEmail: null,
211
+ type: "creation_rating",
212
+ title: `Creation Rating: ${rating} Stars`,
213
+ description: description || `User rated creation ${rating} stars`,
214
+ rating,
215
+ status: "pending",
216
+ });
217
+ } catch (feedbackError) {
218
+ // Log but don't fail - the rating was saved successfully
219
+ const feedbackErrorMessage = feedbackError instanceof Error
220
+ ? feedbackError.message
221
+ : String(feedbackError);
222
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
223
+ console.warn("[CreationsWriter] rate() - feedback submission failed", {
224
+ userId,
225
+ creationId,
226
+ error: feedbackErrorMessage,
227
+ });
228
+ }
229
+ }
230
+ }
231
+ return true;
232
+ } catch (error) {
233
+ const errorMessage = error instanceof Error ? error.message : String(error);
234
+ if (typeof __DEV__ !== "undefined" && __DEV__) {
235
+ console.error("[CreationsWriter] rate() error", {
147
236
  userId,
148
- userEmail: null,
149
- type: "creation_rating",
150
- title: `Creation Rating: ${rating} Stars`,
151
- description: description || `User rated creation ${rating} stars`,
237
+ creationId,
152
238
  rating,
153
- status: "pending",
239
+ error: errorMessage,
154
240
  });
155
241
  }
156
- return true;
157
- } catch (error) {
158
- if (typeof __DEV__ !== "undefined" && __DEV__) console.error("[CreationsWriter] rate() error", error);
159
242
  return false;
160
243
  }
161
244
  }
@@ -88,10 +88,19 @@ export function useAdvancedFilter<T extends FilterableCreation>({
88
88
  setFilter(DEFAULT_CREATION_FILTER);
89
89
  }, []);
90
90
 
91
- const hasActiveFilters =
92
- filter.type !== "all" || filter.status !== "all" || !!filter.searchQuery;
93
- const activeMediaFilter = (filter.type as string) || "all";
94
- const activeStatusFilter = (filter.status as string) || "all";
91
+ const hasActiveFilters = useMemo(
92
+ () =>
93
+ filter.type !== "all" || filter.status !== "all" || !!filter.searchQuery,
94
+ [filter.type, filter.status, filter.searchQuery]
95
+ );
96
+ const activeMediaFilter = useMemo(
97
+ () => (filter.type as string) || "all",
98
+ [filter.type]
99
+ );
100
+ const activeStatusFilter = useMemo(
101
+ () => (filter.status as string) || "all",
102
+ [filter.status]
103
+ );
95
104
 
96
105
  return {
97
106
  filtered,
@@ -8,6 +8,7 @@
8
8
  import { useEffect, useRef, useCallback, useMemo } from "react";
9
9
  import { providerRegistry } from "../../../../infrastructure/services/provider-registry.service";
10
10
  import { QUEUE_STATUS, CREATION_STATUS } from "../../../../domain/constants/queue-status.constants";
11
+ import { GALLERY_POLL_INTERVAL_MS } from "../../../../infrastructure/constants/polling.constants";
11
12
  import {
12
13
  extractResultUrl,
13
14
  type FalResult,
@@ -17,8 +18,6 @@ import type { ICreationsRepository } from "../../domain/repositories/ICreationsR
17
18
 
18
19
  declare const __DEV__: boolean;
19
20
 
20
- const POLL_INTERVAL_MS = 5000; // Gallery polls slower than wizard
21
-
22
21
  export interface UseProcessingJobsPollerConfig {
23
22
  readonly userId?: string | null;
24
23
  readonly creations: Creation[];
@@ -43,12 +42,13 @@ export function useProcessingJobsPoller(
43
42
  const pollingRef = useRef<Set<string>>(new Set());
44
43
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
45
44
 
46
- // Find creations that need polling - stabilize reference with useMemo
45
+ // Find creations that need polling - use Set for O(1) lookups
47
46
  const processingJobIds = useMemo(
48
- () => creations
49
- .filter((c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model)
50
- .map((c) => c.id)
51
- .join(","),
47
+ () => new Set(
48
+ creations
49
+ .filter((c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model)
50
+ .map((c) => c.id)
51
+ ),
52
52
  [creations],
53
53
  );
54
54
 
@@ -56,8 +56,7 @@ export function useProcessingJobsPoller(
56
56
  () => creations.filter(
57
57
  (c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
58
58
  ),
59
- // eslint-disable-next-line react-hooks/exhaustive-deps
60
- [processingJobIds],
59
+ [creations],
61
60
  );
62
61
 
63
62
  const pollJob = useCallback(
@@ -134,6 +133,8 @@ export function useProcessingJobsPoller(
134
133
  clearInterval(intervalRef.current);
135
134
  intervalRef.current = null;
136
135
  }
136
+ // Clear polling set to prevent memory leak
137
+ pollingRef.current.clear();
137
138
  };
138
139
  }, [enabled, userId, processingJobs, pollJob]);
139
140
 
@@ -52,7 +52,7 @@ export const useFaceDetection = ({ aiAnalyzer, model }: UseFaceDetectionProps):
52
52
  setState(initialState);
53
53
  }, []);
54
54
 
55
- const isValid = state.result ? isValidFace(state.result) : false;
55
+ const isValid = state.result !== null && state.result !== undefined ? isValidFace(state.result) : false;
56
56
 
57
57
  return {
58
58
  state,
@@ -29,25 +29,29 @@ let flowStoreInstance: FlowStoreType | null = null;
29
29
 
30
30
  export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
31
31
  const storeRef = useRef<FlowStoreType | null>(null);
32
- const prevConfigRef = useRef<{ initialStepIndex?: number; initialStepId?: string } | undefined>(undefined);
32
+ const prevConfigRef = useRef<{ initialStepIndex?: number; initialStepId?: string; stepsCount: number } | undefined>(undefined);
33
+ const isResettingRef = useRef(false);
33
34
 
34
- // Detect config changes (initialStepIndex or initialStepId changed)
35
+ // Detect config changes (initialStepIndex, initialStepId, or steps changed)
35
36
  const configChanged =
36
37
  prevConfigRef.current !== undefined &&
37
38
  (prevConfigRef.current.initialStepIndex !== config.initialStepIndex ||
38
- prevConfigRef.current.initialStepId !== config.initialStepId);
39
+ prevConfigRef.current.initialStepId !== config.initialStepId ||
40
+ prevConfigRef.current.stepsCount !== config.steps.length);
39
41
 
40
- // If config changed, reset and recreate store
41
- if (configChanged) {
42
+ // If config changed, reset and recreate store (with guard against multiple resets)
43
+ if (configChanged && !isResettingRef.current) {
44
+ isResettingRef.current = true;
42
45
  if (flowStoreInstance) {
43
46
  flowStoreInstance.getState().reset();
44
47
  }
45
48
  flowStoreInstance = null;
46
49
  storeRef.current = null;
50
+ isResettingRef.current = false;
47
51
  }
48
52
 
49
53
  // Initialize store if needed
50
- if (!storeRef.current) {
54
+ if (!storeRef.current && !isResettingRef.current) {
51
55
  if (!flowStoreInstance) {
52
56
  flowStoreInstance = createFlowStore({
53
57
  steps: config.steps,
@@ -62,6 +66,7 @@ export const useFlow = (config: UseFlowConfig): UseFlowReturn => {
62
66
  prevConfigRef.current = {
63
67
  initialStepIndex: config.initialStepIndex,
64
68
  initialStepId: config.initialStepId,
69
+ stepsCount: config.steps.length,
65
70
  };
66
71
 
67
72
  const store = storeRef.current;