@umituz/react-native-ai-generation-content 1.82.9 → 1.83.1
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/creations/domain/constants/creation-fields.constants.ts +7 -3
- package/src/domains/creations/domain/entities/Creation.ts +22 -35
- package/src/domains/creations/infrastructure/repositories/creation-create.operations.ts +1 -0
- package/src/domains/creations/infrastructure/repositories/creation-update.operations.ts +2 -18
- package/src/domains/generation/wizard/infrastructure/utils/creation-persistence.types.ts +2 -0
- package/src/domains/generation/wizard/infrastructure/utils/creation-save-operations.ts +5 -3
- package/src/domains/generation/wizard/infrastructure/utils/creation-update-operations.ts +8 -1
- package/src/domains/generation/wizard/presentation/hooks/useVideoQueueGeneration.ts +45 -16
- package/src/domains/generation/wizard/presentation/hooks/videoQueuePoller.ts +19 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-ai-generation-content",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.83.1",
|
|
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",
|
|
@@ -32,18 +32,20 @@ export const CREATION_FIELDS = {
|
|
|
32
32
|
|
|
33
33
|
// Timestamps
|
|
34
34
|
CREATED_AT: "createdAt" as const,
|
|
35
|
+
STARTED_AT: "startedAt" as const,
|
|
35
36
|
UPDATED_AT: "updatedAt" as const,
|
|
36
37
|
DELETED_AT: "deletedAt" as const,
|
|
37
38
|
RATED_AT: "ratedAt" as const,
|
|
39
|
+
COMPLETED_AT: "completedAt" as const,
|
|
40
|
+
|
|
41
|
+
// Duration (ms elapsed from startedAt to completedAt)
|
|
42
|
+
DURATION_MS: "durationMs" as const,
|
|
38
43
|
|
|
39
44
|
// User interactions
|
|
40
45
|
IS_FAVORITE: "isFavorite" as const,
|
|
41
46
|
IS_SHARED: "isShared" as const,
|
|
42
47
|
RATING: "rating" as const,
|
|
43
48
|
|
|
44
|
-
// Completion timestamp
|
|
45
|
-
COMPLETED_AT: "completedAt" as const,
|
|
46
|
-
|
|
47
49
|
// AI provider metadata
|
|
48
50
|
REQUEST_ID: "requestId" as const,
|
|
49
51
|
MODEL: "model" as const,
|
|
@@ -73,6 +75,8 @@ export const UPDATABLE_FIELDS: ReadonlyArray<CreationFieldName> = [
|
|
|
73
75
|
CREATION_FIELDS.MODEL,
|
|
74
76
|
CREATION_FIELDS.PROMPT,
|
|
75
77
|
CREATION_FIELDS.COMPLETED_AT,
|
|
78
|
+
CREATION_FIELDS.STARTED_AT,
|
|
79
|
+
CREATION_FIELDS.DURATION_MS,
|
|
76
80
|
] as const;
|
|
77
81
|
|
|
78
82
|
/**
|
|
@@ -39,11 +39,17 @@ export interface Creation {
|
|
|
39
39
|
readonly requestId?: string;
|
|
40
40
|
readonly model?: string;
|
|
41
41
|
// Timestamps
|
|
42
|
-
readonly
|
|
42
|
+
readonly startedAt?: Date; // When generation was submitted to the queue
|
|
43
|
+
readonly completedAt?: Date; // When generation finished (success or failure)
|
|
44
|
+
readonly durationMs?: number; // Elapsed ms from startedAt to completedAt
|
|
43
45
|
// Soft delete - if set, the creation is considered deleted
|
|
44
46
|
readonly deletedAt?: Date;
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
interface FirebaseTimestamp {
|
|
50
|
+
toDate: () => Date;
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
export interface CreationDocument {
|
|
48
54
|
readonly uri?: string;
|
|
49
55
|
readonly prompt?: string;
|
|
@@ -61,30 +67,30 @@ export interface CreationDocument {
|
|
|
61
67
|
readonly rating?: number;
|
|
62
68
|
readonly ratedAt?: FirebaseTimestamp | Date | null;
|
|
63
69
|
readonly createdAt: FirebaseTimestamp | Date;
|
|
70
|
+
readonly startedAt?: FirebaseTimestamp | Date | null;
|
|
64
71
|
readonly completedAt?: FirebaseTimestamp | Date | null;
|
|
65
72
|
readonly deletedAt?: FirebaseTimestamp | Date | null;
|
|
66
73
|
readonly updatedAt?: FirebaseTimestamp | Date | null;
|
|
74
|
+
readonly durationMs?: number;
|
|
67
75
|
// Background job tracking
|
|
68
76
|
readonly requestId?: string;
|
|
69
77
|
readonly model?: string;
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
function toDate(value: FirebaseTimestamp | Date | null | undefined): Date | undefined {
|
|
81
|
+
if (!value) return undefined;
|
|
82
|
+
if (value instanceof Date) return value;
|
|
83
|
+
if (typeof value === "object" && "toDate" in value && typeof (value as FirebaseTimestamp).toDate === "function") {
|
|
84
|
+
return (value as FirebaseTimestamp).toDate();
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
export function mapDocumentToCreation(
|
|
77
90
|
id: string,
|
|
78
91
|
data: CreationDocument,
|
|
79
92
|
): Creation {
|
|
80
|
-
|
|
81
|
-
if (data.createdAt instanceof Date) {
|
|
82
|
-
creationDate = data.createdAt;
|
|
83
|
-
} else if (data.createdAt && typeof data.createdAt === "object" && "toDate" in data.createdAt && typeof data.createdAt.toDate === "function") {
|
|
84
|
-
creationDate = data.createdAt.toDate();
|
|
85
|
-
} else {
|
|
86
|
-
creationDate = new Date();
|
|
87
|
-
}
|
|
93
|
+
const creationDate = toDate(data.createdAt) ?? new Date();
|
|
88
94
|
|
|
89
95
|
// Get URI from output or direct fields
|
|
90
96
|
const uri = data.output?.imageUrl ||
|
|
@@ -94,27 +100,6 @@ export function mapDocumentToCreation(
|
|
|
94
100
|
data.uri ||
|
|
95
101
|
"";
|
|
96
102
|
|
|
97
|
-
let ratedAtDate: Date | undefined;
|
|
98
|
-
if (data.ratedAt instanceof Date) {
|
|
99
|
-
ratedAtDate = data.ratedAt;
|
|
100
|
-
} else if (data.ratedAt && typeof data.ratedAt === "object" && "toDate" in data.ratedAt && typeof data.ratedAt.toDate === "function") {
|
|
101
|
-
ratedAtDate = data.ratedAt.toDate();
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
let deletedAtDate: Date | undefined;
|
|
105
|
-
if (data.deletedAt instanceof Date) {
|
|
106
|
-
deletedAtDate = data.deletedAt;
|
|
107
|
-
} else if (data.deletedAt && typeof data.deletedAt === "object" && "toDate" in data.deletedAt && typeof data.deletedAt.toDate === "function") {
|
|
108
|
-
deletedAtDate = data.deletedAt.toDate();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let completedAtDate: Date | undefined;
|
|
112
|
-
if (data.completedAt instanceof Date) {
|
|
113
|
-
completedAtDate = data.completedAt;
|
|
114
|
-
} else if (data.completedAt && typeof data.completedAt === "object" && "toDate" in data.completedAt && typeof data.completedAt.toDate === "function") {
|
|
115
|
-
completedAtDate = data.completedAt.toDate();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
103
|
return {
|
|
119
104
|
id,
|
|
120
105
|
uri,
|
|
@@ -126,12 +111,14 @@ export function mapDocumentToCreation(
|
|
|
126
111
|
isShared: data.isShared ?? false,
|
|
127
112
|
isFavorite: data.isFavorite ?? false,
|
|
128
113
|
rating: data.rating,
|
|
129
|
-
ratedAt:
|
|
114
|
+
ratedAt: toDate(data.ratedAt),
|
|
130
115
|
status: data.status as CreationStatus | undefined,
|
|
131
116
|
output: data.output ?? undefined,
|
|
132
117
|
requestId: data.requestId,
|
|
133
118
|
model: data.model,
|
|
134
|
-
|
|
135
|
-
|
|
119
|
+
startedAt: toDate(data.startedAt),
|
|
120
|
+
completedAt: toDate(data.completedAt),
|
|
121
|
+
durationMs: data.durationMs,
|
|
122
|
+
deletedAt: toDate(data.deletedAt),
|
|
136
123
|
};
|
|
137
124
|
}
|
|
@@ -27,6 +27,7 @@ export async function createCreation(
|
|
|
27
27
|
...(creation.prompt !== undefined && { prompt: creation.prompt }),
|
|
28
28
|
...(creation.requestId !== undefined && { requestId: creation.requestId }),
|
|
29
29
|
...(creation.model !== undefined && { model: creation.model }),
|
|
30
|
+
...(creation.startedAt !== undefined && { startedAt: creation.startedAt }),
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
try {
|
|
@@ -5,27 +5,11 @@
|
|
|
5
5
|
import { updateDoc } from "firebase/firestore";
|
|
6
6
|
import type { IPathResolver } from "@umituz/react-native-firebase";
|
|
7
7
|
import type { Creation } from "../../domain/entities/Creation";
|
|
8
|
-
import {
|
|
8
|
+
import { UPDATABLE_FIELDS } from "../../domain/constants";
|
|
9
9
|
|
|
10
10
|
declare const __DEV__: boolean;
|
|
11
11
|
|
|
12
|
-
export
|
|
13
|
-
CREATION_FIELDS.URI,
|
|
14
|
-
CREATION_FIELDS.STATUS,
|
|
15
|
-
CREATION_FIELDS.OUTPUT,
|
|
16
|
-
CREATION_FIELDS.IMAGE_URL,
|
|
17
|
-
CREATION_FIELDS.VIDEO_URL,
|
|
18
|
-
CREATION_FIELDS.METADATA,
|
|
19
|
-
CREATION_FIELDS.IS_SHARED,
|
|
20
|
-
CREATION_FIELDS.IS_FAVORITE,
|
|
21
|
-
CREATION_FIELDS.RATING,
|
|
22
|
-
CREATION_FIELDS.RATED_AT,
|
|
23
|
-
CREATION_FIELDS.DELETED_AT,
|
|
24
|
-
CREATION_FIELDS.REQUEST_ID,
|
|
25
|
-
CREATION_FIELDS.MODEL,
|
|
26
|
-
CREATION_FIELDS.PROMPT,
|
|
27
|
-
"type" as CreationFieldName,
|
|
28
|
-
] as const;
|
|
12
|
+
export { UPDATABLE_FIELDS };
|
|
29
13
|
|
|
30
14
|
export async function updateCreation(
|
|
31
15
|
pathResolver: IPathResolver,
|
|
@@ -26,4 +26,6 @@ export interface CompletedCreationData {
|
|
|
26
26
|
readonly imageUrl?: string;
|
|
27
27
|
readonly videoUrl?: string;
|
|
28
28
|
readonly thumbnailUrl?: string;
|
|
29
|
+
/** Unix timestamp (ms) when generation was submitted; used to compute durationMs */
|
|
30
|
+
readonly generationStartedAt?: number;
|
|
29
31
|
}
|
|
@@ -16,9 +16,11 @@ export async function saveAsProcessing(
|
|
|
16
16
|
repository: ICreationsRepository,
|
|
17
17
|
userId: string,
|
|
18
18
|
data: ProcessingCreationData
|
|
19
|
-
): Promise<string> {
|
|
19
|
+
): Promise<{ creationId: string; startedAt: Date }> {
|
|
20
20
|
const creationId = `${data.scenarioId}_${Date.now()}`;
|
|
21
21
|
|
|
22
|
+
const startedAt = new Date();
|
|
23
|
+
|
|
22
24
|
await repository.create(userId, {
|
|
23
25
|
id: creationId,
|
|
24
26
|
uri: "",
|
|
@@ -26,6 +28,7 @@ export async function saveAsProcessing(
|
|
|
26
28
|
prompt: data.prompt,
|
|
27
29
|
status: "processing" as const,
|
|
28
30
|
createdAt: new Date(),
|
|
31
|
+
startedAt,
|
|
29
32
|
isShared: false,
|
|
30
33
|
isFavorite: false,
|
|
31
34
|
requestId: data.requestId,
|
|
@@ -39,7 +42,6 @@ export async function saveAsProcessing(
|
|
|
39
42
|
...(data.aspectRatio && { aspectRatio: data.aspectRatio }),
|
|
40
43
|
...(data.provider && { provider: data.provider }),
|
|
41
44
|
...(data.outputType && { outputType: data.outputType }),
|
|
42
|
-
startedAt: new Date().toISOString(),
|
|
43
45
|
},
|
|
44
46
|
});
|
|
45
47
|
|
|
@@ -51,5 +53,5 @@ export async function saveAsProcessing(
|
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
return creationId;
|
|
56
|
+
return { creationId, startedAt };
|
|
55
57
|
}
|
|
@@ -22,11 +22,18 @@ export async function updateToCompleted(
|
|
|
22
22
|
if (data.videoUrl) output.videoUrl = data.videoUrl;
|
|
23
23
|
if (data.thumbnailUrl) output.thumbnailUrl = data.thumbnailUrl;
|
|
24
24
|
|
|
25
|
+
const completedAt = new Date();
|
|
26
|
+
const durationMs =
|
|
27
|
+
data.generationStartedAt !== undefined
|
|
28
|
+
? completedAt.getTime() - data.generationStartedAt
|
|
29
|
+
: undefined;
|
|
30
|
+
|
|
25
31
|
await repository.update(userId, creationId, {
|
|
26
32
|
uri: data.uri,
|
|
27
33
|
status: "completed" as const,
|
|
28
34
|
output,
|
|
29
|
-
completedAt
|
|
35
|
+
completedAt,
|
|
36
|
+
...(durationMs !== undefined && { durationMs }),
|
|
30
37
|
} as Partial<import("../../../../creations/domain/entities/Creation").Creation>);
|
|
31
38
|
|
|
32
39
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
@@ -6,7 +6,10 @@ import { useEffect, useRef, useCallback, useState } from "react";
|
|
|
6
6
|
|
|
7
7
|
declare const __DEV__: boolean;
|
|
8
8
|
import { pollQueueStatus } from "./videoQueuePoller";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_POLL_INTERVAL_MS,
|
|
11
|
+
DEFAULT_MAX_POLL_TIME_MS,
|
|
12
|
+
} from "../../../../../infrastructure/constants/polling.constants";
|
|
10
13
|
import type { GenerationUrls } from "./generation-result.utils";
|
|
11
14
|
import type {
|
|
12
15
|
UseVideoQueueGenerationProps,
|
|
@@ -23,6 +26,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
23
26
|
const isGeneratingRef = useRef(false);
|
|
24
27
|
const isPollingRef = useRef(false);
|
|
25
28
|
const consecutiveErrorsRef = useRef(0);
|
|
29
|
+
const pollStartTimeRef = useRef<number | null>(null);
|
|
26
30
|
const [isGenerating, setIsGenerating] = useState(false);
|
|
27
31
|
|
|
28
32
|
const clearPolling = useCallback(() => {
|
|
@@ -38,6 +42,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
38
42
|
isGeneratingRef.current = false;
|
|
39
43
|
isPollingRef.current = false;
|
|
40
44
|
consecutiveErrorsRef.current = 0;
|
|
45
|
+
pollStartTimeRef.current = null;
|
|
41
46
|
creationIdRef.current = null;
|
|
42
47
|
requestIdRef.current = null;
|
|
43
48
|
modelRef.current = null;
|
|
@@ -53,12 +58,12 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
53
58
|
isGeneratingRef.current = false;
|
|
54
59
|
isPollingRef.current = false;
|
|
55
60
|
consecutiveErrorsRef.current = 0;
|
|
61
|
+
pollStartTimeRef.current = null;
|
|
56
62
|
setIsGenerating(false);
|
|
57
63
|
}, [clearPolling]);
|
|
58
64
|
|
|
59
65
|
const handleComplete = useCallback(
|
|
60
66
|
async (urls: GenerationUrls) => {
|
|
61
|
-
// Stop polling immediately on completion
|
|
62
67
|
clearPolling();
|
|
63
68
|
|
|
64
69
|
const creationId = creationIdRef.current;
|
|
@@ -74,7 +79,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
74
79
|
});
|
|
75
80
|
}
|
|
76
81
|
|
|
77
|
-
// Validate non-empty URI
|
|
78
82
|
if (!creationId || !userId || !uri || uri.trim() === "") {
|
|
79
83
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
80
84
|
console.error("[VideoQueue] ❌ Invalid completion data:", { creationId, userId, uri });
|
|
@@ -92,6 +96,7 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
92
96
|
imageUrl: urls.imageUrl,
|
|
93
97
|
videoUrl: urls.videoUrl,
|
|
94
98
|
thumbnailUrl: urls.thumbnailUrl,
|
|
99
|
+
generationStartedAt: pollStartTimeRef.current ?? undefined,
|
|
95
100
|
});
|
|
96
101
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
97
102
|
console.log("[VideoQueue] ✅ Updated completion status in Firestore");
|
|
@@ -106,8 +111,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
106
111
|
|
|
107
112
|
resetRefs();
|
|
108
113
|
|
|
109
|
-
// Still call onSuccess even if persistence failed - the generation itself succeeded
|
|
110
|
-
// The video/image URL is valid, user should still see the result
|
|
111
114
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
112
115
|
console.log("[VideoQueue] 🎯 Calling onSuccess callback now...", { persistenceSucceeded });
|
|
113
116
|
}
|
|
@@ -139,11 +142,30 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
139
142
|
[userId, persistence, onError, resetRefs, clearPolling],
|
|
140
143
|
);
|
|
141
144
|
|
|
145
|
+
// Use a ref to hold the latest handleComplete/handleError to avoid stale closures
|
|
146
|
+
// in the setInterval callback
|
|
147
|
+
const handleCompleteRef = useRef(handleComplete);
|
|
148
|
+
const handleErrorRef = useRef(handleError);
|
|
149
|
+
useEffect(() => { handleCompleteRef.current = handleComplete; }, [handleComplete]);
|
|
150
|
+
useEffect(() => { handleErrorRef.current = handleError; }, [handleError]);
|
|
151
|
+
|
|
142
152
|
const pollStatus = useCallback(async () => {
|
|
143
153
|
const requestId = requestIdRef.current;
|
|
144
154
|
const model = modelRef.current;
|
|
145
155
|
if (!requestId || !model) return;
|
|
146
156
|
|
|
157
|
+
// Check max poll time
|
|
158
|
+
if (pollStartTimeRef.current !== null) {
|
|
159
|
+
const elapsed = Date.now() - pollStartTimeRef.current;
|
|
160
|
+
if (elapsed >= DEFAULT_MAX_POLL_TIME_MS) {
|
|
161
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
162
|
+
console.warn("[VideoQueue] ⏰ Max poll time exceeded, aborting");
|
|
163
|
+
}
|
|
164
|
+
await handleErrorRef.current("Generation timed out. Please try again.");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
147
169
|
try {
|
|
148
170
|
await pollQueueStatus({
|
|
149
171
|
requestId,
|
|
@@ -151,15 +173,19 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
151
173
|
isPollingRef,
|
|
152
174
|
pollingRef,
|
|
153
175
|
consecutiveErrorsRef,
|
|
154
|
-
onComplete:
|
|
155
|
-
onError:
|
|
176
|
+
onComplete: handleCompleteRef.current,
|
|
177
|
+
onError: handleErrorRef.current,
|
|
156
178
|
});
|
|
157
179
|
} catch (error) {
|
|
158
180
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
159
181
|
console.error("[VideoQueue] Unexpected poll error:", error);
|
|
160
182
|
}
|
|
161
183
|
}
|
|
162
|
-
}, [
|
|
184
|
+
}, []);
|
|
185
|
+
|
|
186
|
+
// Keep a stable ref to pollStatus for the setInterval closure
|
|
187
|
+
const pollStatusRef = useRef(pollStatus);
|
|
188
|
+
useEffect(() => { pollStatusRef.current = pollStatus; }, [pollStatus]);
|
|
163
189
|
|
|
164
190
|
const startGeneration = useCallback(
|
|
165
191
|
async (input: unknown, prompt: string) => {
|
|
@@ -175,13 +201,12 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
175
201
|
let creationId: string | null = null;
|
|
176
202
|
if (userId && prompt) {
|
|
177
203
|
try {
|
|
178
|
-
// Extract generation parameters from input
|
|
179
204
|
const inputData = input as Record<string, unknown>;
|
|
180
205
|
const duration = typeof inputData?.duration === "number" ? inputData.duration : undefined;
|
|
181
206
|
const resolution = typeof inputData?.resolution === "string" ? inputData.resolution : undefined;
|
|
182
207
|
const aspectRatio = typeof inputData?.aspectRatio === "string" ? inputData.aspectRatio : undefined;
|
|
183
208
|
|
|
184
|
-
|
|
209
|
+
const result = await persistence.saveAsProcessing(userId, {
|
|
185
210
|
scenarioId: scenario.id,
|
|
186
211
|
scenarioTitle: scenario.title || scenario.id,
|
|
187
212
|
prompt,
|
|
@@ -192,13 +217,14 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
192
217
|
provider: "fal",
|
|
193
218
|
outputType: scenario.outputType,
|
|
194
219
|
});
|
|
220
|
+
creationId = result.creationId;
|
|
195
221
|
creationIdRef.current = creationId;
|
|
222
|
+
// Record the actual DB-level start time for accurate durationMs
|
|
223
|
+
pollStartTimeRef.current = result.startedAt.getTime();
|
|
196
224
|
} catch (error) {
|
|
197
225
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
198
226
|
console.error("[VideoQueue] Failed to save processing creation:", error);
|
|
199
227
|
}
|
|
200
|
-
// Continue without creation tracking - generation can still proceed
|
|
201
|
-
// The video will be generated but won't appear in gallery history
|
|
202
228
|
}
|
|
203
229
|
}
|
|
204
230
|
|
|
@@ -206,7 +232,6 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
206
232
|
try {
|
|
207
233
|
queueResult = await strategy.submitToQueue(input);
|
|
208
234
|
} catch (error) {
|
|
209
|
-
// Queue submission threw - reset state and report error
|
|
210
235
|
if (creationId && userId) {
|
|
211
236
|
try {
|
|
212
237
|
await persistence.updateToFailed(userId, creationId, error instanceof Error ? error.message : "Queue submission failed");
|
|
@@ -243,10 +268,14 @@ export function useVideoQueueGeneration(props: UseVideoQueueGenerationProps): Us
|
|
|
243
268
|
}
|
|
244
269
|
}
|
|
245
270
|
|
|
246
|
-
|
|
247
|
-
|
|
271
|
+
// Start polling: use DB-level startedAt if available, otherwise fallback to now
|
|
272
|
+
if (pollStartTimeRef.current === null) {
|
|
273
|
+
pollStartTimeRef.current = Date.now();
|
|
274
|
+
}
|
|
275
|
+
pollingRef.current = setInterval(() => void pollStatusRef.current(), DEFAULT_POLL_INTERVAL_MS);
|
|
276
|
+
void pollStatusRef.current();
|
|
248
277
|
},
|
|
249
|
-
[userId, scenario, persistence, strategy, creditCost,
|
|
278
|
+
[userId, scenario, persistence, strategy, creditCost, onError],
|
|
250
279
|
);
|
|
251
280
|
|
|
252
281
|
return { isGenerating, startGeneration };
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { providerRegistry } from "../../../../../infrastructure/services/provider-registry.service";
|
|
2
2
|
import { extractResultUrl, type GenerationUrls, type GenerationResult } from "./generation-result.utils";
|
|
3
3
|
import { QUEUE_STATUS } from "../../../../../domain/constants/queue-status.constants";
|
|
4
|
+
import { DEFAULT_MAX_CONSECUTIVE_ERRORS } from "../../../../../infrastructure/constants/polling.constants";
|
|
4
5
|
|
|
5
6
|
declare const __DEV__: boolean;
|
|
6
7
|
|
|
7
|
-
/** Max consecutive transient errors before aborting */
|
|
8
|
-
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
9
|
* Extract meaningful error message from various error formats.
|
|
12
10
|
* Fal AI client throws ValidationError with empty .message but details in .body/.detail
|
|
@@ -14,12 +12,8 @@ const MAX_CONSECUTIVE_ERRORS = 5;
|
|
|
14
12
|
function extractErrorMessage(err: unknown): string {
|
|
15
13
|
if (!err) return "Generation failed";
|
|
16
14
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
return err.message;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Fal AI ValidationError - has .body.detail array
|
|
15
|
+
// Fal AI ValidationError - has .body.detail array (check before instanceof Error
|
|
16
|
+
// because ValidationError may extend Error with empty .message)
|
|
23
17
|
const errObj = err as Record<string, unknown>;
|
|
24
18
|
if (errObj.body && typeof errObj.body === "object") {
|
|
25
19
|
const body = errObj.body as Record<string, unknown>;
|
|
@@ -35,6 +29,11 @@ function extractErrorMessage(err: unknown): string {
|
|
|
35
29
|
if (first?.msg) return first.msg;
|
|
36
30
|
}
|
|
37
31
|
|
|
32
|
+
// Standard Error with message
|
|
33
|
+
if (err instanceof Error && err.message && err.message.length > 0) {
|
|
34
|
+
return err.message;
|
|
35
|
+
}
|
|
36
|
+
|
|
38
37
|
// Fallback to string conversion
|
|
39
38
|
const str = String(err);
|
|
40
39
|
return str.length > 0 && str !== "[object Object]" ? str : "Generation failed";
|
|
@@ -96,15 +95,21 @@ export const pollQueueStatus = async (params: PollParams): Promise<void> => {
|
|
|
96
95
|
await onError(errorMessage);
|
|
97
96
|
}
|
|
98
97
|
} else {
|
|
99
|
-
|
|
98
|
+
// Try to extract error from FAL job logs (error-level log takes priority)
|
|
99
|
+
const logs = status.logs ?? [];
|
|
100
|
+
const errorLog = logs.findLast?.((l) => l.level === "error") ?? logs[logs.length - 1];
|
|
101
|
+
const failMessage =
|
|
102
|
+
errorLog?.message && errorLog.message !== "[object Object]"
|
|
103
|
+
? errorLog.message
|
|
104
|
+
: "Generation failed";
|
|
105
|
+
await onError(failMessage);
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
108
|
} catch (err) {
|
|
103
109
|
consecutiveErrorsRef.current += 1;
|
|
104
|
-
const errorMessage = err
|
|
110
|
+
const errorMessage = extractErrorMessage(err);
|
|
105
111
|
|
|
106
|
-
if (consecutiveErrorsRef.current >=
|
|
107
|
-
// Too many consecutive errors - abort
|
|
112
|
+
if (consecutiveErrorsRef.current >= DEFAULT_MAX_CONSECUTIVE_ERRORS) {
|
|
108
113
|
if (pollingRef.current) {
|
|
109
114
|
clearInterval(pollingRef.current);
|
|
110
115
|
pollingRef.current = null;
|
|
@@ -112,10 +117,9 @@ export const pollQueueStatus = async (params: PollParams): Promise<void> => {
|
|
|
112
117
|
if (__DEV__) console.error("[VideoQueueGeneration] Max consecutive errors reached, aborting:", errorMessage);
|
|
113
118
|
await onError(errorMessage);
|
|
114
119
|
} else {
|
|
115
|
-
// Transient error - continue polling
|
|
116
120
|
if (__DEV__) {
|
|
117
121
|
console.warn(
|
|
118
|
-
`[VideoQueueGeneration] Transient poll error (${consecutiveErrorsRef.current}/${
|
|
122
|
+
`[VideoQueueGeneration] Transient poll error (${consecutiveErrorsRef.current}/${DEFAULT_MAX_CONSECUTIVE_ERRORS}):`,
|
|
119
123
|
errorMessage,
|
|
120
124
|
);
|
|
121
125
|
}
|