prevent preloads from firing twice when in React strictMode
This commit is contained in:
@@ -393,6 +393,7 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
|
||||
private sessions: Map<string, ChatSessionState> = new Map();
|
||||
private activeStreams: Map<string, AbortController> = new Map();
|
||||
private pendingPreloads: Map<string, Promise<void>> = new Map();
|
||||
|
||||
constructor(options: TriggerChatTransportOptions) {
|
||||
this.taskId = options.task;
|
||||
@@ -800,26 +801,38 @@ export class TriggerChatTransport implements ChatTransport<UIMessage> {
|
||||
// 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<string> {
|
||||
|
||||
@@ -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(() => { });
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<ChatSidebar
|
||||
chats={chatList}
|
||||
@@ -75,6 +82,7 @@ export function ChatSidebarWrapper({
|
||||
onSelectChat={handleSelectChat}
|
||||
onNewChat={handleNewChat}
|
||||
onDeleteChat={handleDeleteChat}
|
||||
onWipeAll={handleWipeAll}
|
||||
preloadEnabled={preloadEnabled}
|
||||
onPreloadChange={setPreloadEnabled}
|
||||
idleTimeoutInSeconds={idleTimeoutInSeconds}
|
||||
|
||||
@@ -24,6 +24,7 @@ type ChatSidebarProps = {
|
||||
onSelectChat: (id: string) => 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({
|
||||
<option value="ai-chat-session">ai-chat-session (session)</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onWipeAll}
|
||||
className="w-full rounded border border-red-300 px-2 py-1 text-xs text-red-600 hover:bg-red-50"
|
||||
>
|
||||
Wipe all chats
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<SessionInfo | null>(initialSession);
|
||||
|
||||
const sessions: Record<string, SessionInfo> = {};
|
||||
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 (
|
||||
<Chat
|
||||
|
||||
Reference in New Issue
Block a user