import * as plugins from '../plugins.js'; import type { TransactionEngine } from './TransactionEngine.js'; /** * Session state */ export interface ISession { /** Session ID (UUID) */ id: string; /** Timestamp when the session was created */ createdAt: number; /** Timestamp of the last activity */ lastActivityAt: number; /** Current transaction ID if any */ txnId?: string; /** Transaction number for ordering */ txnNumber?: number; /** Whether the session is in a transaction */ inTransaction: boolean; /** Session metadata */ metadata?: Record; } /** * Session engine options */ export interface ISessionEngineOptions { /** Session timeout in milliseconds (default: 30 minutes) */ sessionTimeoutMs?: number; /** Interval to check for expired sessions in ms (default: 60 seconds) */ cleanupIntervalMs?: number; } /** * Session engine for managing client sessions * - Tracks session lifecycle (create, touch, end) * - Links sessions to transactions * - Auto-aborts transactions on session expiry */ export class SessionEngine { private sessions: Map = new Map(); private sessionTimeoutMs: number; private cleanupInterval?: ReturnType; private transactionEngine?: TransactionEngine; constructor(options?: ISessionEngineOptions) { this.sessionTimeoutMs = options?.sessionTimeoutMs ?? 30 * 60 * 1000; // 30 minutes default const cleanupIntervalMs = options?.cleanupIntervalMs ?? 60 * 1000; // 1 minute default // Start cleanup interval this.cleanupInterval = setInterval(() => { this.cleanupExpiredSessions(); }, cleanupIntervalMs); } /** * Set the transaction engine to use for auto-abort */ setTransactionEngine(engine: TransactionEngine): void { this.transactionEngine = engine; } /** * Start a new session */ startSession(sessionId?: string, metadata?: Record): ISession { const id = sessionId ?? new plugins.bson.UUID().toHexString(); const now = Date.now(); const session: ISession = { id, createdAt: now, lastActivityAt: now, inTransaction: false, metadata, }; this.sessions.set(id, session); return session; } /** * Get a session by ID */ getSession(sessionId: string): ISession | undefined { const session = this.sessions.get(sessionId); if (session && this.isSessionExpired(session)) { // Session expired, clean it up this.endSession(sessionId); return undefined; } return session; } /** * Touch a session to update last activity time */ touchSession(sessionId: string): boolean { const session = this.sessions.get(sessionId); if (!session) return false; if (this.isSessionExpired(session)) { this.endSession(sessionId); return false; } session.lastActivityAt = Date.now(); return true; } /** * End a session explicitly * This will also abort any active transaction */ async endSession(sessionId: string): Promise { const session = this.sessions.get(sessionId); if (!session) return false; // If session has an active transaction, abort it if (session.inTransaction && session.txnId && this.transactionEngine) { try { await this.transactionEngine.abortTransaction(session.txnId); } catch (e) { // Ignore abort errors during cleanup } } this.sessions.delete(sessionId); return true; } /** * Start a transaction in a session */ startTransaction(sessionId: string, txnId: string, txnNumber?: number): boolean { const session = this.sessions.get(sessionId); if (!session) return false; if (this.isSessionExpired(session)) { this.endSession(sessionId); return false; } session.txnId = txnId; session.txnNumber = txnNumber; session.inTransaction = true; session.lastActivityAt = Date.now(); return true; } /** * End a transaction in a session (commit or abort) */ endTransaction(sessionId: string): boolean { const session = this.sessions.get(sessionId); if (!session) return false; session.txnId = undefined; session.txnNumber = undefined; session.inTransaction = false; session.lastActivityAt = Date.now(); return true; } /** * Get transaction ID for a session */ getTransactionId(sessionId: string): string | undefined { const session = this.sessions.get(sessionId); return session?.txnId; } /** * Check if session is in a transaction */ isInTransaction(sessionId: string): boolean { const session = this.sessions.get(sessionId); return session?.inTransaction ?? false; } /** * Check if a session is expired */ isSessionExpired(session: ISession): boolean { return Date.now() - session.lastActivityAt > this.sessionTimeoutMs; } /** * Cleanup expired sessions * This is called periodically by the cleanup interval */ private async cleanupExpiredSessions(): Promise { const expiredSessions: string[] = []; for (const [id, session] of this.sessions) { if (this.isSessionExpired(session)) { expiredSessions.push(id); } } // End all expired sessions (this will also abort their transactions) for (const sessionId of expiredSessions) { await this.endSession(sessionId); } } /** * Get all active sessions */ listSessions(): ISession[] { const activeSessions: ISession[] = []; for (const session of this.sessions.values()) { if (!this.isSessionExpired(session)) { activeSessions.push(session); } } return activeSessions; } /** * Get session count */ getSessionCount(): number { return this.sessions.size; } /** * Get sessions with active transactions */ getSessionsWithTransactions(): ISession[] { return this.listSessions().filter(s => s.inTransaction); } /** * Refresh session timeout */ refreshSession(sessionId: string): boolean { return this.touchSession(sessionId); } /** * Close the session engine and cleanup */ close(): void { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = undefined; } // Clear all sessions this.sessions.clear(); } /** * Get or create a session for a given session ID * Useful for handling MongoDB driver session requests */ getOrCreateSession(sessionId: string): ISession { let session = this.getSession(sessionId); if (!session) { session = this.startSession(sessionId); } else { this.touchSession(sessionId); } return session; } /** * Extract session ID from MongoDB lsid (logical session ID) */ static extractSessionId(lsid: any): string | undefined { if (!lsid) return undefined; // MongoDB session ID format: { id: UUID } if (lsid.id) { if (lsid.id instanceof plugins.bson.UUID) { return lsid.id.toHexString(); } if (typeof lsid.id === 'string') { return lsid.id; } if (lsid.id.$binary?.base64) { // Binary UUID format return Buffer.from(lsid.id.$binary.base64, 'base64').toString('hex'); } } return undefined; } }