feat(FN-1975): stream chat session updates through SSE
- Emit chat:session:updated from ChatStore when message deletion mutates a session and cover deleteMessage false returns - Pass ChatStore into SSE setup and forward chat session update events to connected clients - Update useChat to consume SSE updates in real time with safer EnrichedChatSession typing - Add core and dashboard tests for chat-store emissions, SSE forwarding, route wiring, and hook behavior
This commit is contained in:
@@ -555,6 +555,31 @@ describe("ChatStore", () => {
|
||||
expect(store.getMessages(session2.id)).toHaveLength(1);
|
||||
expect(store.getMessages(session2.id)[0].content).toBe("Session 2");
|
||||
});
|
||||
|
||||
it("updates the parent session's updatedAt timestamp", async () => {
|
||||
const session = createTestSession(store);
|
||||
store.addMessage(session.id, { role: "user", content: "Hello" });
|
||||
const originalUpdatedAt = store.getSession(session.id)!.updatedAt;
|
||||
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
|
||||
const msg = store.addMessage(session.id, { role: "assistant", content: "Reply" });
|
||||
const afterAddUpdatedAt = store.getSession(session.id)!.updatedAt;
|
||||
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
|
||||
store.deleteMessage(msg.id);
|
||||
|
||||
const afterDeleteUpdatedAt = store.getSession(session.id)!.updatedAt;
|
||||
|
||||
// The updatedAt should be newer after adding and after deleting
|
||||
expect(new Date(afterAddUpdatedAt).getTime()).toBeGreaterThan(
|
||||
new Date(originalUpdatedAt).getTime(),
|
||||
);
|
||||
expect(new Date(afterDeleteUpdatedAt).getTime()).toBeGreaterThan(
|
||||
new Date(afterAddUpdatedAt).getTime(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -627,6 +652,20 @@ describe("ChatStore", () => {
|
||||
expect(handler).toHaveBeenCalledWith(message.id);
|
||||
});
|
||||
|
||||
it("deleteMessage emits chat:session:updated for the parent session", () => {
|
||||
const handler = vi.fn();
|
||||
store.on("chat:session:updated", handler);
|
||||
|
||||
const session = createTestSession(store);
|
||||
const message = store.addMessage(session.id, { role: "user", content: "Hello" });
|
||||
handler.mockClear();
|
||||
|
||||
store.deleteMessage(message.id);
|
||||
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
expect(handler.mock.calls[0][0].id).toBe(session.id);
|
||||
});
|
||||
|
||||
it("deleteMessage does NOT emit for non-existent message", () => {
|
||||
const handler = vi.fn();
|
||||
store.on("chat:message:deleted", handler);
|
||||
@@ -636,6 +675,15 @@ describe("ChatStore", () => {
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("deleteMessage does NOT emit chat:session:updated for non-existent message", () => {
|
||||
const handler = vi.fn();
|
||||
store.on("chat:session:updated", handler);
|
||||
|
||||
store.deleteMessage("msg-nonexistent");
|
||||
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("archiveSession emits chat:session:updated", () => {
|
||||
const handler = vi.fn();
|
||||
store.on("chat:session:updated", handler);
|
||||
|
||||
@@ -387,9 +387,23 @@ export class ChatStore extends EventEmitter<ChatStoreEvents> {
|
||||
const existing = this.getMessage(id);
|
||||
if (!existing) return false;
|
||||
|
||||
const sessionId = existing.sessionId;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
this.db.prepare("DELETE FROM chat_messages WHERE id = ?").run(id);
|
||||
|
||||
// Update the parent session's updatedAt timestamp
|
||||
this.db.prepare("UPDATE chat_sessions SET updatedAt = ? WHERE id = ?").run(now, sessionId);
|
||||
|
||||
this.db.bumpLastModified();
|
||||
this.emit("chat:message:deleted", id);
|
||||
|
||||
// Emit session:updated for the parent session
|
||||
const updatedSession = this.getSession(sessionId);
|
||||
if (updatedSession) {
|
||||
this.emit("chat:session:updated", updatedSession);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,18 @@ export interface ChatSession {
|
||||
*/
|
||||
export type ChatSessionSummary = ChatSession;
|
||||
|
||||
/**
|
||||
* Chat session enriched with last message preview data.
|
||||
* The server enriches sessions with lastMessagePreview and lastMessageAt
|
||||
* by fetching the most recent message for each session.
|
||||
*/
|
||||
export type EnrichedChatSession = ChatSession & {
|
||||
/** Preview of the last message in the session (truncated to 100 chars) */
|
||||
lastMessagePreview?: string;
|
||||
/** Timestamp of the last message in the session */
|
||||
lastMessageAt?: string;
|
||||
};
|
||||
|
||||
/** A parsed @ mention of an agent in a chat message */
|
||||
export interface ChatMention {
|
||||
agentId: string;
|
||||
|
||||
@@ -588,6 +588,7 @@ export type {
|
||||
ChatMessageRole,
|
||||
ChatSession,
|
||||
ChatSessionSummary,
|
||||
EnrichedChatSession,
|
||||
ChatMention,
|
||||
ChatMessage,
|
||||
ChatMessageCreateInput,
|
||||
|
||||
Reference in New Issue
Block a user