Skip to content

Commit 278acb4

Browse files
committed
fix: cf worker waitUntil timeout
1 parent e5163bf commit 278acb4

File tree

5 files changed

+213
-65
lines changed

5 files changed

+213
-65
lines changed

src/app/routes/chat/-components/chat/ChatMessageItem.tsx

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function ChatMessageItem({
4040
const chatService = useChatService();
4141
const intervalRef = useRef<NodeJS.Timeout | null>(null);
4242
const skipPoll = useRef<boolean>(false);
43+
const pollingGenerationIdRef = useRef<string | null>(null); // Track which generation is being polled
4344
const [isLightboxOpen, setIsLightboxOpen] = useState(false);
4445
const [currentImageIndex, setCurrentImageIndex] = useState(0);
4546
const [isHovered, setIsHovered] = useState(false);
@@ -122,6 +123,20 @@ export function ChatMessageItem({
122123
const generationId = message.generationId || message.generation?.id;
123124

124125
if (isMessageGenerating && generationId && onMessageUpdate) {
126+
// If we're already polling this exact generation, don't restart
127+
if (intervalRef.current && pollingGenerationIdRef.current === generationId) {
128+
return;
129+
}
130+
131+
// Clear any existing interval for a different generation
132+
if (intervalRef.current) {
133+
clearInterval(intervalRef.current);
134+
intervalRef.current = null;
135+
}
136+
137+
// Mark this generation as being polled
138+
pollingGenerationIdRef.current = generationId;
139+
125140
const pollStatus = async () => {
126141
try {
127142
if (skipPoll.current) {
@@ -143,6 +158,7 @@ export function ChatMessageItem({
143158
if (intervalRef.current) {
144159
clearInterval(intervalRef.current);
145160
intervalRef.current = null;
161+
pollingGenerationIdRef.current = null;
146162
}
147163
}
148164
}
@@ -151,21 +167,34 @@ export function ChatMessageItem({
151167
}
152168
};
153169

154-
// Start polling every 3 seconds
155-
intervalRef.current = setInterval(pollStatus, 3000);
156-
157-
// Also poll immediately (unless skipFirstPoll is set)
158-
pollStatus();
170+
// Start polling every 3 seconds after initial 3 second delay
171+
const timeoutId = setTimeout(() => {
172+
if (isMessageGenerating && pollingGenerationIdRef.current === generationId) {
173+
pollStatus();
174+
// Start interval polling after first poll completes
175+
intervalRef.current = setInterval(pollStatus, 3000);
176+
}
177+
}, 3000);
159178

160179
// Cleanup on unmount or when generation is no longer pending
161180
return () => {
181+
clearTimeout(timeoutId);
162182
if (intervalRef.current) {
163183
clearInterval(intervalRef.current);
164184
intervalRef.current = null;
185+
pollingGenerationIdRef.current = null;
165186
}
166187
};
167188
}
168-
}, [isMessageGenerating, message.generationId, message.generation?.id, message.id, onMessageUpdate, chatService]);
189+
190+
// If message is no longer generating, clear the interval
191+
if (!isMessageGenerating && intervalRef.current) {
192+
clearInterval(intervalRef.current);
193+
intervalRef.current = null;
194+
pollingGenerationIdRef.current = null;
195+
}
196+
}, [isMessageGenerating, message.generationId, message.id, onMessageUpdate, chatService]);
197+
// Note: Removed message.generation?.id from dependencies to avoid unnecessary re-runs
169198

170199
// Cleanup interval on unmount
171200
useEffect(() => {

src/app/routes/chat/-hooks/useChat.ts

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,20 @@ export const useChat = (initialChatId?: string, selectedProvider?: string, selec
309309
setCurrentChatId(result.id);
310310
setIsChatIdValidated(true);
311311

312+
// Trigger image generation if messages were created
313+
if (result.messages) {
314+
const assistantMessage = result.messages.find((msg: any) => msg.role === "assistant");
315+
if (assistantMessage?.generation?.id) {
316+
// Mark as generating before triggering
317+
assistantMessage.generation.status = "generating";
318+
319+
// Fire and forget - don't await, let it run in background
320+
chatService.createMessageGenerate({ generationId: assistantMessage.generation.id }).catch((error) => {
321+
console.error("Error triggering image generation:", error);
322+
});
323+
}
324+
}
325+
312326
// Return the chat ID - no need to send another message since createChat already handled it
313327
return result.id;
314328
} catch (error) {
@@ -418,6 +432,11 @@ export const useChat = (initialChatId?: string, selectedProvider?: string, selec
418432

419433
// Use returned messages to update the chat data instead of revalidating
420434
if (result?.messages) {
435+
// Trigger image generation in browser first (not blocked by CF Worker timeout)
436+
const assistantMessage = result.messages.find((msg) => msg.role === "assistant");
437+
const shouldTriggerGeneration = assistantMessage?.generation?.id;
438+
439+
// Update UI - mark generation as "generating" to avoid immediate polling
421440
currentChatMutate((currentData) => {
422441
if (!currentData) return currentData;
423442

@@ -426,13 +445,35 @@ export const useChat = (initialChatId?: string, selectedProvider?: string, selec
426445
const existingMessages =
427446
currentData.messages?.filter((msg: any) => msg.id !== optimisticUserMessage.id) || [];
428447

429-
// Append new messages from server
448+
// Append new messages from server, but mark as "generating" if we're about to trigger
449+
const newMessages = shouldTriggerGeneration
450+
? result.messages.map((msg: any) => {
451+
if (msg.role === "assistant" && msg.generation) {
452+
return {
453+
...msg,
454+
generation: {
455+
...msg.generation,
456+
status: "generating" as const,
457+
},
458+
};
459+
}
460+
return msg;
461+
})
462+
: result.messages;
463+
430464
return {
431465
...currentData,
432-
messages: [...existingMessages, ...result.messages],
466+
messages: [...existingMessages, ...newMessages],
433467
updatedAt: new Date().toISOString(),
434468
};
435469
}, false);
470+
471+
if (shouldTriggerGeneration) {
472+
// Fire and forget - don't await, let it run in background
473+
chatService.createMessageGenerate({ generationId: assistantMessage!.generation!.id }).catch((error) => {
474+
console.error("Error triggering image generation:", error);
475+
});
476+
}
436477
} else {
437478
// Fallback: revalidate if no messages returned
438479
await currentChatMutate();
@@ -546,7 +587,7 @@ export const useChat = (initialChatId?: string, selectedProvider?: string, selec
546587
...msg,
547588
generation: {
548589
...msg.generation,
549-
status: "pending" as const,
590+
status: "generating" as const,
550591
fileIds: null,
551592
errorReason: null,
552593
},
@@ -558,7 +599,15 @@ export const useChat = (initialChatId?: string, selectedProvider?: string, selec
558599
}, false);
559600

560601
// Call the API
561-
await chatService.regenerateMessage({ messageId });
602+
const result = await chatService.regenerateMessage({ messageId });
603+
604+
// Trigger image generation in browser (not blocked by CF Worker timeout)
605+
if (result?.generationId) {
606+
// Fire and forget - don't await, let it run in background
607+
chatService.createMessageGenerate({ generationId: result.generationId }).catch((error) => {
608+
console.error("Error triggering image generation:", error);
609+
});
610+
}
562611

563612
// The polling mechanism in ChatMessageItem will handle updating the UI
564613
} catch (error) {

src/server/api/routes/chat.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
CreateChatSchema,
3+
CreateMessageGenerateSchema,
34
CreateMessageSchema,
45
DeleteMessageSchema,
56
GetChatByIdSchema,
@@ -75,6 +76,17 @@ const app = new Hono<Env>()
7576

7677
return c.json(ok(await chatService.getGenerationStatus(req, { userId: user.id })));
7778
})
79+
.post("/createMessageGenerate", zValidator("json", CreateMessageGenerateSchema), async (c) => {
80+
const user = c.var.user!;
81+
const req = c.req.valid("json");
82+
83+
try {
84+
const result = await chatService.createMessageGenerate(req, { userId: user.id, executionCtx: c.executionCtx });
85+
return c.json(ok(result));
86+
} catch (err: any) {
87+
return c.json(error(err.code || "error", err.message || "Failed to generate image"));
88+
}
89+
})
7890
.post("/regenerateMessage", zValidator("json", RegenerateMessageSchema), async (c) => {
7991
const user = c.var.user!;
8092
const req = c.req.valid("json");

0 commit comments

Comments
 (0)