From fb360be0d24f04b2964d6811d55962c5255cb3e8 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 14 Apr 2026 15:00:05 +0100 Subject: [PATCH] prevent preloads from firing twice when in React strictMode --- packages/trigger-sdk/src/v3/chat.ts | 47 ++++++++++++------- references/ai-chat/src/app/actions.ts | 5 ++ .../src/components/chat-sidebar-wrapper.tsx | 10 +++- .../ai-chat/src/components/chat-sidebar.tsx | 9 ++++ .../ai-chat/src/components/chat-view.tsx | 11 +++-- 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/packages/trigger-sdk/src/v3/chat.ts b/packages/trigger-sdk/src/v3/chat.ts index 258c6dfc3..208489097 100644 --- a/packages/trigger-sdk/src/v3/chat.ts +++ b/packages/trigger-sdk/src/v3/chat.ts @@ -393,6 +393,7 @@ export class TriggerChatTransport implements ChatTransport { private sessions: Map = new Map(); private activeStreams: Map = new Map(); + private pendingPreloads: Map> = new Map(); constructor(options: TriggerChatTransportOptions) { this.taskId = options.task; @@ -800,26 +801,38 @@ export class TriggerChatTransport implements ChatTransport { // Don't preload if session already exists if (this.sessions.get(chatId)?.runId) return; - const mergedMetadata = - this.defaultMetadata || options?.metadata - ? { ...(this.defaultMetadata ?? {}), ...(options?.metadata ?? {}) } - : undefined; + // Deduplicate concurrent preload calls (e.g. React strict mode double-firing effects) + const pending = this.pendingPreloads.get(chatId); + if (pending) return pending; - const payload = { - messages: [] as never[], - chatId, - trigger: "preload" as const, - metadata: mergedMetadata, - ...(options?.idleTimeoutInSeconds !== undefined - ? { idleTimeoutInSeconds: options.idleTimeoutInSeconds } - : {}), + const doPreload = async () => { + const mergedMetadata = + this.defaultMetadata || options?.metadata + ? { ...(this.defaultMetadata ?? {}), ...(options?.metadata ?? {}) } + : undefined; + + const payload = { + messages: [] as never[], + chatId, + trigger: "preload" as const, + metadata: mergedMetadata, + ...(options?.idleTimeoutInSeconds !== undefined + ? { idleTimeoutInSeconds: options.idleTimeoutInSeconds } + : {}), + }; + + const { runId, publicAccessToken } = await this.triggerNewRun(chatId, payload, "preload"); + + const newSession: ChatSessionState = { runId, publicAccessToken }; + this.sessions.set(chatId, newSession); + this.notifySessionChange(chatId, newSession); }; - const { runId, publicAccessToken } = await this.triggerNewRun(chatId, payload, "preload"); - - const newSession: ChatSessionState = { runId, publicAccessToken }; - this.sessions.set(chatId, newSession); - this.notifySessionChange(chatId, newSession); + const promise = doPreload().finally(() => { + this.pendingPreloads.delete(chatId); + }); + this.pendingPreloads.set(chatId, promise); + return promise; } private async resolveAccessToken(params: ResolveChatAccessTokenParams): Promise { diff --git a/references/ai-chat/src/app/actions.ts b/references/ai-chat/src/app/actions.ts index 29f974b8c..d4d681e33 100644 --- a/references/ai-chat/src/app/actions.ts +++ b/references/ai-chat/src/app/actions.ts @@ -98,6 +98,11 @@ export async function deleteChat(chatId: string) { await prisma.chatSession.delete({ where: { id: chatId } }).catch(() => { }); } +export async function deleteAllChats() { + await prisma.chatSession.deleteMany(); + await prisma.chat.deleteMany(); +} + export async function updateChatTitle(chatId: string, title: string) { await prisma.chat.update({ where: { id: chatId }, data: { title } }).catch(() => { }); } diff --git a/references/ai-chat/src/components/chat-sidebar-wrapper.tsx b/references/ai-chat/src/components/chat-sidebar-wrapper.tsx index d489818bb..ecfcb10e1 100644 --- a/references/ai-chat/src/components/chat-sidebar-wrapper.tsx +++ b/references/ai-chat/src/components/chat-sidebar-wrapper.tsx @@ -5,7 +5,7 @@ import { ChatSidebar } from "@/components/chat-sidebar"; import { useChatSettings } from "@/components/chat-settings-context"; import { useState, useCallback, useEffect } from "react"; import { generateId } from "ai"; -import { getChatList, deleteChat as deleteChatAction } from "@/app/actions"; +import { getChatList, deleteChat as deleteChatAction, deleteAllChats } from "@/app/actions"; type ChatMeta = { id: string; @@ -68,6 +68,13 @@ export function ChatSidebarWrapper({ } } + async function handleWipeAll() { + if (!confirm("Delete ALL chats? This cannot be undone.")) return; + await deleteAllChats(); + setChatList([]); + router.push("/chats"); + } + return ( void; onNewChat: () => void; onDeleteChat: (id: string) => void; + onWipeAll: () => void; preloadEnabled: boolean; onPreloadChange: (enabled: boolean) => void; idleTimeoutInSeconds: number; @@ -38,6 +39,7 @@ export function ChatSidebar({ onSelectChat, onNewChat, onDeleteChat, + onWipeAll, preloadEnabled, onPreloadChange, idleTimeoutInSeconds, @@ -124,6 +126,13 @@ export function ChatSidebar({ + ); diff --git a/references/ai-chat/src/components/chat-view.tsx b/references/ai-chat/src/components/chat-view.tsx index b1c7cb614..0cf82a9bf 100644 --- a/references/ai-chat/src/components/chat-view.tsx +++ b/references/ai-chat/src/components/chat-view.tsx @@ -12,7 +12,7 @@ import { deleteSessionAction, renewRunAccessTokenForChat, } from "@/app/actions"; -import { useCallback, useEffect } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; type SessionInfo = { @@ -39,13 +39,18 @@ export function ChatView({ const router = useRouter(); const { taskMode, preloadEnabled, idleTimeoutInSeconds } = useChatSettings(); + const [currentSession, setCurrentSession] = useState(initialSession); + const sessions: Record = {}; if (initialSession) { sessions[chatId] = initialSession; } const handleSessionChange = useCallback((_id: string, session: SessionInfo | null) => { - if (!session) { + if (session) { + setCurrentSession(session); + } else { + setCurrentSession(null); deleteSessionAction(_id); } }, []); @@ -86,7 +91,7 @@ export function ChatView({ [router] ); - const activeSession = initialSession ?? undefined; + const activeSession = currentSession ?? undefined; return (