@umituz/react-native-ai-generation-content 1.61.37 → 1.61.38
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 +6 -4
- package/src/domain/entities/error.types.ts +1 -0
- package/src/domain/entities/polling.types.ts +1 -0
- package/src/domains/access-control/hooks/useAIFeatureGate.ts +1 -1
- package/src/domains/content-moderation/infrastructure/services/moderators/text.moderator.ts +0 -24
- package/src/domains/creations/domain/types/creation-filter.ts +1 -1
- package/src/domains/creations/infrastructure/repositories/CreationsFetcher.ts +3 -3
- package/src/domains/creations/infrastructure/repositories/CreationsRepository.ts +4 -9
- package/src/domains/creations/presentation/components/CreationActions.tsx +2 -1
- package/src/domains/creations/presentation/components/CreationImageViewer.tsx +1 -1
- package/src/domains/creations/presentation/components/CreationRating.tsx +1 -1
- package/src/domains/creations/presentation/components/CreationsGrid.tsx +1 -1
- package/src/domains/creations/presentation/components/CreationsHomeCard.tsx +1 -1
- package/src/domains/creations/presentation/components/FilterSheets.tsx +2 -2
- package/src/domains/creations/presentation/components/GalleryEmptyStates.tsx +1 -1
- package/src/domains/creations/presentation/components/GalleryHeader.tsx +2 -2
- package/src/domains/creations/presentation/hooks/useCreationsFilter.ts +1 -1
- package/src/domains/creations/presentation/hooks/useProcessingJobsPoller.ts +6 -14
- package/src/domains/generation/infrastructure/flow/step-builders.ts +0 -1
- package/src/domains/generation/infrastructure/flow/useFlow.ts +3 -0
- package/src/domains/generation/wizard/presentation/hooks/generation-result.utils.ts +14 -9
- package/src/index.ts +0 -3
- package/src/infrastructure/constants/index.ts +0 -6
- package/src/infrastructure/logging/logger.ts +0 -11
- package/src/infrastructure/services/job-poller.service.ts +17 -8
- package/src/infrastructure/services/video-feature-executor.service.ts +2 -2
- package/src/infrastructure/utils/error-classifier.util.ts +1 -1
- package/src/infrastructure/utils/result-validator.util.ts +3 -1
- package/src/infrastructure/utils/status-checker.util.ts +13 -10
- package/src/infrastructure/utils/url-extractor/media-extractors.ts +6 -4
- package/src/infrastructure/validation/input-validator.ts +5 -0
- package/src/presentation/components/AIGenerationForm.tsx +1 -2
- package/src/presentation/components/PhotoUploadCard/useBorderColor.ts +1 -1
- package/src/presentation/types/result-config.types.ts +7 -0
- package/src/utils/arrayUtils.ts +0 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.61.
|
|
3
|
+
"version": "1.61.38",
|
|
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",
|
|
@@ -65,12 +65,14 @@
|
|
|
65
65
|
"@tanstack/react-query": "^5.66.7",
|
|
66
66
|
"@tanstack/react-query-persist-client": "^5.66.7",
|
|
67
67
|
"@types/react": "~19.1.10",
|
|
68
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
69
|
-
"@typescript-eslint/parser": "^8.
|
|
68
|
+
"@typescript-eslint/eslint-plugin": "^8.54.0",
|
|
69
|
+
"@typescript-eslint/parser": "^8.54.0",
|
|
70
70
|
"@umituz/react-native-design-system": "^4.23.42",
|
|
71
71
|
"@umituz/react-native-firebase": "^1.13.87",
|
|
72
72
|
"@umituz/react-native-subscription": "^2.27.23",
|
|
73
|
-
"eslint": "^9.
|
|
73
|
+
"eslint": "^9.39.2",
|
|
74
|
+
"eslint-plugin-react": "^7.37.5",
|
|
75
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
74
76
|
"expo-apple-authentication": "^8.0.8",
|
|
75
77
|
"expo-application": "^7.0.8",
|
|
76
78
|
"expo-auth-session": "^5.0.0",
|
|
@@ -65,7 +65,7 @@ export function useAIFeatureGate(
|
|
|
65
65
|
// Configure feature gate from subscription package
|
|
66
66
|
const { requireFeature: requireFeatureFromPackage } = useFeatureGate({
|
|
67
67
|
isAuthenticated,
|
|
68
|
-
onShowAuthModal: (cb) => showAuthModal(cb),
|
|
68
|
+
onShowAuthModal: (cb?: () => void) => showAuthModal(cb),
|
|
69
69
|
hasSubscription: isPremium,
|
|
70
70
|
creditBalance,
|
|
71
71
|
requiredCredits: creditCost,
|
|
@@ -91,30 +91,6 @@ function containsPromptInjection(content: string): boolean {
|
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
// Kept for reference but no longer used directly - using safer functions above
|
|
95
|
-
const MALICIOUS_CODE_PATTERNS = [
|
|
96
|
-
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
|
97
|
-
/javascript:/gi,
|
|
98
|
-
/on\w+\s*=/gi,
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
const PROMPT_INJECTION_PATTERNS = [
|
|
102
|
-
/ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?|rules?)/gi,
|
|
103
|
-
/disregard\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?)/gi,
|
|
104
|
-
/forget\s+(all\s+)?(previous|prior|your)\s+(instructions?|prompts?|rules?)/gi,
|
|
105
|
-
/you\s+are\s+now\s+(a|an)\s+/gi,
|
|
106
|
-
/act\s+as\s+(if|though)\s+you/gi,
|
|
107
|
-
/pretend\s+(you\s+are|to\s+be)/gi,
|
|
108
|
-
/bypass\s+(your\s+)?(safety|content|moderation)/gi,
|
|
109
|
-
/override\s+(your\s+)?(restrictions?|limitations?|rules?)/gi,
|
|
110
|
-
/jailbreak/gi,
|
|
111
|
-
/DAN\s*mode/gi,
|
|
112
|
-
/developer\s+mode\s+(enabled|on|activated)/gi,
|
|
113
|
-
/system\s*:\s*/gi,
|
|
114
|
-
/\[system\]/gi,
|
|
115
|
-
/<<\s*sys\s*>>/gi,
|
|
116
|
-
];
|
|
117
|
-
|
|
118
94
|
class TextModerator extends BaseModerator {
|
|
119
95
|
private maxLength = DEFAULT_MAX_TEXT_LENGTH;
|
|
120
96
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { CreationTypeId, CreationStatus, CreationCategory } from "./creation-types";
|
|
7
|
+
import { getCategoryForCreation } from "./creation-categories";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Filter options for querying creations
|
|
@@ -118,7 +119,6 @@ export function calculateCreationStats(
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
// Calculate category counts based on OUTPUT content (most reliable)
|
|
121
|
-
const { getCategoryForCreation } = require("./creation-categories");
|
|
122
122
|
for (const creation of creations) {
|
|
123
123
|
const category = getCategoryForCreation(creation) as Exclude<CreationCategory, "all">;
|
|
124
124
|
stats.byCategory[category]++;
|
|
@@ -52,7 +52,7 @@ export class CreationsFetcher {
|
|
|
52
52
|
});
|
|
53
53
|
|
|
54
54
|
// Filter out soft-deleted creations
|
|
55
|
-
return allCreations.filter((creation) => !creation.deletedAt);
|
|
55
|
+
return allCreations.filter((creation: Creation) => !creation.deletedAt);
|
|
56
56
|
} catch (error) {
|
|
57
57
|
if (__DEV__) {
|
|
58
58
|
|
|
@@ -132,7 +132,7 @@ export class CreationsFetcher {
|
|
|
132
132
|
return creation;
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
const filtered = allCreations.filter((c) => !c.deletedAt);
|
|
135
|
+
const filtered = allCreations.filter((c: Creation) => !c.deletedAt);
|
|
136
136
|
|
|
137
137
|
if (__DEV__) {
|
|
138
138
|
console.log("[CreationsFetcher] Realtime update:", filtered.length);
|
|
@@ -140,7 +140,7 @@ export class CreationsFetcher {
|
|
|
140
140
|
|
|
141
141
|
onData(filtered);
|
|
142
142
|
},
|
|
143
|
-
(error) => {
|
|
143
|
+
(error: Error) => {
|
|
144
144
|
if (__DEV__) {
|
|
145
145
|
console.error("[CreationsFetcher] subscribeToAll() ERROR", error);
|
|
146
146
|
}
|
|
@@ -43,22 +43,17 @@ export class CreationsRepository
|
|
|
43
43
|
collectionName: string,
|
|
44
44
|
options?: RepositoryOptions,
|
|
45
45
|
) {
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository - Constructor start");
|
|
48
48
|
super();
|
|
49
49
|
|
|
50
50
|
const documentMapper = options?.documentMapper ?? mapDocumentToCreation;
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const db = this.getDb();
|
|
55
|
-
|
|
56
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository - db:", db ? "available" : "null");
|
|
57
|
-
|
|
58
|
-
this.pathResolver = new FirestorePathResolver(collectionName, db);
|
|
52
|
+
// Initialize with default database (will be resolved by FirestorePathResolver)
|
|
53
|
+
this.pathResolver = new FirestorePathResolver(collectionName, null);
|
|
59
54
|
this.fetcher = new CreationsFetcher(this.pathResolver, documentMapper);
|
|
60
55
|
this.writer = new CreationsWriter(this.pathResolver);
|
|
61
|
-
|
|
56
|
+
|
|
62
57
|
if (typeof __DEV__ !== "undefined" && __DEV__) console.log("📍 [LIFECYCLE] CreationsRepository - Constructor end");
|
|
63
58
|
}
|
|
64
59
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
View,
|
|
9
9
|
StyleSheet,
|
|
10
10
|
TouchableOpacity,
|
|
11
|
+
type GestureResponderEvent,
|
|
11
12
|
} from "react-native";
|
|
12
13
|
import {
|
|
13
14
|
useAppDesignTokens,
|
|
@@ -99,7 +100,7 @@ export function CreationActions({
|
|
|
99
100
|
action.filled && styles.actionButtonFilled,
|
|
100
101
|
action.disabled && styles.actionButtonDisabled,
|
|
101
102
|
]}
|
|
102
|
-
onPress={(e) => {
|
|
103
|
+
onPress={(e: GestureResponderEvent) => {
|
|
103
104
|
e.stopPropagation();
|
|
104
105
|
action.onPress();
|
|
105
106
|
}}
|
|
@@ -21,7 +21,7 @@ export const CreationImageViewer: React.FC<CreationImageViewerProps> = ({
|
|
|
21
21
|
onDismiss,
|
|
22
22
|
onIndexChange,
|
|
23
23
|
onImageEdit,
|
|
24
|
-
}) => {
|
|
24
|
+
}: CreationImageViewerProps) => {
|
|
25
25
|
const handleImageChange = useCallback(async (uri: string, idx: number) => {
|
|
26
26
|
const creation = creations[idx];
|
|
27
27
|
if (creation && onImageEdit) {
|
|
@@ -101,7 +101,7 @@ export function CreationsGrid<T extends CreationCardData>({
|
|
|
101
101
|
<FlatList
|
|
102
102
|
data={creations}
|
|
103
103
|
renderItem={renderItem}
|
|
104
|
-
keyExtractor={(item) => item.id}
|
|
104
|
+
keyExtractor={(item: T) => item.id}
|
|
105
105
|
ListHeaderComponent={ListHeaderComponent}
|
|
106
106
|
ListEmptyComponent={ListEmptyComponent}
|
|
107
107
|
contentContainerStyle={[
|
|
@@ -165,7 +165,7 @@ export function CreationsHomeCard({
|
|
|
165
165
|
<FlatList
|
|
166
166
|
data={displayItems}
|
|
167
167
|
renderItem={renderItem}
|
|
168
|
-
keyExtractor={(item) => item.id}
|
|
168
|
+
keyExtractor={(item: Creation) => item.id}
|
|
169
169
|
horizontal
|
|
170
170
|
showsHorizontalScrollIndicator={false}
|
|
171
171
|
contentContainerStyle={styles.thumbnailList}
|
|
@@ -27,7 +27,7 @@ export const StatusFilterSheet: React.FC<FilterSheetConfig> = ({
|
|
|
27
27
|
onClear,
|
|
28
28
|
title,
|
|
29
29
|
clearLabel
|
|
30
|
-
}) => (
|
|
30
|
+
}: FilterSheetConfig) => (
|
|
31
31
|
<FilterSheet
|
|
32
32
|
visible={visible}
|
|
33
33
|
onClose={onClose}
|
|
@@ -49,7 +49,7 @@ export const MediaFilterSheet: React.FC<FilterSheetConfig> = ({
|
|
|
49
49
|
onClear,
|
|
50
50
|
title,
|
|
51
51
|
clearLabel
|
|
52
|
-
}) => (
|
|
52
|
+
}: FilterSheetConfig) => (
|
|
53
53
|
<FilterSheet
|
|
54
54
|
visible={visible}
|
|
55
55
|
onClose={onClose}
|
|
@@ -76,7 +76,7 @@ export function GalleryEmptyStates({
|
|
|
76
76
|
if (isLoading && (!creations || creations?.length === 0)) {
|
|
77
77
|
return (
|
|
78
78
|
<View style={styles.skeletonContainer}>
|
|
79
|
-
{[1, 2, 3].map((i) => (
|
|
79
|
+
{[1, 2, 3].map((i: number) => (
|
|
80
80
|
<CreationCardSkeleton key={i} tokens={tokens} />
|
|
81
81
|
))}
|
|
82
82
|
</View>
|
|
@@ -28,7 +28,7 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
|
|
|
28
28
|
filterButtons = [],
|
|
29
29
|
showFilter = true,
|
|
30
30
|
style,
|
|
31
|
-
}) => {
|
|
31
|
+
}: GalleryHeaderProps) => {
|
|
32
32
|
const tokens = useAppDesignTokens();
|
|
33
33
|
const styles = useStyles(tokens);
|
|
34
34
|
|
|
@@ -44,7 +44,7 @@ export const GalleryHeader: React.FC<GalleryHeaderProps> = ({
|
|
|
44
44
|
</View>
|
|
45
45
|
{showFilter && filterButtons.length > 0 && (
|
|
46
46
|
<View style={styles.filterRow}>
|
|
47
|
-
{filterButtons.map((btn) => (
|
|
47
|
+
{filterButtons.map((btn: FilterButtonConfig) => (
|
|
48
48
|
<TouchableOpacity
|
|
49
49
|
key={btn.id}
|
|
50
50
|
onPress={() => {
|
|
@@ -27,7 +27,7 @@ export function useCreationsFilter({
|
|
|
27
27
|
}: UseCreationsFilterProps): UseCreationsFilterReturn {
|
|
28
28
|
|
|
29
29
|
const filtered = useMemo(() => {
|
|
30
|
-
if (!creations) return [];
|
|
30
|
+
if (!creations || creations.length === 0) return [];
|
|
31
31
|
|
|
32
32
|
return creations.filter((creation) => {
|
|
33
33
|
// Status filter
|
|
@@ -8,7 +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 {
|
|
11
|
+
import { DEFAULT_POLL_INTERVAL_MS } from "../../../../infrastructure/constants/polling.constants";
|
|
12
12
|
import {
|
|
13
13
|
extractResultUrl,
|
|
14
14
|
type FalResult,
|
|
@@ -42,16 +42,6 @@ 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
|
-
// Find creations that need polling - use Set for O(1) lookups
|
|
46
|
-
const processingJobIds = useMemo(
|
|
47
|
-
() => new Set(
|
|
48
|
-
creations
|
|
49
|
-
.filter((c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model)
|
|
50
|
-
.map((c) => c.id)
|
|
51
|
-
),
|
|
52
|
-
[creations],
|
|
53
|
-
);
|
|
54
|
-
|
|
55
45
|
const processingJobs = useMemo(
|
|
56
46
|
() => creations.filter(
|
|
57
47
|
(c) => c.status === CREATION_STATUS.PROCESSING && c.requestId && c.model,
|
|
@@ -126,15 +116,17 @@ export function useProcessingJobsPoller(
|
|
|
126
116
|
// Set up interval polling
|
|
127
117
|
intervalRef.current = setInterval(() => {
|
|
128
118
|
processingJobs.forEach((job) => void pollJob(job));
|
|
129
|
-
},
|
|
119
|
+
}, DEFAULT_POLL_INTERVAL_MS);
|
|
130
120
|
|
|
131
121
|
return () => {
|
|
122
|
+
// Clear polling set first to prevent new operations
|
|
123
|
+
pollingRef.current.clear();
|
|
124
|
+
|
|
125
|
+
// Then clear interval
|
|
132
126
|
if (intervalRef.current) {
|
|
133
127
|
clearInterval(intervalRef.current);
|
|
134
128
|
intervalRef.current = null;
|
|
135
129
|
}
|
|
136
|
-
// Clear polling set to prevent memory leak
|
|
137
|
-
pollingRef.current.clear();
|
|
138
130
|
};
|
|
139
131
|
}, [enabled, userId, processingJobs, pollJob]);
|
|
140
132
|
|
|
@@ -31,8 +31,10 @@ function checkForErrors(result: FalResult): void {
|
|
|
31
31
|
// Check for FAL API error format: {detail: [{msg, type}]}
|
|
32
32
|
if (result.detail && Array.isArray(result.detail) && result.detail.length > 0) {
|
|
33
33
|
const firstError = result.detail[0];
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
if (!firstError) return;
|
|
35
|
+
|
|
36
|
+
const errorType = firstError.type || "unknown";
|
|
37
|
+
const errorMsg = firstError.msg || "Generation failed";
|
|
36
38
|
|
|
37
39
|
// Map error type to translation key
|
|
38
40
|
if (errorType === "content_policy_violation") {
|
|
@@ -47,7 +49,7 @@ function checkForErrors(result: FalResult): void {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// Check for simple error field
|
|
50
|
-
if (result.error) {
|
|
52
|
+
if (result.error && typeof result.error === "string" && result.error.length > 0) {
|
|
51
53
|
throw new Error(result.error);
|
|
52
54
|
}
|
|
53
55
|
}
|
|
@@ -62,25 +64,28 @@ export function extractResultUrl(result: FalResult): GenerationUrls {
|
|
|
62
64
|
checkForErrors(result);
|
|
63
65
|
|
|
64
66
|
// Video result
|
|
65
|
-
if (result.video?.url) {
|
|
67
|
+
if (result.video?.url && typeof result.video.url === "string") {
|
|
66
68
|
return { videoUrl: result.video.url };
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
// Output URL (some models return direct URL)
|
|
70
|
-
if (typeof result.output === "string" && result.output.startsWith("http")) {
|
|
72
|
+
if (typeof result.output === "string" && result.output.length > 0 && result.output.startsWith("http")) {
|
|
71
73
|
if (result.output.includes(".mp4") || result.output.includes("video")) {
|
|
72
74
|
return { videoUrl: result.output };
|
|
73
75
|
}
|
|
74
76
|
return { imageUrl: result.output };
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
// Images array (most image models)
|
|
78
|
-
if (result.images
|
|
79
|
-
|
|
79
|
+
// Images array (most image models) with bounds checking
|
|
80
|
+
if (result.images && Array.isArray(result.images) && result.images.length > 0) {
|
|
81
|
+
const firstImage = result.images[0];
|
|
82
|
+
if (firstImage?.url && typeof firstImage.url === "string") {
|
|
83
|
+
return { imageUrl: firstImage.url };
|
|
84
|
+
}
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
// Single image
|
|
83
|
-
if (result.image?.url) {
|
|
88
|
+
if (result.image?.url && typeof result.image.url === "string") {
|
|
84
89
|
return { imageUrl: result.image.url };
|
|
85
90
|
}
|
|
86
91
|
|
package/src/index.ts
CHANGED
|
@@ -13,9 +13,3 @@ export * from "./content.constants";
|
|
|
13
13
|
|
|
14
14
|
// Storage Constants
|
|
15
15
|
export * from "./storage.constants";
|
|
16
|
-
|
|
17
|
-
/** Video generation timeout in milliseconds (5 minutes) - @deprecated Use DEFAULT_MAX_POLL_TIME_MS instead */
|
|
18
|
-
export const VIDEO_TIMEOUT_MS = 300000;
|
|
19
|
-
|
|
20
|
-
/** Maximum consecutive transient errors before failing - @deprecated Use DEFAULT_MAX_CONSECUTIVE_ERRORS instead */
|
|
21
|
-
export const MAX_TRANSIENT_ERRORS = 5;
|
|
@@ -52,16 +52,6 @@ class Logger {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
private formatMessage(entry: LogEntry): string {
|
|
56
|
-
const levelName = LogLevel[entry.level];
|
|
57
|
-
const timestamp = new Date(entry.timestamp).toISOString();
|
|
58
|
-
const contextStr = entry.context
|
|
59
|
-
? ` ${JSON.stringify(entry.context)}`
|
|
60
|
-
: "";
|
|
61
|
-
const errorStr = entry.error ? ` ${entry.error.message}` : "";
|
|
62
|
-
return `[${timestamp}] [${levelName}] ${entry.message}${contextStr}${errorStr}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
55
|
debug(message: string, context?: LogContext): void {
|
|
66
56
|
if (!this.shouldLog(LogLevel.DEBUG)) return;
|
|
67
57
|
|
|
@@ -182,4 +172,3 @@ export function setProductionMode(): void {
|
|
|
182
172
|
logger.setMinLevel(LogLevel.WARN);
|
|
183
173
|
}
|
|
184
174
|
|
|
185
|
-
export type { LogEntry, LogContext };
|
|
@@ -28,9 +28,10 @@ export async function pollJob<T = unknown>(
|
|
|
28
28
|
} = options;
|
|
29
29
|
|
|
30
30
|
const pollingConfig = { ...DEFAULT_POLLING_CONFIG, ...config };
|
|
31
|
-
const { maxAttempts, maxTotalTimeMs } = pollingConfig;
|
|
31
|
+
const { maxAttempts, maxTotalTimeMs, maxConsecutiveErrors } = pollingConfig;
|
|
32
32
|
|
|
33
33
|
const startTime = Date.now();
|
|
34
|
+
let consecutiveTransientErrors = 0;
|
|
34
35
|
|
|
35
36
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
36
37
|
// Check total time limit
|
|
@@ -97,13 +98,21 @@ export async function pollJob<T = unknown>(
|
|
|
97
98
|
elapsedMs: Date.now() - startTime,
|
|
98
99
|
};
|
|
99
100
|
}
|
|
100
|
-
} catch
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
} catch {
|
|
102
|
+
consecutiveTransientErrors++;
|
|
103
|
+
|
|
104
|
+
// Check if we've hit max consecutive transient errors
|
|
105
|
+
if (maxConsecutiveErrors && consecutiveTransientErrors >= maxConsecutiveErrors) {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
error: new Error(`Too many consecutive errors (${consecutiveTransientErrors})`),
|
|
109
|
+
attempts: attempt + 1,
|
|
110
|
+
elapsedMs: Date.now() - startTime,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Continue polling on transient errors
|
|
115
|
+
continue;
|
|
107
116
|
}
|
|
108
117
|
}
|
|
109
118
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { extractErrorMessage, checkFalApiError, validateProvider, prepareVideoInputData } from "../utils";
|
|
8
8
|
import { extractVideoResult } from "../utils/url-extractor";
|
|
9
|
-
import {
|
|
9
|
+
import { DEFAULT_MAX_POLL_TIME_MS } from "../constants";
|
|
10
10
|
import type { VideoFeatureType } from "../../domain/interfaces";
|
|
11
11
|
import type { ExecuteVideoFeatureOptions, VideoFeatureResult, VideoFeatureRequest } from "./video-feature-executor.types";
|
|
12
12
|
|
|
@@ -38,7 +38,7 @@ export async function executeVideoFeature(
|
|
|
38
38
|
const input = provider.buildVideoFeatureInput(featureType, inputData);
|
|
39
39
|
|
|
40
40
|
const result = await provider.subscribe(model, input, {
|
|
41
|
-
timeoutMs:
|
|
41
|
+
timeoutMs: DEFAULT_MAX_POLL_TIME_MS,
|
|
42
42
|
onQueueUpdate: (status) => onStatusChange?.(status.status),
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -143,7 +143,7 @@ export function classifyError(error: unknown): AIErrorInfo {
|
|
|
143
143
|
|
|
144
144
|
export function isTransientError(error: unknown): boolean {
|
|
145
145
|
const info = classifyError(error);
|
|
146
|
-
return info.retryable;
|
|
146
|
+
return info.retryable ?? false;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
export function isPermanentError(error: unknown): boolean {
|
|
@@ -77,11 +77,13 @@ export function validateResult(
|
|
|
77
77
|
|
|
78
78
|
if (typeof value === "object" && value !== null) {
|
|
79
79
|
const nested = value as Record<string, unknown>;
|
|
80
|
+
// Cache Object.keys result for performance
|
|
81
|
+
const nestedKeys = Object.keys(nested);
|
|
80
82
|
return !!(
|
|
81
83
|
nested.url ||
|
|
82
84
|
nested.image_url ||
|
|
83
85
|
nested.video_url ||
|
|
84
|
-
|
|
86
|
+
nestedKeys.length > 0
|
|
85
87
|
);
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -50,7 +50,7 @@ export function checkStatusForErrors(
|
|
|
50
50
|
safeString(status, "message");
|
|
51
51
|
const hasStatusError = statusError.length > 0;
|
|
52
52
|
|
|
53
|
-
// Check logs array for ERROR/FATAL level logs
|
|
53
|
+
// Check logs array for ERROR/FATAL level logs with bounds checking
|
|
54
54
|
const rawLogs = (status as JobStatus)?.logs;
|
|
55
55
|
const logs = Array.isArray(rawLogs) ? rawLogs : [];
|
|
56
56
|
const errorLogs = logs.filter((log: AILogEntry) => {
|
|
@@ -59,14 +59,17 @@ export function checkStatusForErrors(
|
|
|
59
59
|
});
|
|
60
60
|
const hasErrorLog = errorLogs.length > 0;
|
|
61
61
|
|
|
62
|
-
// Extract error message from logs
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
// Extract error message from logs with safer access
|
|
63
|
+
let errorLogMessage: string | undefined;
|
|
64
|
+
if (errorLogs.length > 0) {
|
|
65
|
+
const firstErrorLog = errorLogs[0];
|
|
66
|
+
if (firstErrorLog && typeof firstErrorLog === "object") {
|
|
67
|
+
errorLogMessage =
|
|
68
|
+
safeString(firstErrorLog, "message") ||
|
|
69
|
+
safeString(firstErrorLog, "text") ||
|
|
70
|
+
safeString(firstErrorLog, "content");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
70
73
|
|
|
71
74
|
// Combine error messages
|
|
72
75
|
const errorMessage = statusError || errorLogMessage;
|
|
@@ -78,7 +81,7 @@ export function checkStatusForErrors(
|
|
|
78
81
|
return {
|
|
79
82
|
status: statusString,
|
|
80
83
|
hasError: hasStatusError || hasErrorLog,
|
|
81
|
-
errorMessage: errorMessage ?
|
|
84
|
+
errorMessage: errorMessage && errorMessage.length > 0 ? errorMessage : undefined,
|
|
82
85
|
shouldStop,
|
|
83
86
|
};
|
|
84
87
|
}
|
|
@@ -47,14 +47,16 @@ export function extractImageUrls(result: unknown): string[] {
|
|
|
47
47
|
return urls;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
// Check images array
|
|
51
|
-
if (Array.isArray(resultObj.images)) {
|
|
50
|
+
// Check images array with bounds checking
|
|
51
|
+
if (Array.isArray(resultObj.images) && resultObj.images.length > 0) {
|
|
52
52
|
for (const img of resultObj.images) {
|
|
53
|
+
if (!img) continue; // Skip null/undefined items
|
|
54
|
+
|
|
53
55
|
if (typeof img === "string" && img.length > 0) {
|
|
54
56
|
urls.push(img);
|
|
55
|
-
} else if (
|
|
57
|
+
} else if (typeof img === "object") {
|
|
56
58
|
const imgObj = img as Record<string, unknown>;
|
|
57
|
-
if (typeof imgObj.url === "string") {
|
|
59
|
+
if (typeof imgObj.url === "string" && imgObj.url.length > 0) {
|
|
58
60
|
urls.push(imgObj.url);
|
|
59
61
|
}
|
|
60
62
|
}
|
|
@@ -50,7 +50,12 @@ export function sanitizeString(input: unknown): string {
|
|
|
50
50
|
.trim()
|
|
51
51
|
.replace(/[<>]/g, "") // Remove potential HTML tags
|
|
52
52
|
.replace(/javascript:/gi, "") // Remove javascript: protocol
|
|
53
|
+
.replace(/data:/gi, "") // Remove data: protocol
|
|
54
|
+
.replace(/vbscript:/gi, "") // Remove vbscript: protocol
|
|
53
55
|
.replace(/on\w+\s*=/gi, "") // Remove event handlers
|
|
56
|
+
.replace(/--/g, "") // Remove SQL comment sequences
|
|
57
|
+
.replace(/;\s*drop\s+/gi, "") // Remove SQL injection attempts
|
|
58
|
+
.replace(/['"\\]/g, "") // Remove quotes and backslashes
|
|
54
59
|
.slice(0, 10000); // Limit length
|
|
55
60
|
}
|
|
56
61
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import {
|
|
4
4
|
AtomicText,
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
useAppDesignTokens,
|
|
7
7
|
} from "@umituz/react-native-design-system";
|
|
8
8
|
|
|
9
|
-
declare const __DEV__: boolean;
|
|
10
9
|
import { StyleSelector } from "./selectors/StyleSelector";
|
|
11
10
|
import { DurationSelector } from "./selectors/DurationSelector";
|
|
12
11
|
import { AspectRatioSelector } from "./selectors/AspectRatioSelector";
|
|
@@ -27,5 +27,5 @@ export function useBorderColor({
|
|
|
27
27
|
if (isValid === true) return tokens.colors.success;
|
|
28
28
|
if (isValid === false) return tokens.colors.error;
|
|
29
29
|
return tokens.colors.borderLight;
|
|
30
|
-
}, [isValidating, isValid, showValidationStatus, tokens]);
|
|
30
|
+
}, [isValidating, isValid, showValidationStatus, tokens.colors.borderLight, tokens.colors.primary, tokens.colors.success, tokens.colors.error]);
|
|
31
31
|
}
|
|
@@ -78,6 +78,7 @@ export interface ResultActionButton {
|
|
|
78
78
|
export interface ResultActionsConfig {
|
|
79
79
|
share?: ResultActionButton;
|
|
80
80
|
save?: ResultActionButton;
|
|
81
|
+
retry?: ResultActionButton;
|
|
81
82
|
layout?: "horizontal" | "vertical" | "grid";
|
|
82
83
|
buttonSpacing?: number;
|
|
83
84
|
spacing?: {
|
|
@@ -168,6 +169,12 @@ export const DEFAULT_RESULT_CONFIG: ResultConfig = {
|
|
|
168
169
|
variant: "secondary",
|
|
169
170
|
position: "bottom",
|
|
170
171
|
},
|
|
172
|
+
retry: {
|
|
173
|
+
enabled: true,
|
|
174
|
+
icon: "refresh",
|
|
175
|
+
variant: "outline",
|
|
176
|
+
position: "bottom",
|
|
177
|
+
},
|
|
171
178
|
layout: "horizontal",
|
|
172
179
|
buttonSpacing: 10,
|
|
173
180
|
spacing: {
|
package/src/utils/arrayUtils.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deduplicates an array of objects based on a specific key.
|
|
3
|
-
* Keeps the first occurrence of each item with a unique key.
|
|
4
|
-
*
|
|
5
|
-
* @param array The array to deduplicate
|
|
6
|
-
* @param keyFn A function that returns the unique key for each item
|
|
7
|
-
* @returns A new array with duplicate items removed
|
|
8
|
-
*/
|
|
9
|
-
export const distinctBy = <T>(array: readonly T[], keyFn: (item: T) => string | number): T[] => {
|
|
10
|
-
const seen = new Set<string | number>();
|
|
11
|
-
const result: T[] = [];
|
|
12
|
-
|
|
13
|
-
for (const item of array) {
|
|
14
|
-
const key = keyFn(item);
|
|
15
|
-
if (!seen.has(key)) {
|
|
16
|
-
seen.add(key);
|
|
17
|
-
result.push(item);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return result;
|
|
22
|
-
};
|