feat: add SQLite session persistence
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { SessionStore } from './store.js';
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { SessionStore } from './store.js';
|
||||
import { unlinkSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
describe('SessionStore', () => {
|
||||
const dbPath = join(tmpdir(), 'flynn-test-sessions.db');
|
||||
let store: SessionStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new SessionStore(dbPath);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
store.close();
|
||||
if (existsSync(dbPath)) {
|
||||
unlinkSync(dbPath);
|
||||
}
|
||||
});
|
||||
|
||||
it('saves and retrieves messages', () => {
|
||||
const sessionId = 'test-session';
|
||||
|
||||
store.addMessage(sessionId, { role: 'user', content: 'Hello' });
|
||||
store.addMessage(sessionId, { role: 'assistant', content: 'Hi there!' });
|
||||
|
||||
const messages = store.getMessages(sessionId);
|
||||
|
||||
expect(messages).toHaveLength(2);
|
||||
expect(messages[0].role).toBe('user');
|
||||
expect(messages[0].content).toBe('Hello');
|
||||
expect(messages[1].role).toBe('assistant');
|
||||
expect(messages[1].content).toBe('Hi there!');
|
||||
});
|
||||
|
||||
it('clears session messages', () => {
|
||||
const sessionId = 'test-session';
|
||||
|
||||
store.addMessage(sessionId, { role: 'user', content: 'Hello' });
|
||||
store.clearSession(sessionId);
|
||||
|
||||
const messages = store.getMessages(sessionId);
|
||||
expect(messages).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('handles multiple sessions independently', () => {
|
||||
store.addMessage('session-1', { role: 'user', content: 'Session 1' });
|
||||
store.addMessage('session-2', { role: 'user', content: 'Session 2' });
|
||||
|
||||
expect(store.getMessages('session-1')).toHaveLength(1);
|
||||
expect(store.getMessages('session-2')).toHaveLength(1);
|
||||
expect(store.getMessages('session-1')[0].content).toBe('Session 1');
|
||||
});
|
||||
|
||||
it('lists all sessions', () => {
|
||||
store.addMessage('session-a', { role: 'user', content: 'A' });
|
||||
store.addMessage('session-b', { role: 'user', content: 'B' });
|
||||
|
||||
const sessions = store.listSessions();
|
||||
|
||||
expect(sessions).toContain('session-a');
|
||||
expect(sessions).toContain('session-b');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import Database from 'better-sqlite3';
|
||||
import type { Message } from '../models/types.js';
|
||||
|
||||
export class SessionStore {
|
||||
private db: Database.Database;
|
||||
|
||||
constructor(dbPath: string) {
|
||||
this.db = new Database(dbPath);
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init(): void {
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
||||
`);
|
||||
}
|
||||
|
||||
addMessage(sessionId: string, message: Message): void {
|
||||
const stmt = this.db.prepare(
|
||||
'INSERT INTO messages (session_id, role, content) VALUES (?, ?, ?)'
|
||||
);
|
||||
stmt.run(sessionId, message.role, message.content);
|
||||
}
|
||||
|
||||
getMessages(sessionId: string): Message[] {
|
||||
const stmt = this.db.prepare(
|
||||
'SELECT role, content FROM messages WHERE session_id = ? ORDER BY id ASC'
|
||||
);
|
||||
const rows = stmt.all(sessionId) as Array<{ role: string; content: string }>;
|
||||
return rows.map(row => ({
|
||||
role: row.role as 'user' | 'assistant',
|
||||
content: row.content,
|
||||
}));
|
||||
}
|
||||
|
||||
clearSession(sessionId: string): void {
|
||||
const stmt = this.db.prepare('DELETE FROM messages WHERE session_id = ?');
|
||||
stmt.run(sessionId);
|
||||
}
|
||||
|
||||
listSessions(): string[] {
|
||||
const stmt = this.db.prepare('SELECT DISTINCT session_id FROM messages');
|
||||
const rows = stmt.all() as Array<{ session_id: string }>;
|
||||
return rows.map(row => row.session_id);
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.db.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user