Skip to content

Commit e8633a0

Browse files
committed
prevent preloads from firing twice when in React strictMode
1 parent 6ba8551 commit e8633a0

File tree

5 files changed

+61
-21
lines changed

5 files changed

+61
-21
lines changed

packages/trigger-sdk/src/v3/chat.ts

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
393393

394394
private sessions: Map<string, ChatSessionState> = new Map();
395395
private activeStreams: Map<string, AbortController> = new Map();
396+
private pendingPreloads: Map<string, Promise<void>> = new Map();
396397

397398
constructor(options: TriggerChatTransportOptions) {
398399
this.taskId = options.task;
@@ -800,26 +801,38 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
800801
// Don't preload if session already exists
801802
if (this.sessions.get(chatId)?.runId) return;
802803

803-
const mergedMetadata =
804-
this.defaultMetadata || options?.metadata
805-
? { ...(this.defaultMetadata ?? {}), ...(options?.metadata ?? {}) }
806-
: undefined;
804+
// Deduplicate concurrent preload calls (e.g. React strict mode double-firing effects)
805+
const pending = this.pendingPreloads.get(chatId);
806+
if (pending) return pending;
807+
808+
const doPreload = async () => {
809+
const mergedMetadata =
810+
this.defaultMetadata || options?.metadata
811+
? { ...(this.defaultMetadata ?? {}), ...(options?.metadata ?? {}) }
812+
: undefined;
813+
814+
const payload = {
815+
messages: [] as never[],
816+
chatId,
817+
trigger: "preload" as const,
818+
metadata: mergedMetadata,
819+
...(options?.idleTimeoutInSeconds !== undefined
820+
? { idleTimeoutInSeconds: options.idleTimeoutInSeconds }
821+
: {}),
822+
};
807823

808-
const payload = {
809-
messages: [] as never[],
810-
chatId,
811-
trigger: "preload" as const,
812-
metadata: mergedMetadata,
813-
...(options?.idleTimeoutInSeconds !== undefined
814-
? { idleTimeoutInSeconds: options.idleTimeoutInSeconds }
815-
: {}),
816-
};
824+
const { runId, publicAccessToken } = await this.triggerNewRun(chatId, payload, "preload");
817825

818-
const { runId, publicAccessToken } = await this.triggerNewRun(chatId, payload, "preload");
826+
const newSession: ChatSessionState = { runId, publicAccessToken };
827+
this.sessions.set(chatId, newSession);
828+
this.notifySessionChange(chatId, newSession);
829+
};
819830

820-
const newSession: ChatSessionState = { runId, publicAccessToken };
821-
this.sessions.set(chatId, newSession);
822-
this.notifySessionChange(chatId, newSession);
831+
const promise = doPreload().finally(() => {
832+
this.pendingPreloads.delete(chatId);
833+
});
834+
this.pendingPreloads.set(chatId, promise);
835+
return promise;
823836
}
824837

825838
private async resolveAccessToken(params: ResolveChatAccessTokenParams): Promise<string> {

references/ai-chat/src/app/actions.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,11 @@ export async function deleteChat(chatId: string) {
9898
await prisma.chatSession.delete({ where: { id: chatId } }).catch(() => { });
9999
}
100100

101+
export async function deleteAllChats() {
102+
await prisma.chatSession.deleteMany();
103+
await prisma.chat.deleteMany();
104+
}
105+
101106
export async function updateChatTitle(chatId: string, title: string) {
102107
await prisma.chat.update({ where: { id: chatId }, data: { title } }).catch(() => { });
103108
}

references/ai-chat/src/components/chat-sidebar-wrapper.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ChatSidebar } from "@/components/chat-sidebar";
55
import { useChatSettings } from "@/components/chat-settings-context";
66
import { useState, useCallback, useEffect } from "react";
77
import { generateId } from "ai";
8-
import { getChatList, deleteChat as deleteChatAction } from "@/app/actions";
8+
import { getChatList, deleteChat as deleteChatAction, deleteAllChats } from "@/app/actions";
99

1010
type ChatMeta = {
1111
id: string;
@@ -68,13 +68,21 @@ export function ChatSidebarWrapper({
6868
}
6969
}
7070

71+
async function handleWipeAll() {
72+
if (!confirm("Delete ALL chats? This cannot be undone.")) return;
73+
await deleteAllChats();
74+
setChatList([]);
75+
router.push("/chats");
76+
}
77+
7178
return (
7279
<ChatSidebar
7380
chats={chatList}
7481
activeChatId={activeChatId}
7582
onSelectChat={handleSelectChat}
7683
onNewChat={handleNewChat}
7784
onDeleteChat={handleDeleteChat}
85+
onWipeAll={handleWipeAll}
7886
preloadEnabled={preloadEnabled}
7987
onPreloadChange={setPreloadEnabled}
8088
idleTimeoutInSeconds={idleTimeoutInSeconds}

references/ai-chat/src/components/chat-sidebar.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type ChatSidebarProps = {
2424
onSelectChat: (id: string) => void;
2525
onNewChat: () => void;
2626
onDeleteChat: (id: string) => void;
27+
onWipeAll: () => void;
2728
preloadEnabled: boolean;
2829
onPreloadChange: (enabled: boolean) => void;
2930
idleTimeoutInSeconds: number;
@@ -38,6 +39,7 @@ export function ChatSidebar({
3839
onSelectChat,
3940
onNewChat,
4041
onDeleteChat,
42+
onWipeAll,
4143
preloadEnabled,
4244
onPreloadChange,
4345
idleTimeoutInSeconds,
@@ -124,6 +126,13 @@ export function ChatSidebar({
124126
<option value="ai-chat-session">ai-chat-session (session)</option>
125127
</select>
126128
</div>
129+
<button
130+
type="button"
131+
onClick={onWipeAll}
132+
className="w-full rounded border border-red-300 px-2 py-1 text-xs text-red-600 hover:bg-red-50"
133+
>
134+
Wipe all chats
135+
</button>
127136
</div>
128137
</div>
129138
);

references/ai-chat/src/components/chat-view.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
deleteSessionAction,
1313
renewRunAccessTokenForChat,
1414
} from "@/app/actions";
15-
import { useCallback, useEffect } from "react";
15+
import { useCallback, useEffect, useState } from "react";
1616
import { useRouter } from "next/navigation";
1717

1818
type SessionInfo = {
@@ -39,13 +39,18 @@ export function ChatView({
3939
const router = useRouter();
4040
const { taskMode, preloadEnabled, idleTimeoutInSeconds } = useChatSettings();
4141

42+
const [currentSession, setCurrentSession] = useState<SessionInfo | null>(initialSession);
43+
4244
const sessions: Record<string, SessionInfo> = {};
4345
if (initialSession) {
4446
sessions[chatId] = initialSession;
4547
}
4648

4749
const handleSessionChange = useCallback((_id: string, session: SessionInfo | null) => {
48-
if (!session) {
50+
if (session) {
51+
setCurrentSession(session);
52+
} else {
53+
setCurrentSession(null);
4954
deleteSessionAction(_id);
5055
}
5156
}, []);
@@ -86,7 +91,7 @@ export function ChatView({
8691
[router]
8792
);
8893

89-
const activeSession = initialSession ?? undefined;
94+
const activeSession = currentSession ?? undefined;
9095

9196
return (
9297
<Chat

0 commit comments

Comments
 (0)