@@ -5,9 +5,17 @@ import { uploadVideoToR2 } from "@/lib/r2";
5
5
import { GeneratedAssetType } from "@/types" ;
6
6
import { revalidatePath } from "next/cache" ;
7
7
8
- const TIMEOUT_MS = 5 * 60 * 1000 ;
8
+ const TIMEOUT_MS = 10 * 60 * 1000 ;
9
9
const RETRY_ATTEMPTS = 3 ;
10
- const RETRY_DELAY_MS = 1000 ;
10
+ const RETRY_DELAY_MS = 2000 ;
11
+ const CONNECTION_TIMEOUT_MS = 30000 ;
12
+
13
+ class GenerationError extends Error {
14
+ constructor ( message : string , public readonly details ?: any ) {
15
+ super ( message ) ;
16
+ this . name = "GenerationError" ;
17
+ }
18
+ }
11
19
12
20
async function fetchWithRetry (
13
21
url : string ,
@@ -17,19 +25,37 @@ async function fetchWithRetry(
17
25
console . log (
18
26
`[fetchWithRetry] Attempt ${ RETRY_ATTEMPTS - attempts + 1 } for URL: ${ url } `
19
27
) ;
28
+
20
29
try {
21
- const response = await fetch ( url , options ) ;
30
+ const controller = new AbortController ( ) ;
31
+ const timeoutId = setTimeout (
32
+ ( ) => controller . abort ( ) ,
33
+ CONNECTION_TIMEOUT_MS
34
+ ) ;
35
+
36
+ const response = await fetch ( url , {
37
+ ...options ,
38
+ signal : controller . signal ,
39
+ } ) ;
40
+
41
+ clearTimeout ( timeoutId ) ;
42
+
22
43
if ( ! response . ok ) {
23
- console . log (
24
- `[fetchWithRetry] Failed attempt with status: ${ response . status } `
25
- ) ;
26
44
throw new Error ( `HTTP error! status: ${ response . status } ` ) ;
27
45
}
28
- console . log ( `[fetchWithRetry] Successful response received` ) ;
46
+
29
47
return response ;
30
48
} catch ( error ) {
31
49
console . log ( `[fetchWithRetry] Error during fetch:` , error ) ;
32
- if ( attempts <= 1 ) throw error ;
50
+
51
+ if ( error instanceof Error && error . name === "AbortError" ) {
52
+ throw new GenerationError ( "Connection timeout exceeded" ) ;
53
+ }
54
+
55
+ if ( attempts <= 1 ) {
56
+ throw new GenerationError ( "Failed to connect to renderer service" , error ) ;
57
+ }
58
+
33
59
console . log ( `[fetchWithRetry] Retrying in ${ RETRY_DELAY_MS } ms` ) ;
34
60
await new Promise ( ( resolve ) => setTimeout ( resolve , RETRY_DELAY_MS ) ) ;
35
61
return fetchWithRetry ( url , options , attempts - 1 ) ;
@@ -43,11 +69,7 @@ export async function startGeneration({
43
69
} ) {
44
70
console . log ( `[startGeneration] Starting generation for asset:` , asset ) ;
45
71
const { user } = await validateRequest ( ) ;
46
- if ( ! user ) {
47
- console . log ( `[startGeneration] Authorization failed - no user found` ) ;
48
- throw new Error ( "Unauthorized" ) ;
49
- }
50
- console . log ( `[startGeneration] User authorized:` , user . googleId ) ;
72
+ if ( ! user ) throw new Error ( "Unauthorized" ) ;
51
73
52
74
const encoder = new TextEncoder ( ) ;
53
75
let abortController : AbortController | null = null ;
@@ -63,6 +85,7 @@ export async function startGeneration({
63
85
JSON . stringify ( {
64
86
error : "Generation failed" ,
65
87
details : errorMessage ,
88
+ recoverable : error instanceof GenerationError ,
66
89
} ) + "\n"
67
90
)
68
91
) ;
@@ -87,9 +110,9 @@ export async function startGeneration({
87
110
abortController ?. abort ( ) ;
88
111
} , TIMEOUT_MS ) ;
89
112
90
- console . log (
91
- `[stream] Initiating renderer request to ${ process . env . RENDERER_URL } /render/ ${ asset . configId } /`
92
- ) ;
113
+ let lastProgress = 0 ;
114
+ let lastStage = "STARTING" ;
115
+
93
116
const response = await fetchWithRetry (
94
117
`${ process . env . RENDERER_URL } /render/${ asset . configId } /` ,
95
118
{
@@ -106,11 +129,9 @@ export async function startGeneration({
106
129
) ;
107
130
108
131
clearTimeout ( timeoutId ) ;
109
- console . log ( `[stream] Renderer response received` ) ;
110
132
111
133
if ( ! response . body ) {
112
- console . log ( `[stream] No response body received from renderer` ) ;
113
- throw new Error ( "No response body received from renderer" ) ;
134
+ throw new GenerationError ( "No response body received from renderer" ) ;
114
135
}
115
136
116
137
const reader = response . body . getReader ( ) ;
@@ -119,18 +140,10 @@ export async function startGeneration({
119
140
120
141
const heartbeatInterval = setInterval ( ( ) => {
121
142
const now = Date . now ( ) ;
122
- console . log (
123
- `[stream] Heartbeat check - Time since last event: ${
124
- now - lastEventTime
125
- } ms`
126
- ) ;
127
143
if ( now - lastEventTime > 30000 ) {
128
- console . log (
129
- `[stream] Connection stalled - no data received for 30s`
130
- ) ;
131
144
clearInterval ( heartbeatInterval ) ;
132
145
controller . error (
133
- new Error ( "Connection stalled - no data received" )
146
+ new GenerationError ( "Connection stalled - no data received" )
134
147
) ;
135
148
}
136
149
} , 5000 ) ;
@@ -139,12 +152,9 @@ export async function startGeneration({
139
152
while ( true ) {
140
153
const { value, done } = await reader . read ( ) ;
141
154
142
- if ( done ) {
143
- console . log ( `[stream] Reader completed` ) ;
144
- break ;
145
- }
146
- lastEventTime = Date . now ( ) ;
155
+ if ( done ) break ;
147
156
157
+ lastEventTime = Date . now ( ) ;
148
158
buffer += new TextDecoder ( ) . decode ( value , { stream : true } ) ;
149
159
const messages = buffer . split ( "\n\n" ) ;
150
160
buffer = messages . pop ( ) || "" ;
@@ -156,57 +166,38 @@ export async function startGeneration({
156
166
const data = JSON . parse ( message . slice ( 6 ) ) ;
157
167
console . log ( `[stream] Received message:` , data ) ;
158
168
169
+ // Update last known progress
170
+ if ( data . progress ) lastProgress = data . progress ;
171
+ if ( data . stage ) lastStage = data . stage ;
172
+
159
173
if ( data . error ) {
160
- console . log ( `[stream] Renderer error:` , data . error ) ;
161
- sendError ( data . error ) ;
162
- return ;
174
+ throw new GenerationError ( data . error ) ;
163
175
}
164
176
165
177
if ( data . path ) {
166
- const lastProgress = data . progress || 0 ;
167
- const lastStage = data . stage || "ENCODING" ;
168
- console . log (
169
- `[stream] Upload stage - Progress: ${ lastProgress } , Stage: ${ lastStage } `
170
- ) ;
171
-
172
178
sendStatus ( "Uploading to R2..." , {
173
179
progress : Math . min ( lastProgress + 5 , 99 ) ,
174
180
stage : lastStage ,
175
181
status : "Uploading to storage..." ,
176
182
} ) ;
177
183
178
- try {
179
- console . log (
180
- `[stream] Starting R2 upload for configId:` ,
181
- asset . configId
182
- ) ;
183
- const { url, signedUrl } = await uploadVideoToR2 (
184
- `${ process . env . RENDERER_URL } /assets/${ asset . configId } ` ,
185
- asset . configId !
186
- ) ;
187
- console . log ( `[stream] R2 upload complete - URL:` , url ) ;
188
-
189
- console . log ( `[stream] Storing video in database` ) ;
190
- await storeGeneratedVideo ( {
191
- r2Url : url ,
192
- configId : asset . configId ! ,
193
- userGoogleId : user . googleId ,
194
- } ) ;
195
-
196
- sendStatus ( "complete" , {
197
- signedUrl,
198
- progress : 100 ,
199
- stage : "COMPLETE" ,
200
- } ) ;
201
- revalidatePath ( `/history/${ asset . configId } ` ) ;
202
- console . log (
203
- `[stream] Generation complete for configId:` ,
204
- asset . configId
205
- ) ;
206
- } catch ( error ) {
207
- console . log ( `[stream] R2 upload error:` , error ) ;
208
- sendError ( error ) ;
209
- }
184
+ const { url, signedUrl } = await uploadVideoToR2 (
185
+ `${ process . env . RENDERER_URL } /assets/${ asset . configId } ` ,
186
+ asset . configId !
187
+ ) ;
188
+
189
+ await storeGeneratedVideo ( {
190
+ r2Url : url ,
191
+ configId : asset . configId ! ,
192
+ userGoogleId : user . googleId ,
193
+ } ) ;
194
+
195
+ sendStatus ( "complete" , {
196
+ signedUrl,
197
+ progress : 100 ,
198
+ stage : "COMPLETE" ,
199
+ } ) ;
200
+ revalidatePath ( `/history/${ asset . configId } ` ) ;
210
201
} else {
211
202
controller . enqueue (
212
203
encoder . encode ( JSON . stringify ( data ) + "\n" )
@@ -218,13 +209,16 @@ export async function startGeneration({
218
209
error ,
219
210
message
220
211
) ;
212
+ if ( error instanceof GenerationError ) {
213
+ sendError ( error ) ;
214
+ return ;
215
+ }
221
216
continue ;
222
217
}
223
218
}
224
219
}
225
220
} finally {
226
221
clearInterval ( heartbeatInterval ) ;
227
- console . log ( `[stream] Stream processing completed` ) ;
228
222
}
229
223
} catch ( error ) {
230
224
console . log ( `[stream] Fatal stream error:` , error ) ;
0 commit comments