diff --git a/server/src/services/__tests__/logService.test.ts b/server/src/services/__tests__/logService.test.ts new file mode 100644 index 0000000..5d1c2d5 --- /dev/null +++ b/server/src/services/__tests__/logService.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createLogService } from '../logService.js'; + +describe('LogService', () => { + let logService: ReturnType; + + beforeEach(() => { + logService = createLogService(); + }); + + it('logs entries and returns them via getRecent', () => { + logService.log('info', 'Game', 'Server started'); + const entries = logService.getRecent(); + expect(entries).toHaveLength(1); + expect(entries[0].severity).toBe('info'); + expect(entries[0].category).toBe('Game'); + expect(entries[0].message).toBe('Server started'); + expect(entries[0].timestamp).toBeGreaterThan(0); + }); + + it('enforces ring buffer limit of 200', () => { + for (let i = 0; i < 210; i++) { + logService.log('info', 'Game', `Entry ${i}`); + } + const entries = logService.getRecent(); + expect(entries).toHaveLength(200); + expect(entries[0].message).toBe('Entry 10'); + }); + + it('notifies subscribers on new entry', () => { + const received: any[] = []; + const unsub = logService.subscribe((entry) => received.push(entry)); + logService.log('error', 'LLM', 'Timeout'); + expect(received).toHaveLength(1); + expect(received[0].severity).toBe('error'); + unsub(); + logService.log('info', 'Game', 'Ignored'); + expect(received).toHaveLength(1); + }); +}); diff --git a/server/src/services/logService.ts b/server/src/services/logService.ts new file mode 100644 index 0000000..3050d60 --- /dev/null +++ b/server/src/services/logService.ts @@ -0,0 +1,37 @@ +import type { LogEntry, LogSeverity, LogCategory } from '@dflike/shared'; + +const MAX_ENTRIES = 200; + +export function createLogService() { + const entries: LogEntry[] = []; + const subscribers = new Set<(entry: LogEntry) => void>(); + + return { + log(severity: LogSeverity, category: LogCategory, message: string): void { + const entry: LogEntry = { + timestamp: Date.now(), + severity, + category, + message, + }; + entries.push(entry); + if (entries.length > MAX_ENTRIES) { + entries.shift(); + } + for (const cb of subscribers) { + cb(entry); + } + }, + + getRecent(): LogEntry[] { + return [...entries]; + }, + + subscribe(cb: (entry: LogEntry) => void): () => void { + subscribers.add(cb); + return () => { subscribers.delete(cb); }; + }, + }; +} + +export type LogService = ReturnType;