feat: support resuming ongoing streams (#974)

This commit is contained in:
Jeremy 2025-05-01 12:36:52 -07:00 committed by GitHub
parent 45978c27a2
commit a3221fbcdc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1005 additions and 47 deletions

View file

@ -13,6 +13,7 @@ jobs:
AUTH_SECRET: ${{ secrets.AUTH_SECRET }} AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
POSTGRES_URL: ${{ secrets.POSTGRES_URL }} POSTGRES_URL: ${{ secrets.POSTGRES_URL }}
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }} BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
REDIS_URL: ${{ secrets.REDIS_URL }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -1,17 +1,19 @@
import { import {
appendClientMessage, appendClientMessage,
appendResponseMessages, appendResponseMessages,
createDataStreamResponse, createDataStream,
smoothStream, smoothStream,
streamText, streamText,
} from 'ai'; } from 'ai';
import { auth, type UserType } from '@/app/(auth)/auth'; import { auth, type UserType } from '@/app/(auth)/auth';
import { type RequestHints, systemPrompt } from '@/lib/ai/prompts'; import { type RequestHints, systemPrompt } from '@/lib/ai/prompts';
import { import {
createStreamId,
deleteChatById, deleteChatById,
getChatById, getChatById,
getMessageCountByUserId, getMessageCountByUserId,
getMessagesByChatId, getMessagesByChatId,
getStreamIdsByChatId,
saveChat, saveChat,
saveMessages, saveMessages,
} from '@/lib/db/queries'; } from '@/lib/db/queries';
@ -26,9 +28,16 @@ import { myProvider } from '@/lib/ai/providers';
import { entitlementsByUserType } from '@/lib/ai/entitlements'; import { entitlementsByUserType } from '@/lib/ai/entitlements';
import { postRequestBodySchema, type PostRequestBody } from './schema'; import { postRequestBodySchema, type PostRequestBody } from './schema';
import { geolocation } from '@vercel/functions'; import { geolocation } from '@vercel/functions';
import { createResumableStreamContext } from 'resumable-stream';
import { after } from 'next/server';
import type { Chat } from '@/lib/db/schema';
export const maxDuration = 60; export const maxDuration = 60;
const streamContext = createResumableStreamContext({
waitUntil: after,
});
export async function POST(request: Request) { export async function POST(request: Request) {
let requestBody: PostRequestBody; let requestBody: PostRequestBody;
@ -108,7 +117,10 @@ export async function POST(request: Request) {
], ],
}); });
return createDataStreamResponse({ const streamId = generateUUID();
await createStreamId({ streamId, chatId: id });
const stream = createDataStream({
execute: (dataStream) => { execute: (dataStream) => {
const result = streamText({ const result = streamText({
model: myProvider.languageModel(selectedChatModel), model: myProvider.languageModel(selectedChatModel),
@ -187,6 +199,10 @@ export async function POST(request: Request) {
return 'Oops, an error occurred!'; return 'Oops, an error occurred!';
}, },
}); });
return new Response(
await streamContext.resumableStream(streamId, () => stream),
);
} catch (_) { } catch (_) {
return new Response('An error occurred while processing your request!', { return new Response('An error occurred while processing your request!', {
status: 500, status: 500,
@ -194,6 +210,60 @@ export async function POST(request: Request) {
} }
} }
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const chatId = searchParams.get('chatId');
if (!chatId) {
return new Response('id is required', { status: 400 });
}
const session = await auth();
if (!session?.user) {
return new Response('Unauthorized', { status: 401 });
}
let chat: Chat;
try {
chat = await getChatById({ id: chatId });
} catch {
return new Response('Not found', { status: 404 });
}
if (!chat) {
return new Response('Not found', { status: 404 });
}
if (chat.userId !== session.user.id) {
return new Response('Forbidden', { status: 403 });
}
const streamIds = await getStreamIdsByChatId({ chatId });
if (!streamIds.length) {
return new Response('No streams found', { status: 404 });
}
const recentStreamId = streamIds.at(-1);
if (!recentStreamId) {
return new Response('No recent stream found', { status: 404 });
}
const emptyDataStream = createDataStream({
execute: () => {},
});
return new Response(
await streamContext.resumableStream(recentStreamId, () => emptyDataStream),
{
status: 200,
},
);
}
export async function DELETE(request: Request) { export async function DELETE(request: Request) {
const { searchParams } = new URL(request.url); const { searchParams } = new URL(request.url);
const id = searchParams.get('id'); const id = searchParams.get('id');
@ -219,6 +289,7 @@ export async function DELETE(request: Request) {
return Response.json(deletedChat, { status: 200 }); return Response.json(deletedChat, { status: 200 });
} catch (error) { } catch (error) {
console.error(error);
return new Response('An error occurred while processing your request!', { return new Response('An error occurred while processing your request!', {
status: 500, status: 500,
}); });

View file

@ -64,6 +64,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
selectedVisibilityType={chat.visibility} selectedVisibilityType={chat.visibility}
isReadonly={session?.user?.id !== chat.userId} isReadonly={session?.user?.id !== chat.userId}
session={session} session={session}
autoResume={true}
/> />
<DataStreamHandler id={id} /> <DataStreamHandler id={id} />
</> </>
@ -79,6 +80,7 @@ export default async function Page(props: { params: Promise<{ id: string }> }) {
selectedVisibilityType={chat.visibility} selectedVisibilityType={chat.visibility}
isReadonly={session?.user?.id !== chat.userId} isReadonly={session?.user?.id !== chat.userId}
session={session} session={session}
autoResume={true}
/> />
<DataStreamHandler id={id} /> <DataStreamHandler id={id} />
</> </>

View file

@ -30,6 +30,7 @@ export default async function Page() {
selectedVisibilityType="private" selectedVisibilityType="private"
isReadonly={false} isReadonly={false}
session={session} session={session}
autoResume={false}
/> />
<DataStreamHandler id={id} /> <DataStreamHandler id={id} />
</> </>
@ -46,6 +47,7 @@ export default async function Page() {
selectedVisibilityType="private" selectedVisibilityType="private"
isReadonly={false} isReadonly={false}
session={session} session={session}
autoResume={false}
/> />
<DataStreamHandler id={id} /> <DataStreamHandler id={id} />
</> </>

View file

@ -25,6 +25,7 @@ export function Chat({
selectedVisibilityType, selectedVisibilityType,
isReadonly, isReadonly,
session, session,
autoResume,
}: { }: {
id: string; id: string;
initialMessages: Array<UIMessage>; initialMessages: Array<UIMessage>;
@ -32,6 +33,7 @@ export function Chat({
selectedVisibilityType: VisibilityType; selectedVisibilityType: VisibilityType;
isReadonly: boolean; isReadonly: boolean;
session: Session; session: Session;
autoResume: boolean;
}) { }) {
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
@ -45,6 +47,7 @@ export function Chat({
status, status,
stop, stop,
reload, reload,
experimental_resume,
} = useChat({ } = useChat({
id, id,
initialMessages, initialMessages,
@ -67,6 +70,15 @@ export function Chat({
}, },
}); });
useEffect(() => {
if (autoResume) {
experimental_resume();
}
// note: this hook has no dependencies since it only needs to run once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const query = searchParams.get('query'); const query = searchParams.get('query');

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS "Stream" (
"id" uuid DEFAULT gen_random_uuid() NOT NULL,
"chatId" uuid NOT NULL,
"createdAt" timestamp NOT NULL,
CONSTRAINT "Stream_id_pk" PRIMARY KEY("id")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "Stream" ADD CONSTRAINT "Stream_chatId_Chat_id_fk" FOREIGN KEY ("chatId") REFERENCES "public"."Chat"("id") ON DELETE no action ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View file

@ -0,0 +1,565 @@
{
"id": "443de550-b7e8-4bfb-b229-c12dc6c132f0",
"prevId": "c6c102e6-b64e-4f0c-a7a6-32df758de437",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.Chat": {
"name": "Chat",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"visibility": {
"name": "visibility",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'private'"
}
},
"indexes": {},
"foreignKeys": {
"Chat_userId_User_id_fk": {
"name": "Chat_userId_User_id_fk",
"tableFrom": "Chat",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Document": {
"name": "Document",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": false
},
"text": {
"name": "text",
"type": "varchar",
"primaryKey": false,
"notNull": true,
"default": "'text'"
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Document_userId_User_id_fk": {
"name": "Document_userId_User_id_fk",
"tableFrom": "Document",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Document_id_createdAt_pk": {
"name": "Document_id_createdAt_pk",
"columns": [
"id",
"createdAt"
]
}
},
"uniqueConstraints": {}
},
"public.Message_v2": {
"name": "Message_v2",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"parts": {
"name": "parts",
"type": "json",
"primaryKey": false,
"notNull": true
},
"attachments": {
"name": "attachments",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_v2_chatId_Chat_id_fk": {
"name": "Message_v2_chatId_Chat_id_fk",
"tableFrom": "Message_v2",
"tableTo": "Chat",
"columnsFrom": [
"chatId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Message": {
"name": "Message",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"role": {
"name": "role",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "json",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Message_chatId_Chat_id_fk": {
"name": "Message_chatId_Chat_id_fk",
"tableFrom": "Message",
"tableTo": "Chat",
"columnsFrom": [
"chatId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Stream": {
"name": "Stream",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Stream_chatId_Chat_id_fk": {
"name": "Stream_chatId_Chat_id_fk",
"tableFrom": "Stream",
"tableTo": "Chat",
"columnsFrom": [
"chatId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Stream_id_pk": {
"name": "Stream_id_pk",
"columns": [
"id"
]
}
},
"uniqueConstraints": {}
},
"public.Suggestion": {
"name": "Suggestion",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"documentId": {
"name": "documentId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"documentCreatedAt": {
"name": "documentCreatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
},
"originalText": {
"name": "originalText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"suggestedText": {
"name": "suggestedText",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"isResolved": {
"name": "isResolved",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"userId": {
"name": "userId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Suggestion_userId_User_id_fk": {
"name": "Suggestion_userId_User_id_fk",
"tableFrom": "Suggestion",
"tableTo": "User",
"columnsFrom": [
"userId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk": {
"name": "Suggestion_documentId_documentCreatedAt_Document_id_createdAt_fk",
"tableFrom": "Suggestion",
"tableTo": "Document",
"columnsFrom": [
"documentId",
"documentCreatedAt"
],
"columnsTo": [
"id",
"createdAt"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Suggestion_id_pk": {
"name": "Suggestion_id_pk",
"columns": [
"id"
]
}
},
"uniqueConstraints": {}
},
"public.User": {
"name": "User",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"email": {
"name": "email",
"type": "varchar(64)",
"primaryKey": false,
"notNull": true
},
"password": {
"name": "password",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.Vote_v2": {
"name": "Vote_v2",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_v2_chatId_Chat_id_fk": {
"name": "Vote_v2_chatId_Chat_id_fk",
"tableFrom": "Vote_v2",
"tableTo": "Chat",
"columnsFrom": [
"chatId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_v2_messageId_Message_v2_id_fk": {
"name": "Vote_v2_messageId_Message_v2_id_fk",
"tableFrom": "Vote_v2",
"tableTo": "Message_v2",
"columnsFrom": [
"messageId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_v2_chatId_messageId_pk": {
"name": "Vote_v2_chatId_messageId_pk",
"columns": [
"chatId",
"messageId"
]
}
},
"uniqueConstraints": {}
},
"public.Vote": {
"name": "Vote",
"schema": "",
"columns": {
"chatId": {
"name": "chatId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"messageId": {
"name": "messageId",
"type": "uuid",
"primaryKey": false,
"notNull": true
},
"isUpvoted": {
"name": "isUpvoted",
"type": "boolean",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"Vote_chatId_Chat_id_fk": {
"name": "Vote_chatId_Chat_id_fk",
"tableFrom": "Vote",
"tableTo": "Chat",
"columnsFrom": [
"chatId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"Vote_messageId_Message_id_fk": {
"name": "Vote_messageId_Message_id_fk",
"tableFrom": "Vote",
"tableTo": "Message",
"columnsFrom": [
"messageId"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"Vote_chatId_messageId_pk": {
"name": "Vote_chatId_messageId_pk",
"columns": [
"chatId",
"messageId"
]
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View file

@ -43,6 +43,13 @@
"when": 1741934630596, "when": 1741934630596,
"tag": "0005_wooden_whistler", "tag": "0005_wooden_whistler",
"breakpoints": true "breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1746118166211,
"tag": "0006_marvelous_frog_thor",
"breakpoints": true
} }
] ]
} }

View file

@ -26,6 +26,7 @@ import {
vote, vote,
type DBMessage, type DBMessage,
type Chat, type Chat,
stream,
} from './schema'; } from './schema';
import type { ArtifactKind } from '@/components/artifact'; import type { ArtifactKind } from '@/components/artifact';
import { generateUUID } from '../utils'; import { generateUUID } from '../utils';
@ -100,6 +101,7 @@ export async function deleteChatById({ id }: { id: string }) {
try { try {
await db.delete(vote).where(eq(vote.chatId, id)); await db.delete(vote).where(eq(vote.chatId, id));
await db.delete(message).where(eq(message.chatId, id)); await db.delete(message).where(eq(message.chatId, id));
await db.delete(stream).where(eq(stream.chatId, id));
const [chatsDeleted] = await db const [chatsDeleted] = await db
.delete(chat) .delete(chat)
@ -470,3 +472,36 @@ export async function getMessageCountByUserId({
throw error; throw error;
} }
} }
export async function createStreamId({
streamId,
chatId,
}: {
streamId: string;
chatId: string;
}) {
try {
await db
.insert(stream)
.values({ id: streamId, chatId, createdAt: new Date() });
} catch (error) {
console.error('Failed to create stream id in database');
throw error;
}
}
export async function getStreamIdsByChatId({ chatId }: { chatId: string }) {
try {
const streamIds = await db
.select({ id: stream.id })
.from(stream)
.where(eq(stream.chatId, chatId))
.orderBy(desc(stream.createdAt))
.execute();
return streamIds.map(({ id }) => id);
} catch (error) {
console.error('Failed to get stream ids by chat id from database');
throw error;
}
}

View file

@ -150,3 +150,21 @@ export const suggestion = pgTable(
); );
export type Suggestion = InferSelectModel<typeof suggestion>; export type Suggestion = InferSelectModel<typeof suggestion>;
export const stream = pgTable(
'Stream',
{
id: uuid('id').notNull().defaultRandom(),
chatId: uuid('chatId').notNull(),
createdAt: timestamp('createdAt').notNull(),
},
(table) => ({
pk: primaryKey({ columns: [table.id] }),
chatRef: foreignKey({
columns: [table.chatId],
foreignColumns: [chat.id],
}),
}),
);
export type Stream = InferSelectModel<typeof stream>;

View file

@ -1,6 +1,6 @@
{ {
"name": "ai-chatbot", "name": "ai-chatbot",
"version": "3.0.15", "version": "3.0.16",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbo", "dev": "next dev --turbo",
@ -19,8 +19,8 @@
"test": "export PLAYWRIGHT=True && pnpm exec playwright test" "test": "export PLAYWRIGHT=True && pnpm exec playwright test"
}, },
"dependencies": { "dependencies": {
"@ai-sdk/react": "^1.2.8", "@ai-sdk/react": "^1.2.11",
"@ai-sdk/xai": "^1.2.10", "@ai-sdk/xai": "^1.2.15",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
"@codemirror/state": "^6.5.0", "@codemirror/state": "^6.5.0",
@ -40,7 +40,7 @@
"@vercel/blob": "^0.24.1", "@vercel/blob": "^0.24.1",
"@vercel/functions": "^2.0.0", "@vercel/functions": "^2.0.0",
"@vercel/postgres": "^0.10.0", "@vercel/postgres": "^0.10.0",
"ai": "4.3.4", "ai": "4.3.13",
"bcrypt-ts": "^5.0.2", "bcrypt-ts": "^5.0.2",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.0",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@ -74,7 +74,9 @@
"react-dom": "19.0.0-rc-45804af1-20241021", "react-dom": "19.0.0-rc-45804af1-20241021",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.7",
"redis": "^5.0.0",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"resumable-stream": "^2.0.0",
"server-only": "^0.0.1", "server-only": "^0.0.1",
"sonner": "^1.5.0", "sonner": "^1.5.0",
"swr": "^2.2.5", "swr": "^2.2.5",

159
pnpm-lock.yaml generated
View file

@ -9,11 +9,11 @@ importers:
.: .:
dependencies: dependencies:
'@ai-sdk/react': '@ai-sdk/react':
specifier: ^1.2.8 specifier: ^1.2.11
version: 1.2.8(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2) version: 1.2.11(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2)
'@ai-sdk/xai': '@ai-sdk/xai':
specifier: ^1.2.10 specifier: ^1.2.15
version: 1.2.10(zod@3.24.2) version: 1.2.15(zod@3.24.2)
'@codemirror/lang-javascript': '@codemirror/lang-javascript':
specifier: ^6.2.2 specifier: ^6.2.2
version: 6.2.3 version: 6.2.3
@ -72,8 +72,8 @@ importers:
specifier: ^0.10.0 specifier: ^0.10.0
version: 0.10.0 version: 0.10.0
ai: ai:
specifier: 4.3.4 specifier: 4.3.13
version: 4.3.4(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2) version: 4.3.13(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2)
bcrypt-ts: bcrypt-ts:
specifier: ^5.0.2 specifier: ^5.0.2
version: 5.0.3 version: 5.0.3
@ -173,9 +173,15 @@ importers:
react-resizable-panels: react-resizable-panels:
specifier: ^2.1.7 specifier: ^2.1.7
version: 2.1.7(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021) version: 2.1.7(react-dom@19.0.0-rc-45804af1-20241021(react@19.0.0-rc-45804af1-20241021))(react@19.0.0-rc-45804af1-20241021)
redis:
specifier: ^5.0.0
version: 5.0.0
remark-gfm: remark-gfm:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.1 version: 4.0.1
resumable-stream:
specifier: ^2.0.0
version: 2.0.0
server-only: server-only:
specifier: ^0.0.1 specifier: ^0.0.1
version: 0.0.1 version: 0.0.1
@ -258,24 +264,24 @@ importers:
packages: packages:
'@ai-sdk/openai-compatible@0.2.8': '@ai-sdk/openai-compatible@0.2.13':
resolution: {integrity: sha512-o1CrhTrXnMj72G44oBqlDDBtunw6iwONysXj7EYN/Fx27rhP7YTcWwVeKs5gMx/u/8TIho9iZ9rkqXXc2xg8Bg==} resolution: {integrity: sha512-tB+lL8Z3j0qDod/mvxwjrPhbLUHp/aQW+NvMoJaqeTtP+Vmv5qR800pncGczxn5WN0pllQm+7aIRDnm69XeSbg==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.0.0 zod: ^3.0.0
'@ai-sdk/provider-utils@2.2.6': '@ai-sdk/provider-utils@2.2.7':
resolution: {integrity: sha512-sUlZ7Gnq84DCGWMQRIK8XVbkzIBnvPR1diV4v6JwPgpn5armnLI/j+rqn62MpLrU5ZCQZlDKl/Lw6ed3ulYqaA==} resolution: {integrity: sha512-kM0xS3GWg3aMChh9zfeM+80vEZfXzR3JEUBdycZLtbRZ2TRT8xOj3WodGHPb06sUK5yD7pAXC/P7ctsi2fvUGQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.23.8 zod: ^3.23.8
'@ai-sdk/provider@1.1.2': '@ai-sdk/provider@1.1.3':
resolution: {integrity: sha512-ITdgNilJZwLKR7X5TnUr1BsQW6UTX5yFp0h66Nfx8XjBYkWD9W3yugr50GOz3CnE9m/U/Cd5OyEbTMI0rgi6ZQ==} resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@ai-sdk/react@1.2.8': '@ai-sdk/react@1.2.11':
resolution: {integrity: sha512-S2FzCSi4uTF0JuSN6zYMXyiAWVAzi/Hho8ISYgHpGZiICYLNCP2si4DuXQOsnWef3IXzQPLVoE11C63lILZIkw==} resolution: {integrity: sha512-+kPqLkJ3TWP6czaJPV+vzAKSUcKQ1598BUrcLHt56sH99+LhmIIW3ylZp0OfC3O6TR3eO1Lt0Yzw4R0mK6g9Gw==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
@ -284,14 +290,14 @@ packages:
zod: zod:
optional: true optional: true
'@ai-sdk/ui-utils@1.2.7': '@ai-sdk/ui-utils@1.2.10':
resolution: {integrity: sha512-OVRxa4SDj0wVsMZ8tGr/whT89oqNtNoXBKmqWC2BRv5ZG6azL2LYZ5ZK35u3lb4l1IE7cWGsLlmq0py0ttsL7A==} resolution: {integrity: sha512-GUj+LBoAlRQF1dL/M49jtufGqtLOMApxTpCmVjoRpIPt/dFALVL9RfqfvxwztyIwbK+IxGzcYjSGRsrWrj+86g==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.23.8 zod: ^3.23.8
'@ai-sdk/xai@1.2.10': '@ai-sdk/xai@1.2.15':
resolution: {integrity: sha512-jPaOq7HHJ9A7FF3i/zngcUd6jczFMJF1x9Ayle4IxCZ2gIhtLgv0hiUb1X95keh/gCHjtelBQdzLFNNuiPBadQ==} resolution: {integrity: sha512-18qEYyVHIqTiOMePE00bfx4kJrTHM4dV3D3Rpe+eBISlY80X1FnzZRnRTJo3Q6MOSmW5+ZKVaX9jtryhoFpn0A==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
zod: ^3.0.0 zod: ^3.0.0
@ -1480,6 +1486,34 @@ packages:
'@radix-ui/rect@1.1.0': '@radix-ui/rect@1.1.0':
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
'@redis/bloom@5.0.0':
resolution: {integrity: sha512-YHlra6a7+brJ3Ustqa+jC68Pdnw2/wbnVLeULN0llP3gHpKxOSmiDyLwuHq5t/jFi6QUgjSprUQi2a8CieKFIA==}
engines: {node: '>= 18'}
peerDependencies:
'@redis/client': ^5.0.0
'@redis/client@5.0.0':
resolution: {integrity: sha512-1MqfzOOoFBwWprigJ3S5hM+LuzaWwxyyx+74NDzCpmjKWyjjC07xkWsd5E9fr+61NUDtvUivRiyAxGiRIwJCaQ==}
engines: {node: '>= 18'}
'@redis/json@5.0.0':
resolution: {integrity: sha512-8YFjshsvWR9dWjOiQ50TfBxfl+z1mjCae8jDnfOYN1aCvUZWJXqM1OpL56v6mWRyXwZ5C9qJJKDGmHTr4PVskQ==}
engines: {node: '>= 18'}
peerDependencies:
'@redis/client': ^5.0.0
'@redis/search@5.0.0':
resolution: {integrity: sha512-oEop/S/0NB5p7vpTTlp5X5LQO4oTomJnFEOaKhQ5xZWROLXTgEVvPSfXVNBbtr9maU2+OupxXQR63HW3MGeTUg==}
engines: {node: '>= 18'}
peerDependencies:
'@redis/client': ^5.0.0
'@redis/time-series@5.0.0':
resolution: {integrity: sha512-DNsP4DH5CRfXlQDyvLJm2DltZm3kjpVKZsNTI3SPdbRxhc9i2c0NehUbM/gDECnEfG+jkNN/LUv6Em4LaLwPBQ==}
engines: {node: '>= 18'}
peerDependencies:
'@redis/client': ^5.0.0
'@rtsao/scc@1.1.0': '@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@ -1655,8 +1689,8 @@ packages:
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
hasBin: true hasBin: true
ai@4.3.4: ai@4.3.13:
resolution: {integrity: sha512-uMjzrowIqfU8CCCxhx8QGl7ETydHBROeNL0VoEwetkmDCY6Q8ZTacj6jNNqGJOiCk595aUrGR9VHPY9Ylvy1fg==} resolution: {integrity: sha512-cC5HXItuOwGykSMacCPzNp6+NMTxeuTjOenztVgSJhdC9Z4OrzBxwkyeDAf4h1QP938ZFi7IBdq3u4lxVoVmvw==}
engines: {node: '>=18'} engines: {node: '>=18'}
peerDependencies: peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc react: ^18 || ^19 || ^19.0.0-rc
@ -1865,6 +1899,10 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
codemirror@6.0.1: codemirror@6.0.1:
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
@ -3375,6 +3413,10 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'} engines: {node: '>=8.10.0'}
redis@5.0.0:
resolution: {integrity: sha512-J/fzf0cYeFw5NP4aYIvv9owYOcNUsaDqF4qmwbSuaV4yKBLaIHJQIFbAKLgjn99GDXdJBbfqCRXE7+BIlkpATA==}
engines: {node: '>= 18'}
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -3411,6 +3453,9 @@ packages:
resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
hasBin: true hasBin: true
resumable-stream@2.0.0:
resolution: {integrity: sha512-D7E0wDUnfoy+Lerba/gyuD44OG3G0APqDcQ9soMSerujaVujPLWc5sSCLXf/ZFQPreLb3MKDjSm3TOPXpNtpZw==}
retry@0.13.1: retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@ -3870,45 +3915,45 @@ packages:
snapshots: snapshots:
'@ai-sdk/openai-compatible@0.2.8(zod@3.24.2)': '@ai-sdk/openai-compatible@0.2.13(zod@3.24.2)':
dependencies: dependencies:
'@ai-sdk/provider': 1.1.2 '@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.6(zod@3.24.2) '@ai-sdk/provider-utils': 2.2.7(zod@3.24.2)
zod: 3.24.2 zod: 3.24.2
'@ai-sdk/provider-utils@2.2.6(zod@3.24.2)': '@ai-sdk/provider-utils@2.2.7(zod@3.24.2)':
dependencies: dependencies:
'@ai-sdk/provider': 1.1.2 '@ai-sdk/provider': 1.1.3
nanoid: 3.3.11 nanoid: 3.3.11
secure-json-parse: 2.7.0 secure-json-parse: 2.7.0
zod: 3.24.2 zod: 3.24.2
'@ai-sdk/provider@1.1.2': '@ai-sdk/provider@1.1.3':
dependencies: dependencies:
json-schema: 0.4.0 json-schema: 0.4.0
'@ai-sdk/react@1.2.8(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2)': '@ai-sdk/react@1.2.11(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2)':
dependencies: dependencies:
'@ai-sdk/provider-utils': 2.2.6(zod@3.24.2) '@ai-sdk/provider-utils': 2.2.7(zod@3.24.2)
'@ai-sdk/ui-utils': 1.2.7(zod@3.24.2) '@ai-sdk/ui-utils': 1.2.10(zod@3.24.2)
react: 19.0.0-rc-45804af1-20241021 react: 19.0.0-rc-45804af1-20241021
swr: 2.3.3(react@19.0.0-rc-45804af1-20241021) swr: 2.3.3(react@19.0.0-rc-45804af1-20241021)
throttleit: 2.1.0 throttleit: 2.1.0
optionalDependencies: optionalDependencies:
zod: 3.24.2 zod: 3.24.2
'@ai-sdk/ui-utils@1.2.7(zod@3.24.2)': '@ai-sdk/ui-utils@1.2.10(zod@3.24.2)':
dependencies: dependencies:
'@ai-sdk/provider': 1.1.2 '@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.6(zod@3.24.2) '@ai-sdk/provider-utils': 2.2.7(zod@3.24.2)
zod: 3.24.2 zod: 3.24.2
zod-to-json-schema: 3.24.5(zod@3.24.2) zod-to-json-schema: 3.24.5(zod@3.24.2)
'@ai-sdk/xai@1.2.10(zod@3.24.2)': '@ai-sdk/xai@1.2.15(zod@3.24.2)':
dependencies: dependencies:
'@ai-sdk/openai-compatible': 0.2.8(zod@3.24.2) '@ai-sdk/openai-compatible': 0.2.13(zod@3.24.2)
'@ai-sdk/provider': 1.1.2 '@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.6(zod@3.24.2) '@ai-sdk/provider-utils': 2.2.7(zod@3.24.2)
zod: 3.24.2 zod: 3.24.2
'@alloc/quick-lru@5.2.0': {} '@alloc/quick-lru@5.2.0': {}
@ -4849,6 +4894,26 @@ snapshots:
'@radix-ui/rect@1.1.0': {} '@radix-ui/rect@1.1.0': {}
'@redis/bloom@5.0.0(@redis/client@5.0.0)':
dependencies:
'@redis/client': 5.0.0
'@redis/client@5.0.0':
dependencies:
cluster-key-slot: 1.1.2
'@redis/json@5.0.0(@redis/client@5.0.0)':
dependencies:
'@redis/client': 5.0.0
'@redis/search@5.0.0(@redis/client@5.0.0)':
dependencies:
'@redis/client': 5.0.0
'@redis/time-series@5.0.0(@redis/client@5.0.0)':
dependencies:
'@redis/client': 5.0.0
'@rtsao/scc@1.1.0': {} '@rtsao/scc@1.1.0': {}
'@rushstack/eslint-patch@1.11.0': {} '@rushstack/eslint-patch@1.11.0': {}
@ -5009,12 +5074,12 @@ snapshots:
acorn@8.14.1: {} acorn@8.14.1: {}
ai@4.3.4(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2): ai@4.3.13(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2):
dependencies: dependencies:
'@ai-sdk/provider': 1.1.2 '@ai-sdk/provider': 1.1.3
'@ai-sdk/provider-utils': 2.2.6(zod@3.24.2) '@ai-sdk/provider-utils': 2.2.7(zod@3.24.2)
'@ai-sdk/react': 1.2.8(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2) '@ai-sdk/react': 1.2.11(react@19.0.0-rc-45804af1-20241021)(zod@3.24.2)
'@ai-sdk/ui-utils': 1.2.7(zod@3.24.2) '@ai-sdk/ui-utils': 1.2.10(zod@3.24.2)
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
jsondiffpatch: 0.6.0 jsondiffpatch: 0.6.0
zod: 3.24.2 zod: 3.24.2
@ -5233,6 +5298,8 @@ snapshots:
clsx@2.1.1: {} clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
codemirror@6.0.1: codemirror@6.0.1:
dependencies: dependencies:
'@codemirror/autocomplete': 6.18.6 '@codemirror/autocomplete': 6.18.6
@ -7125,6 +7192,14 @@ snapshots:
dependencies: dependencies:
picomatch: 2.3.1 picomatch: 2.3.1
redis@5.0.0:
dependencies:
'@redis/bloom': 5.0.0(@redis/client@5.0.0)
'@redis/client': 5.0.0
'@redis/json': 5.0.0(@redis/client@5.0.0)
'@redis/search': 5.0.0(@redis/client@5.0.0)
'@redis/time-series': 5.0.0(@redis/client@5.0.0)
reflect.getprototypeof@1.0.10: reflect.getprototypeof@1.0.10:
dependencies: dependencies:
call-bind: 1.0.8 call-bind: 1.0.8
@ -7195,6 +7270,8 @@ snapshots:
path-parse: 1.0.7 path-parse: 1.0.7
supports-preserve-symlinks-flag: 1.0.0 supports-preserve-symlinks-flag: 1.0.0
resumable-stream@2.0.0: {}
retry@0.13.1: {} retry@0.13.1: {}
reusify@1.1.0: {} reusify@1.1.0: {}

View file

@ -80,4 +80,158 @@ test.describe
const deletedChat = await response.json(); const deletedChat = await response.json();
expect(deletedChat).toMatchObject({ id: chatId }); expect(deletedChat).toMatchObject({ id: chatId });
}); });
test('Ada cannot resume stream of chat that does not exist', async ({
adaContext,
}) => {
const response = await adaContext.request.get(
`/api/chat?chatId=${generateUUID()}`,
);
expect(response.status()).toBe(404);
});
test('Ada can resume chat generation', async ({ adaContext }) => {
const chatId = generateUUID();
const firstRequest = adaContext.request.post('/api/chat', {
data: {
id: chatId,
message: {
id: generateUUID(),
role: 'user',
content: 'Help me write an essay about Silcon Valley',
parts: [
{
type: 'text',
text: 'Help me write an essay about Silicon Valley',
},
],
createdAt: new Date().toISOString(),
},
selectedChatModel: 'chat-model',
},
});
await new Promise((resolve) => setTimeout(resolve, 1000));
const secondRequest = adaContext.request.get(
`/api/chat?chatId=${chatId}`,
);
const [firstResponse, secondResponse] = await Promise.all([
firstRequest,
secondRequest,
]);
const [firstStatusCode, secondStatusCode] = await Promise.all([
firstResponse.status(),
secondResponse.status(),
]);
expect(firstStatusCode).toBe(200);
expect(secondStatusCode).toBe(200);
const [firstResponseBody, secondResponseBody] = await Promise.all([
await firstResponse.body(),
await secondResponse.body(),
]);
expect(firstResponseBody.toString()).toEqual(
secondResponseBody.toString(),
);
});
test('Ada cannot resume chat generation that has ended', async ({
adaContext,
}) => {
const chatId = generateUUID();
const firstRequest = await adaContext.request.post('/api/chat', {
data: {
id: chatId,
message: {
id: generateUUID(),
role: 'user',
content: 'Help me write an essay about Silcon Valley',
parts: [
{
type: 'text',
text: 'Help me write an essay about Silicon Valley',
},
],
createdAt: new Date().toISOString(),
},
selectedChatModel: 'chat-model',
},
});
const secondRequest = adaContext.request.get(
`/api/chat?chatId=${chatId}`,
);
const [firstResponse, secondResponse] = await Promise.all([
firstRequest,
secondRequest,
]);
const [firstStatusCode, secondStatusCode] = await Promise.all([
firstResponse.status(),
secondResponse.status(),
]);
expect(firstStatusCode).toBe(200);
expect(secondStatusCode).toBe(200);
const [, secondResponseContent] = await Promise.all([
firstResponse.text(),
secondResponse.text(),
]);
expect(secondResponseContent).toEqual('');
});
test('Babbage cannot resume chat generation that belongs to Ada', async ({
adaContext,
babbageContext,
}) => {
const chatId = generateUUID();
const firstRequest = adaContext.request.post('/api/chat', {
data: {
id: chatId,
message: {
id: generateUUID(),
role: 'user',
content: 'Help me write an essay about Silcon Valley',
parts: [
{
type: 'text',
text: 'Help me write an essay about Silicon Valley',
},
],
createdAt: new Date().toISOString(),
},
selectedChatModel: 'chat-model',
},
});
await new Promise((resolve) => setTimeout(resolve, 1000));
const secondRequest = babbageContext.request.get(
`/api/chat?chatId=${chatId}`,
);
const [firstResponse, secondResponse] = await Promise.all([
firstRequest,
secondRequest,
]);
const [firstStatusCode, secondStatusCode] = await Promise.all([
firstResponse.status(),
secondResponse.status(),
]);
expect(firstStatusCode).toBe(200);
expect(secondStatusCode).toBe(403);
});
}); });