Files
Maestro/src/main/stats/session-lifecycle.ts
Raza Rauf dbcbda1324 refactor: decompose stats-db into focused modules
Break the 1,870-line monolithic stats-db.ts into 13 focused modules under
src/main/stats/, following the same pattern used in the agents module refactor.

Key improvements:
- StatementCache for prepared statement reuse across CRUD operations
- DB guard accessor (get database()) replaces 18 repeated null checks
- Transaction-wrapped clearOldData for atomic multi-table cleanup
- _meta table replaces external file for vacuum timestamp storage
- Row mapper functions centralize snake_case-to-camelCase conversion
- runStatements() helper eliminates repeated split-by-semicolon patterns
- Migration functions accept db param instead of relying on class context
- CSV export adds isRemote column and RFC 4180 compliant escaping
- Statement caches properly cleared on database close

Split the 6,400-line monolithic test file into 8 focused test files under
src/__tests__/main/stats/ matching the source module structure.
2026-01-30 20:10:56 +05:00

106 lines
2.7 KiB
TypeScript

/**
* Session Lifecycle CRUD Operations
*
* Tracks when sessions are created (launched) and closed,
* enabling session duration and lifecycle analytics.
*/
import type Database from 'better-sqlite3';
import type { SessionLifecycleEvent, StatsTimeRange } from '../../shared/stats-types';
import { generateId, getTimeRangeStart, normalizePath, LOG_CONTEXT } from './utils';
import { mapSessionLifecycleRow, type SessionLifecycleRow } from './row-mappers';
import { StatementCache } from './utils';
import { logger } from '../utils/logger';
const stmtCache = new StatementCache();
const INSERT_SQL = `
INSERT INTO session_lifecycle (id, session_id, agent_type, project_path, created_at, is_remote)
VALUES (?, ?, ?, ?, ?, ?)
`;
/**
* Record a session being created (launched)
*/
export function recordSessionCreated(
db: Database.Database,
event: Omit<SessionLifecycleEvent, 'id' | 'closedAt' | 'duration'>
): string {
const id = generateId();
const stmt = stmtCache.get(db, INSERT_SQL);
stmt.run(
id,
event.sessionId,
event.agentType,
normalizePath(event.projectPath),
event.createdAt,
event.isRemote !== undefined ? (event.isRemote ? 1 : 0) : null
);
logger.debug(`Recorded session created: ${event.sessionId}`, LOG_CONTEXT);
return id;
}
/**
* Record a session being closed
*/
export function recordSessionClosed(
db: Database.Database,
sessionId: string,
closedAt: number
): boolean {
// Get the session's created_at time to calculate duration
const session = db
.prepare('SELECT created_at FROM session_lifecycle WHERE session_id = ?')
.get(sessionId) as { created_at: number } | undefined;
if (!session) {
logger.debug(`Session not found for closure: ${sessionId}`, LOG_CONTEXT);
return false;
}
const duration = closedAt - session.created_at;
const stmt = stmtCache.get(
db,
`
UPDATE session_lifecycle
SET closed_at = ?, duration = ?
WHERE session_id = ?
`
);
const result = stmt.run(closedAt, duration, sessionId);
logger.debug(`Recorded session closed: ${sessionId}, duration: ${duration}ms`, LOG_CONTEXT);
return result.changes > 0;
}
/**
* Get session lifecycle events within a time range
*/
export function getSessionLifecycleEvents(
db: Database.Database,
range: StatsTimeRange
): SessionLifecycleEvent[] {
const startTime = getTimeRangeStart(range);
const stmt = stmtCache.get(
db,
`
SELECT * FROM session_lifecycle
WHERE created_at >= ?
ORDER BY created_at DESC
`
);
const rows = stmt.all(startTime) as SessionLifecycleRow[];
return rows.map(mapSessionLifecycleRow);
}
/**
* Clear the statement cache (call when database is closed)
*/
export function clearSessionLifecycleCache(): void {
stmtCache.clear();
}