feat: add SessionManager for multi-frontend session handling

This commit is contained in:
William Valentin
2026-02-05 00:34:25 -08:00
parent 55c2e6bf74
commit 2f1c302d85
3 changed files with 147 additions and 0 deletions
+1
View File
@@ -1 +1,2 @@
export { SessionStore } from './store.js';
export { SessionManager, ManagedSession, type Session } from './manager.js';
+61
View File
@@ -0,0 +1,61 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { SessionManager } from './manager.js';
import { SessionStore } from './store.js';
import { unlinkSync, existsSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
describe('SessionManager', () => {
const dbPath = join(tmpdir(), 'flynn-test-manager.db');
let store: SessionStore;
let manager: SessionManager;
beforeEach(() => {
store = new SessionStore(dbPath);
manager = new SessionManager(store);
});
afterEach(() => {
store.close();
if (existsSync(dbPath)) {
unlinkSync(dbPath);
}
});
it('creates sessions for different frontends', () => {
const telegramSession = manager.getSession('telegram', 'user-123');
const tuiSession = manager.getSession('tui', 'local');
expect(telegramSession.id).toBe('telegram:user-123');
expect(tuiSession.id).toBe('tui:local');
});
it('returns same session for same frontend and user', () => {
const session1 = manager.getSession('telegram', 'user-123');
const session2 = manager.getSession('telegram', 'user-123');
expect(session1).toBe(session2);
});
it('transfers session history between frontends', () => {
const telegramSession = manager.getSession('telegram', 'user-123');
telegramSession.addMessage({ role: 'user', content: 'Hello from Telegram' });
telegramSession.addMessage({ role: 'assistant', content: 'Hi!' });
const tuiSession = manager.getSession('tui', 'local');
manager.transferSession('telegram', 'user-123', 'tui', 'local');
const tuiMessages = tuiSession.getHistory();
expect(tuiMessages).toHaveLength(2);
expect(tuiMessages[0].content).toBe('Hello from Telegram');
});
it('lists active sessions', () => {
manager.getSession('telegram', 'user-123');
manager.getSession('tui', 'local');
const sessions = manager.listSessions();
expect(sessions).toContain('telegram:user-123');
expect(sessions).toContain('tui:local');
});
});
+85
View File
@@ -0,0 +1,85 @@
import type { Message } from '../models/types.js';
import type { SessionStore } from './store.js';
export interface Session {
id: string;
addMessage(message: Message): void;
getHistory(): Message[];
clear(): void;
}
export class ManagedSession implements Session {
constructor(
public readonly id: string,
private store: SessionStore,
private history: Message[] = []
) {}
addMessage(message: Message): void {
this.history.push(message);
this.store.addMessage(this.id, message);
}
getHistory(): Message[] {
return [...this.history];
}
clear(): void {
this.history = [];
this.store.clearSession(this.id);
}
setHistory(messages: Message[]): void {
this.history = [...messages];
}
}
export class SessionManager {
private sessions: Map<string, ManagedSession> = new Map();
constructor(private store: SessionStore) {}
private makeSessionId(frontend: string, userId: string): string {
return `${frontend}:${userId}`;
}
getSession(frontend: string, userId: string): ManagedSession {
const id = this.makeSessionId(frontend, userId);
let session = this.sessions.get(id);
if (!session) {
const history = this.store.getMessages(id);
session = new ManagedSession(id, this.store, history);
this.sessions.set(id, session);
}
return session;
}
transferSession(
fromFrontend: string,
fromUserId: string,
toFrontend: string,
toUserId: string
): void {
const fromSession = this.getSession(fromFrontend, fromUserId);
const toSession = this.getSession(toFrontend, toUserId);
const history = fromSession.getHistory();
// Clear target and copy history
toSession.clear();
for (const message of history) {
toSession.addMessage(message);
}
}
listSessions(): string[] {
return Array.from(this.sessions.keys());
}
closeSession(frontend: string, userId: string): void {
const id = this.makeSessionId(frontend, userId);
this.sessions.delete(id);
}
}