Skip to content

Commit 16f567b

Browse files
committed
getChat()をキャッシュ
1 parent 8fcfe2c commit 16f567b

File tree

4 files changed

+130
-21
lines changed

4 files changed

+130
-21
lines changed

app/[docs_id]/chatHistory.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"use client";
22

3-
import { ChatWithMessages } from "@/lib/chatHistory";
3+
import { ChatWithMessages, getChat } from "@/lib/chatHistory";
44
import {
55
createContext,
66
ReactNode,
77
useContext,
88
useEffect,
99
useState,
1010
} from "react";
11+
import useSWR from "swr";
1112

1213
export interface IChatHistoryContext {
1314
chatHistories: ChatWithMessages[];
@@ -27,19 +28,32 @@ export function useChatHistoryContext() {
2728

2829
export function ChatHistoryProvider({
2930
children,
31+
docs_id,
3032
initialChatHistories,
3133
}: {
3234
children: ReactNode;
35+
docs_id: string;
3336
initialChatHistories: ChatWithMessages[];
3437
}) {
3538
const [chatHistories, setChatHistories] =
3639
useState<ChatWithMessages[]>(initialChatHistories);
40+
// 最初はSSRで取得したinitialChatHistoriesを使用(キャッシュされているので古い可能性がある)
3741
useEffect(() => {
3842
setChatHistories(initialChatHistories);
3943
}, [initialChatHistories]);
44+
// その後、クライアント側で最新のchatHistoriesを改めて取得して更新する
45+
const { data: fetchedChatHistories } = useSWR<ChatWithMessages[]>(
46+
docs_id,
47+
getChat
48+
);
49+
useEffect(() => {
50+
if (fetchedChatHistories) {
51+
setChatHistories(fetchedChatHistories);
52+
}
53+
}, [fetchedChatHistories]);
4054

55+
// チャットを更新した際にはクライアントサイドでchatHistoryに反映する
4156
const addChat = (chat: ChatWithMessages) => {
42-
// サーバー側で追加された新しいchatをクライアント側にも反映する
4357
setChatHistories([...chatHistories, chat]);
4458
};
4559

app/[docs_id]/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { MarkdownSection, splitMarkdown } from "./splitMarkdown";
66
import pyodideLock from "pyodide/pyodide-lock.json";
77
import { PageContent } from "./pageContent";
88
import { ChatHistoryProvider } from "./chatHistory";
9-
import { getChat } from "@/lib/chatHistory";
9+
import { getChat, getChatFromCache } from "@/lib/chatHistory";
1010

1111
export default async function Page({
1212
params,
@@ -50,10 +50,10 @@ export default async function Page({
5050
splitMarkdown(text)
5151
);
5252

53-
const initialChatHistories = getChat(docs_id);
53+
const initialChatHistories = getChatFromCache(docs_id);
5454

5555
return (
56-
<ChatHistoryProvider initialChatHistories={await initialChatHistories}>
56+
<ChatHistoryProvider initialChatHistories={await initialChatHistories} docs_id={docs_id}>
5757
<PageContent
5858
documentContent={await mdContent}
5959
splitMdContent={await splitMdContent}

app/lib/cache.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const cacheData: Map<string, ArrayBuffer> = new Map();
2+
/**
3+
* nodejsにcache apiがないので、web標準のcache APIに相当するものの自前実装
4+
*/
5+
export const inMemoryCache = {
6+
async put(key: string, response: Response): Promise<void> {
7+
const arrayBuffer = await response.arrayBuffer();
8+
cacheData.set(key, arrayBuffer);
9+
},
10+
async match(key: string): Promise<Response | undefined> {
11+
const arrayBuffer = cacheData.get(key);
12+
if (arrayBuffer) {
13+
return new Response(arrayBuffer);
14+
}
15+
return undefined;
16+
},
17+
async delete(key: string): Promise<boolean> {
18+
return cacheData.delete(key);
19+
},
20+
} as const;

app/lib/chatHistory.ts

Lines changed: 91 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,74 @@
1+
"use server";
2+
13
import { headers } from "next/headers";
24
import { getAuthServer } from "./auth";
35
import { getDrizzle } from "./drizzle";
46
import { chat, message } from "@/schema/chat";
57
import { and, asc, eq } from "drizzle-orm";
8+
import { Auth } from "better-auth";
9+
import { inMemoryCache } from "./cache";
610

711
export interface CreateChatMessage {
812
role: "user" | "ai" | "error";
913
content: string;
1014
}
1115

16+
// cacheに使うキーで、実際のURLではない
17+
const CACHE_KEY_BASE = "https://my-code.utcode.net/chatHistory";
18+
19+
interface Context {
20+
drizzle: Awaited<ReturnType<typeof getDrizzle>>;
21+
auth: Auth;
22+
userId: string;
23+
}
24+
/**
25+
* drizzleが初期化されてなければ初期化し、
26+
* authが初期化されてなければ初期化し、
27+
* userIdがなければセッションから取得してセットする。
28+
*/
29+
async function initAll(ctx?: Partial<Context>): Promise<Context> {
30+
if (!ctx) {
31+
ctx = {};
32+
}
33+
if (!ctx.drizzle) {
34+
ctx.drizzle = await getDrizzle();
35+
}
36+
if (!ctx.auth) {
37+
ctx.auth = await getAuthServer(ctx.drizzle);
38+
}
39+
if (!ctx.userId) {
40+
const session = await ctx.auth.api.getSession({
41+
headers: await headers(),
42+
});
43+
if (!session) {
44+
throw new Error("Not authenticated");
45+
}
46+
ctx.userId = session.user.id;
47+
}
48+
return ctx as Context;
49+
}
50+
async function getCache() {
51+
if ("caches" in globalThis) {
52+
// worker
53+
return await caches.open("chatHistory");
54+
} else {
55+
// nodejs
56+
return inMemoryCache;
57+
}
58+
}
59+
1260
export async function addChat(
1361
docsId: string,
1462
sectionId: string,
15-
messages: CreateChatMessage[]
63+
messages: CreateChatMessage[],
64+
context?: Partial<Context>
1665
) {
17-
const drizzle = await getDrizzle();
18-
const auth = await getAuthServer(drizzle);
19-
const session = await auth.api.getSession({ headers: await headers() });
20-
if (!session) {
21-
throw new Error("Not authenticated");
22-
}
66+
const { drizzle, userId } = await initAll(context);
2367

2468
const [newChat] = await drizzle
2569
.insert(chat)
2670
.values({
27-
userId: session.user.id,
71+
userId,
2872
docsId,
2973
sectionId,
3074
})
@@ -41,6 +85,14 @@ export async function addChat(
4185
)
4286
.returning();
4387

88+
console.log(
89+
`deleting cache for chatHistory/getChat for user ${userId} and docs ${docsId}`
90+
);
91+
const cache = await getCache();
92+
await cache.delete(
93+
`${CACHE_KEY_BASE}/getChat?docsId=${docsId}&userId=${userId}`
94+
);
95+
4496
return {
4597
...newChat,
4698
messages: chatMessages,
@@ -49,16 +101,14 @@ export async function addChat(
49101

50102
export type ChatWithMessages = Awaited<ReturnType<typeof addChat>>;
51103

52-
export async function getChat(docsId: string) {
53-
const drizzle = await getDrizzle();
54-
const auth = await getAuthServer(drizzle);
55-
const session = await auth.api.getSession({ headers: await headers() });
56-
if (!session) {
57-
return [];
58-
}
104+
export async function getChat(
105+
docsId: string,
106+
context?: Partial<Context>
107+
): Promise<ChatWithMessages[]> {
108+
const { drizzle, userId } = await initAll(context);
59109

60110
const chats = await drizzle.query.chat.findMany({
61-
where: and(eq(chat.userId, session.user.id), eq(chat.docsId, docsId)),
111+
where: and(eq(chat.userId, userId), eq(chat.docsId, docsId)),
62112
with: {
63113
messages: {
64114
orderBy: [asc(message.createdAt)],
@@ -67,8 +117,33 @@ export async function getChat(docsId: string) {
67117
orderBy: [asc(chat.createdAt)],
68118
});
69119

120+
const cache = await getCache();
121+
await cache.put(
122+
`${CACHE_KEY_BASE}/getChat?docsId=${docsId}&userId=${userId}`,
123+
new Response(JSON.stringify(chats), {
124+
headers: { "Cache-Control": "max-age=86400, s-maxage=86400" },
125+
})
126+
);
70127
return chats;
71128
}
129+
export async function getChatFromCache(
130+
docsId: string,
131+
context?: Partial<Context>
132+
) {
133+
const { drizzle, auth, userId } = await initAll(context);
134+
135+
const cache = await getCache();
136+
const cachedResponse = await cache.match(
137+
`${CACHE_KEY_BASE}/getChat?docsId=${docsId}&userId=${userId}`
138+
);
139+
if (cachedResponse) {
140+
console.log("Cache hit for chatHistory/getChat");
141+
const data = (await cachedResponse.json()) as ChatWithMessages[];
142+
return data;
143+
}
144+
console.log("Cache miss for chatHistory/getChat");
145+
return await getChat(docsId, { drizzle, auth, userId });
146+
}
72147

73148
export async function migrateChatUser(oldUserId: string, newUserId: string) {
74149
const drizzle = await getDrizzle();

0 commit comments

Comments
 (0)