Skip to content

Commit 70dcbc4

Browse files
committed
fix: handle generation progress
1 parent 3cca99b commit 70dcbc4

File tree

4 files changed

+188
-123
lines changed

4 files changed

+188
-123
lines changed

web/bun.lockb

744 Bytes
Binary file not shown.

web/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"class-variance-authority": "^0.7.0",
3939
"clsx": "^2.1.1",
4040
"drizzle-orm": "^0.36.0",
41+
"eventsource": "^2.0.2",
4142
"framer-motion": "^11.11.7",
4243
"lucide-react": "^0.451.0",
4344
"next": "15.0.0-rc.0",
@@ -52,6 +53,7 @@
5253
"zod": "^3.23.8"
5354
},
5455
"devDependencies": {
56+
"@types/eventsource": "^1.1.15",
5557
"@types/node": "^20",
5658
"@types/react": "^18",
5759
"@types/react-dom": "^18",

web/src/app/(history)/history/(item)/[id]/action.ts

+69-75
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ import { uploadVideoToR2 } from "@/lib/r2";
55
import { GeneratedAssetType } from "@/types";
66
import { revalidatePath } from "next/cache";
77

8-
const TIMEOUT_MS = 5 * 60 * 1000;
8+
const TIMEOUT_MS = 10 * 60 * 1000;
99
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+
}
1119

1220
async function fetchWithRetry(
1321
url: string,
@@ -17,19 +25,37 @@ async function fetchWithRetry(
1725
console.log(
1826
`[fetchWithRetry] Attempt ${RETRY_ATTEMPTS - attempts + 1} for URL: ${url}`
1927
);
28+
2029
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+
2243
if (!response.ok) {
23-
console.log(
24-
`[fetchWithRetry] Failed attempt with status: ${response.status}`
25-
);
2644
throw new Error(`HTTP error! status: ${response.status}`);
2745
}
28-
console.log(`[fetchWithRetry] Successful response received`);
46+
2947
return response;
3048
} catch (error) {
3149
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+
3359
console.log(`[fetchWithRetry] Retrying in ${RETRY_DELAY_MS}ms`);
3460
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
3561
return fetchWithRetry(url, options, attempts - 1);
@@ -43,11 +69,7 @@ export async function startGeneration({
4369
}) {
4470
console.log(`[startGeneration] Starting generation for asset:`, asset);
4571
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");
5173

5274
const encoder = new TextEncoder();
5375
let abortController: AbortController | null = null;
@@ -63,6 +85,7 @@ export async function startGeneration({
6385
JSON.stringify({
6486
error: "Generation failed",
6587
details: errorMessage,
88+
recoverable: error instanceof GenerationError,
6689
}) + "\n"
6790
)
6891
);
@@ -87,9 +110,9 @@ export async function startGeneration({
87110
abortController?.abort();
88111
}, TIMEOUT_MS);
89112

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+
93116
const response = await fetchWithRetry(
94117
`${process.env.RENDERER_URL}/render/${asset.configId}/`,
95118
{
@@ -106,11 +129,9 @@ export async function startGeneration({
106129
);
107130

108131
clearTimeout(timeoutId);
109-
console.log(`[stream] Renderer response received`);
110132

111133
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");
114135
}
115136

116137
const reader = response.body.getReader();
@@ -119,18 +140,10 @@ export async function startGeneration({
119140

120141
const heartbeatInterval = setInterval(() => {
121142
const now = Date.now();
122-
console.log(
123-
`[stream] Heartbeat check - Time since last event: ${
124-
now - lastEventTime
125-
}ms`
126-
);
127143
if (now - lastEventTime > 30000) {
128-
console.log(
129-
`[stream] Connection stalled - no data received for 30s`
130-
);
131144
clearInterval(heartbeatInterval);
132145
controller.error(
133-
new Error("Connection stalled - no data received")
146+
new GenerationError("Connection stalled - no data received")
134147
);
135148
}
136149
}, 5000);
@@ -139,12 +152,9 @@ export async function startGeneration({
139152
while (true) {
140153
const { value, done } = await reader.read();
141154

142-
if (done) {
143-
console.log(`[stream] Reader completed`);
144-
break;
145-
}
146-
lastEventTime = Date.now();
155+
if (done) break;
147156

157+
lastEventTime = Date.now();
148158
buffer += new TextDecoder().decode(value, { stream: true });
149159
const messages = buffer.split("\n\n");
150160
buffer = messages.pop() || "";
@@ -156,57 +166,38 @@ export async function startGeneration({
156166
const data = JSON.parse(message.slice(6));
157167
console.log(`[stream] Received message:`, data);
158168

169+
// Update last known progress
170+
if (data.progress) lastProgress = data.progress;
171+
if (data.stage) lastStage = data.stage;
172+
159173
if (data.error) {
160-
console.log(`[stream] Renderer error:`, data.error);
161-
sendError(data.error);
162-
return;
174+
throw new GenerationError(data.error);
163175
}
164176

165177
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-
172178
sendStatus("Uploading to R2...", {
173179
progress: Math.min(lastProgress + 5, 99),
174180
stage: lastStage,
175181
status: "Uploading to storage...",
176182
});
177183

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}`);
210201
} else {
211202
controller.enqueue(
212203
encoder.encode(JSON.stringify(data) + "\n")
@@ -218,13 +209,16 @@ export async function startGeneration({
218209
error,
219210
message
220211
);
212+
if (error instanceof GenerationError) {
213+
sendError(error);
214+
return;
215+
}
221216
continue;
222217
}
223218
}
224219
}
225220
} finally {
226221
clearInterval(heartbeatInterval);
227-
console.log(`[stream] Stream processing completed`);
228222
}
229223
} catch (error) {
230224
console.log(`[stream] Fatal stream error:`, error);

0 commit comments

Comments
 (0)