From 99a8279e9fc151f432ef90a58c70bb4ca1bad0e1 Mon Sep 17 00:00:00 2001 From: nerfZael Date: Thu, 28 Dec 2023 14:49:41 +0100 Subject: [PATCH] supabase client is now a singleton --- apps/browser/app/page.tsx | 5 +- .../components/providers/Providers.tsx | 17 ++-- apps/browser/lib/hooks/useEvoService.ts | 16 ++-- apps/browser/lib/mutations/useAddChatLog.ts | 12 +-- apps/browser/lib/mutations/useAddMessages.ts | 12 +-- apps/browser/lib/mutations/useAddVariable.ts | 12 +-- apps/browser/lib/mutations/useCreateChat.ts | 12 +-- apps/browser/lib/mutations/useDeleteChat.ts | 12 +-- .../lib/mutations/useUpdateChatTitle.ts | 12 +-- apps/browser/lib/queries/useChats.ts | 83 ++++++++++++------- apps/browser/lib/services/evo/EvoService.ts | 6 +- apps/browser/lib/services/evo/EvoThread.ts | 55 ++++++------ .../lib/supabase/SupabaseClientProvider.tsx | 35 ++++++++ .../browser/lib/supabase/useSupabaseClient.ts | 9 -- .../lib/supabase/useSupabaseClient.tsx | 4 + 15 files changed, 187 insertions(+), 115 deletions(-) create mode 100644 apps/browser/lib/supabase/SupabaseClientProvider.tsx delete mode 100644 apps/browser/lib/supabase/useSupabaseClient.ts create mode 100644 apps/browser/lib/supabase/useSupabaseClient.tsx diff --git a/apps/browser/app/page.tsx b/apps/browser/app/page.tsx index a49659d1..17a70fa2 100644 --- a/apps/browser/app/page.tsx +++ b/apps/browser/app/page.tsx @@ -26,8 +26,10 @@ import { useRouter } from "next/navigation"; import { useAtom } from "jotai"; import { v4 as uuid } from "uuid"; import { InMemoryFile } from "@nerfzael/memory-fs"; +import { useSupabaseClient } from "@/lib/supabase/useSupabaseClient"; function Dojo({ params }: { params: { id?: string } }) { + const supabase = useSupabaseClient(); const [evoService, setEvoService] = useAtom(evoServiceAtom); const [newGoalSubmitted, setNewGoalSubmitted] = useAtom(newGoalSubmittedAtom); const [isChatLoading, setIsChatLoading] = useAtom(isChatLoadingAtom); @@ -43,13 +45,14 @@ function Dojo({ params }: { params: { id?: string } }) { const { data: chats, isLoading: isChatsLoading } = useChats(); const router = useRouter(); const { status: sessionStatus, data: sessionData } = useSession(); - const isAuthenticated = sessionStatus === "authenticated"; + const isAuthenticated = sessionStatus === "authenticated" && !!supabase; const { mutateAsync: createChat } = useCreateChat(); const { mutateAsync: updateChatTitle } = useUpdateChatTitle(); const { logs, isConnected, isStarting, isRunning, handleStart, status } = useEvoService( chatId, isAuthenticated, + supabase ); const workspaceUploadUpdate = useWorkspaceUploadUpdate(); diff --git a/apps/browser/components/providers/Providers.tsx b/apps/browser/components/providers/Providers.tsx index d960e474..6518870f 100644 --- a/apps/browser/components/providers/Providers.tsx +++ b/apps/browser/components/providers/Providers.tsx @@ -4,17 +4,20 @@ import { Provider as JotaiProvider } from "jotai"; import ReactQueryProvider from "./ReactQueryProvider"; import { SessionProvider } from "next-auth/react"; import ToastProvider from "./ToastProvider"; +import SupabaseClientProvider from "@/lib/supabase/SupabaseClientProvider"; export function Providers({ children }: { children: React.ReactNode }) { return ( - - - - {children} - - - + + + + + {children} + + + + ); } diff --git a/apps/browser/lib/hooks/useEvoService.ts b/apps/browser/lib/hooks/useEvoService.ts index caf0f593..1fcce57b 100644 --- a/apps/browser/lib/hooks/useEvoService.ts +++ b/apps/browser/lib/hooks/useEvoService.ts @@ -11,9 +11,8 @@ import { EvoThreadCallbacks, EvoThreadConfig } from "@/lib/services/evo/EvoThrea import { useAddChatLog } from "@/lib/mutations/useAddChatLog"; import { useAddMessages } from "@/lib/mutations/useAddMessages"; import { useAddVariable } from "@/lib/mutations/useAddVariable"; -import { useChats } from "@/lib/queries/useChats"; +import { fetchChats, useChats } from "@/lib/queries/useChats"; import { SupabaseWorkspace } from "@/lib/supabase/SupabaseWorkspace"; -import { useSupabaseClient } from "@/lib/supabase/useSupabaseClient"; import { useWorkspaceFilesUpdate } from "@/lib/hooks/useWorkspaceFilesUpdate"; import { useWorkspaceUploadUpdate } from "@/lib/hooks/useWorkspaceUploadUpdate"; import { ChatLog } from "@/components/Chat"; @@ -21,10 +20,12 @@ import { Workspace, InMemoryWorkspace } from "@evo-ninja/agent-utils"; import { ChatLogType, ChatMessage } from "@evo-ninja/agents"; import { useState, useEffect } from "react"; import { useAtom } from "jotai"; +import { EvoSupabaseClient } from "../supabase/EvoSupabaseClient"; export const useEvoService = ( chatId: string | "" | undefined, isAuthenticated: boolean, + supabase: EvoSupabaseClient | undefined ): { logs: ChatLog[]; isConnected: boolean; @@ -33,8 +34,6 @@ export const useEvoService = ( status: string | undefined; handleStart: (goal: string) => Promise; } => { - const supabase = useSupabaseClient(); - // Globals const [evoService] = useAtom(evoServiceAtom); const [allowTelemetry] = useAtom(allowTelemetryAtom); @@ -56,9 +55,6 @@ export const useEvoService = ( const { mutateAsync: addMessages } = useAddMessages(); const { mutateAsync: addVariable } = useAddVariable(); - // Queries - const { refetch: fetchChats } = useChats(); - // Helpers const workspaceFilesUpdate = useWorkspaceFilesUpdate(); const workspaceUploadUpdate = useWorkspaceUploadUpdate(); @@ -122,7 +118,7 @@ export const useEvoService = ( return []; } - const { data: chats, error } = await fetchChats(); + const { data: chats, error } = await fetchChats(supabase!); if (error) { console.error(error); @@ -140,8 +136,10 @@ export const useEvoService = ( }; async function loadWorkspace(chatId: string): Promise { + // isAuthenticated is only true if there's a supabase instance + // so we can safely assume that it's not undefined const workspace = isAuthenticated ? - new SupabaseWorkspace(chatId, supabase.storage) : + new SupabaseWorkspace(chatId, supabase!.storage) : new InMemoryWorkspace(); await workspaceUploadUpdate(workspace); diff --git a/apps/browser/lib/mutations/useAddChatLog.ts b/apps/browser/lib/mutations/useAddChatLog.ts index 66d2d381..715737de 100644 --- a/apps/browser/lib/mutations/useAddChatLog.ts +++ b/apps/browser/lib/mutations/useAddChatLog.ts @@ -1,8 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query" -import { useSession } from "next-auth/react" import { ChatLog } from "@/components/Chat" import { Row } from "../supabase/types" -import { createSupabaseClient } from "../supabase/createSupabaseClient" +import { useSupabaseClient } from "../supabase/useSupabaseClient" const mapChatLogToLogDTO = ( chatId: string, @@ -17,15 +16,18 @@ const mapChatLogToLogDTO = ( } export const useAddChatLog = () => { - const { data: session } = useSession() - const queryClient = useQueryClient() + const queryClient = useQueryClient(); + const supabase = useSupabaseClient(); return useMutation({ mutationFn: async (args: { chatId: string; log: ChatLog; }) => { - const supabase = createSupabaseClient(session?.supabaseAccessToken as string) + if (!supabase) { + throw new Error("Not authenticated"); + } + const { error } = await supabase .from("logs") .insert( diff --git a/apps/browser/lib/mutations/useAddMessages.ts b/apps/browser/lib/mutations/useAddMessages.ts index 35ecfa49..344f1c38 100644 --- a/apps/browser/lib/mutations/useAddMessages.ts +++ b/apps/browser/lib/mutations/useAddMessages.ts @@ -1,8 +1,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query" -import { useSession } from "next-auth/react" import { ChatLogType, ChatMessage } from "@evo-ninja/agents" import { Row } from "../supabase/types" -import { createSupabaseClient } from "../supabase/createSupabaseClient" +import { useSupabaseClient } from "../supabase/useSupabaseClient" const mapChatMessageToMessageDTO = ( chatId: string, @@ -64,8 +63,8 @@ const mapChatMessageToMessageDTO = ( } export const useAddMessages = () => { - const { data: session } = useSession() - const queryClient = useQueryClient() + const queryClient = useQueryClient(); + const supabase = useSupabaseClient(); return useMutation({ mutationFn: async (args: { @@ -73,7 +72,10 @@ export const useAddMessages = () => { messages: ChatMessage[]; type: ChatLogType; }) => { - const supabase = createSupabaseClient(session?.supabaseAccessToken as string) + if (!supabase) { + throw new Error("Not authenticated"); + } + const { error } = await supabase .from("messages") .insert( diff --git a/apps/browser/lib/mutations/useAddVariable.ts b/apps/browser/lib/mutations/useAddVariable.ts index ab8cfc8e..cce4be64 100644 --- a/apps/browser/lib/mutations/useAddVariable.ts +++ b/apps/browser/lib/mutations/useAddVariable.ts @@ -1,7 +1,6 @@ import { useMutation, useQueryClient } from "@tanstack/react-query" -import { useSession } from "next-auth/react" import { Row } from "../supabase/types" -import { createSupabaseClient } from "../supabase/createSupabaseClient" +import { useSupabaseClient } from "../supabase/useSupabaseClient" const mapVariableToVariableDTO = ( chatId: string, @@ -16,8 +15,8 @@ const mapVariableToVariableDTO = ( } export const useAddVariable = () => { - const { data: session } = useSession() - const queryClient = useQueryClient() + const queryClient = useQueryClient(); + const supabase = useSupabaseClient(); return useMutation({ mutationFn: async (args: { @@ -25,7 +24,10 @@ export const useAddVariable = () => { key: string; value: string; }) => { - const supabase = createSupabaseClient(session?.supabaseAccessToken as string) + if (!supabase) { + throw new Error("Not authenticated"); + } + const { error } = await supabase .from("variables") .insert( diff --git a/apps/browser/lib/mutations/useCreateChat.ts b/apps/browser/lib/mutations/useCreateChat.ts index baf06d70..72c6e4f0 100644 --- a/apps/browser/lib/mutations/useCreateChat.ts +++ b/apps/browser/lib/mutations/useCreateChat.ts @@ -1,15 +1,17 @@ -import { createSupabaseClient } from "../supabase/createSupabaseClient" import { Chat } from "@/lib/queries/useChats" import { useMutation, useQueryClient } from "@tanstack/react-query" -import { useSession } from "next-auth/react" +import { useSupabaseClient } from "../supabase/useSupabaseClient"; export const useCreateChat = () => { - const { data: session } = useSession() - const queryClient = useQueryClient() + const queryClient = useQueryClient(); + const supabase = useSupabaseClient(); return useMutation({ mutationFn: async (chatId: string) => { - const supabase = createSupabaseClient(session?.supabaseAccessToken as string) + if (!supabase) { + throw new Error("Not authenticated"); + } + const { data, error } = await supabase .from("chats") .insert({ diff --git a/apps/browser/lib/mutations/useDeleteChat.ts b/apps/browser/lib/mutations/useDeleteChat.ts index 25a6463b..2068231c 100644 --- a/apps/browser/lib/mutations/useDeleteChat.ts +++ b/apps/browser/lib/mutations/useDeleteChat.ts @@ -1,14 +1,16 @@ -import { createSupabaseClient } from "../supabase/createSupabaseClient" import { useMutation, useQueryClient } from "@tanstack/react-query" -import { useSession } from "next-auth/react" +import { useSupabaseClient } from "../supabase/useSupabaseClient"; export const useDeleteChat = () => { - const { data: session } = useSession() - const queryClient = useQueryClient() + const queryClient = useQueryClient(); + const supabase = useSupabaseClient(); return useMutation({ mutationFn: async (chatId: string) => { - const supabase = createSupabaseClient(session?.supabaseAccessToken as string) + if (!supabase) { + throw new Error("Not authenticated"); + } + const { error } = await supabase .from("chats") .delete() diff --git a/apps/browser/lib/mutations/useUpdateChatTitle.ts b/apps/browser/lib/mutations/useUpdateChatTitle.ts index 2e7b35b6..a0459985 100644 --- a/apps/browser/lib/mutations/useUpdateChatTitle.ts +++ b/apps/browser/lib/mutations/useUpdateChatTitle.ts @@ -1,16 +1,16 @@ -import { createSupabaseClient } from "../supabase/createSupabaseClient"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useSession } from "next-auth/react"; +import { useSupabaseClient } from "../supabase/useSupabaseClient"; export const useUpdateChatTitle = () => { - const { data: session } = useSession(); const queryClient = useQueryClient(); + const supabase = useSupabaseClient(); return useMutation({ mutationFn: async (args: { chatId: string; title: string }) => { - const supabase = createSupabaseClient( - session?.supabaseAccessToken as string - ); + if (!supabase) { + throw new Error("Not authenticated"); + } + const { error } = await supabase .from("chats") .update({ title: args.title }) diff --git a/apps/browser/lib/queries/useChats.ts b/apps/browser/lib/queries/useChats.ts index 84922c65..9bf97c46 100644 --- a/apps/browser/lib/queries/useChats.ts +++ b/apps/browser/lib/queries/useChats.ts @@ -3,7 +3,7 @@ import { useSession } from "next-auth/react" import { ChatMessage } from "@evo-ninja/agents" import { ChatLog } from "@/components/Chat" import { Json } from "../supabase/dbTypes" -import { createSupabaseClient } from "../supabase/createSupabaseClient" +import { useSupabaseClient } from "../supabase/useSupabaseClient" export interface Chat { id: string; @@ -110,45 +110,68 @@ const mapChatDTOtoChat = (dto: ChatDTO): Chat => { } } +export const fetchChats = async (supabase: any): Promise<{ + data: Chat[] | undefined, + error: Error | undefined +}> => { + const { data, error } = await supabase + .from('chats') + .select(` + id, + created_at, + logs(id, created_at, title, content, user), + variables(id, key, value), + title, + messages( + id, + created_at, + content, + name, + function_call, + tool_calls, + temporary, + role, + tool_call_id + ) + `).order( + 'created_at', + { ascending: false } + ) + + if (error) { + return { + data: undefined, + error: error + } + } + + return { + data: data.map(mapChatDTOtoChat), + error: undefined + }; +} + export const useChats = () => { - const { data: session } = useSession() + const { data: session } = useSession(); + const supabase = useSupabaseClient(); return useQuery({ - queryKey: ['chats'], - enabled: !!session?.user?.email, + queryKey: ['chats', session?.user?.email, supabase], + enabled: !!session?.user?.email && !!supabase, refetchOnMount: false, queryFn: async () => { - const supabase = createSupabaseClient(session?.supabaseAccessToken as string) - const { data, error } = await supabase - .from('chats') - .select(` - id, - created_at, - logs(id, created_at, title, content, user), - variables(id, key, value), - title, - messages( - id, - created_at, - content, - name, - function_call, - tool_calls, - temporary, - role, - tool_call_id - ) - `).order( - 'created_at', - { ascending: false } - ) + if (!session?.user?.email || !supabase) { + throw new Error("Not authenticated") + } + + const { data, error } = await fetchChats(supabase); if (error) { console.error(error) - throw new Error(error.message) + throw new Error(error.message); } - return data.map(mapChatDTOtoChat) + return data; } }) } \ No newline at end of file diff --git a/apps/browser/lib/services/evo/EvoService.ts b/apps/browser/lib/services/evo/EvoService.ts index 94bcd4a9..65e09fe2 100644 --- a/apps/browser/lib/services/evo/EvoService.ts +++ b/apps/browser/lib/services/evo/EvoService.ts @@ -31,7 +31,7 @@ export class EvoService { } async connect(config: EvoThreadConfig, callbacks: EvoThreadCallbacks): Promise { - this._current = this.acquireThread(config); + this._current = await this.acquireThread(config); await this._current.connect(callbacks); } @@ -42,9 +42,9 @@ export class EvoService { return this._current.start(options); } - private acquireThread(config: EvoThreadConfig): EvoThread { + private async acquireThread(config: EvoThreadConfig): Promise { if (!this._threads[config.chatId]) { - this._threads[config.chatId] = new EvoThread(config); + this._threads[config.chatId] = await EvoThread.load(config); } return this._threads[config.chatId]; } diff --git a/apps/browser/lib/services/evo/EvoThread.ts b/apps/browser/lib/services/evo/EvoThread.ts index 9aaf1ad3..14cd9fe8 100644 --- a/apps/browser/lib/services/evo/EvoThread.ts +++ b/apps/browser/lib/services/evo/EvoThread.ts @@ -58,17 +58,45 @@ export class EvoThread { private _state: EvoThreadState; private _callbacks?: EvoThreadCallbacks; - constructor( + protected constructor( private _config: EvoThreadConfig ) { this._state = Object.assign({}, INIT_STATE); - this.load(); } get chatId(): string { return this._config.chatId; } + public static async load( + config: EvoThreadConfig + ): Promise { + const thread = new EvoThread(config); + + const chatId = thread._config.chatId; + thread._state.isLoading = true; + + const results = await Promise.all<[ + Promise, + Promise + ]>([ + thread._config.loadChatLog(chatId).catch((reason) => { + thread._callbacks?.onError(reason.toString()); + return []; + }), + thread._config.loadWorkspace(chatId).catch((reason) => { + thread._callbacks?.onError(reason.toString()); + return new InMemoryWorkspace(); + }) + ]); + + thread._state.logs = results[0]; + thread._state.workspace = results[1]; + thread._state.isLoading = false; + + return thread; + } + destroy() { // Destroy all child objects & processes } @@ -167,29 +195,6 @@ export class EvoThread { this._state.goal = undefined; } - private async load() { - const chatId = this._config.chatId; - this._state.isLoading = true; - - const results = await Promise.all<[ - Promise, - Promise - ]>([ - this._config.loadChatLog(chatId).catch((reason) => { - this._callbacks?.onError(reason.toString()); - return []; - }), - this._config.loadWorkspace(chatId).catch((reason) => { - this._callbacks?.onError(reason.toString()); - return new InMemoryWorkspace(); - }) - ]); - - this._state.logs = results[0]; - this._state.workspace = results[1]; - this._state.isLoading = false; - } - private async waitForLoad() { while (this._state.isLoading) { await new Promise((resolve) => diff --git a/apps/browser/lib/supabase/SupabaseClientProvider.tsx b/apps/browser/lib/supabase/SupabaseClientProvider.tsx new file mode 100644 index 00000000..5a3536cb --- /dev/null +++ b/apps/browser/lib/supabase/SupabaseClientProvider.tsx @@ -0,0 +1,35 @@ +import { EvoSupabaseClient } from "./EvoSupabaseClient"; +import { createSupabaseClient } from "./createSupabaseClient"; + +import { useSession } from "next-auth/react"; +import { createContext, useEffect, useRef, useState } from "react"; +import React from "react"; + +export const SupabaseClientContext = createContext(undefined); + +const SupabaseClientProvider = ({ children }: { children: React.ReactNode}) => { + const { data: session } = useSession(); + const [supabaseClient, setSupabaseClient] = useState(undefined); + const [lastAccessToken, setLastAccessToken] = useState(undefined); + + useEffect(() => { + if (!session?.supabaseAccessToken) { + setSupabaseClient(undefined); + setLastAccessToken(undefined) + return; + } + + if (lastAccessToken === session.supabaseAccessToken) { + return; + } + setSupabaseClient(createSupabaseClient(session.supabaseAccessToken)); + setLastAccessToken(session.supabaseAccessToken); + }, [session?.supabaseAccessToken]); + + return ( + + {children} + + ); +}; +export default SupabaseClientProvider diff --git a/apps/browser/lib/supabase/useSupabaseClient.ts b/apps/browser/lib/supabase/useSupabaseClient.ts deleted file mode 100644 index 730d36eb..00000000 --- a/apps/browser/lib/supabase/useSupabaseClient.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EvoSupabaseClient } from "./EvoSupabaseClient"; -import { createSupabaseClient } from "./createSupabaseClient"; - -import { useSession } from "next-auth/react"; - -export const useSupabaseClient = (): EvoSupabaseClient => { - const { data: session } = useSession(); - return createSupabaseClient(session?.supabaseAccessToken as string); -}; diff --git a/apps/browser/lib/supabase/useSupabaseClient.tsx b/apps/browser/lib/supabase/useSupabaseClient.tsx new file mode 100644 index 00000000..b48046a6 --- /dev/null +++ b/apps/browser/lib/supabase/useSupabaseClient.tsx @@ -0,0 +1,4 @@ +import React from "react"; +import { SupabaseClientContext } from "./SupabaseClientProvider"; + +export const useSupabaseClient = () => React.useContext(SupabaseClientContext);