import type { LogLevel, LogContext, LogEntry, LoggerConfig, LogTransport } from './types'; const LEVEL_PRIORITY: Record = { debug: 0, info: 1, warn: 2, error: 3, }; /** Default transport: sends batched logs via sendBeacon or fetch. */ class HttpTransport implements LogTransport { constructor(private endpoint: string) {} send(entries: LogEntry[]): void { const payload = JSON.stringify(entries); // sendBeacon is fire-and-forget, works during page unload if (typeof navigator !== 'undefined' && navigator.sendBeacon) { const blob = new Blob([payload], { type: 'application/json' }); const sent = navigator.sendBeacon(this.endpoint, blob); if (sent) return; } // Fallback to fetch (non-blocking) fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: payload, keepalive: true, }).catch(() => { // Silently drop - we don't want logging failures to break the app }); } } /** Console transport for development. */ class ConsoleTransport implements LogTransport { send(entries: LogEntry[]): void { for (const entry of entries) { const method = entry.level === 'debug' ? 'log' : entry.level; const ctx = Object.keys(entry.context).length > 0 ? entry.context : undefined; if (entry.error) { console[method](`[${entry.level.toUpperCase()}] ${entry.message}`, entry.error, ctx); } else if (ctx) { console[method](`[${entry.level.toUpperCase()}] ${entry.message}`, ctx); } else { console[method](`[${entry.level.toUpperCase()}] ${entry.message}`); } } } } export class Logger { private buffer: LogEntry[] = []; private timer: ReturnType | null = null; private transport: LogTransport; private minLevel: number; private baseContext: LogContext; private batchSize: number; private flushInterval: number; constructor(config: LoggerConfig) { this.minLevel = LEVEL_PRIORITY[config.level]; this.batchSize = config.batchSize ?? 20; this.flushInterval = config.flushInterval ?? 5000; this.baseContext = { service: config.service }; if (config.endpoint) { this.transport = new HttpTransport(config.endpoint); } else { this.transport = new ConsoleTransport(); } this.startFlushTimer(); // Flush on page unload if (typeof window !== 'undefined') { window.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { this.flush(); } }); window.addEventListener('pagehide', () => this.flush()); } } /** Create a child logger with additional context fields. */ withContext(ctx: LogContext): Logger { const child = Object.create(this) as Logger; child.baseContext = { ...this.baseContext, ...ctx }; return child; } debug(message: string, ctx?: LogContext): void { this.log('debug', message, ctx); } info(message: string, ctx?: LogContext): void { this.log('info', message, ctx); } warn(message: string, ctx?: LogContext): void { this.log('warn', message, ctx); } error(message: string, error?: Error | unknown, ctx?: LogContext): void { const entry = this.createEntry('error', message, ctx); if (error instanceof Error) { entry.error = { name: error.name, message: error.message, stack: error.stack, }; } else if (error !== undefined) { entry.error = { name: 'UnknownError', message: String(error), }; } this.enqueue(entry); } /** Force-flush the buffer immediately. */ flush(): void { if (this.buffer.length === 0) return; const entries = this.buffer.splice(0); this.transport.send(entries); } /** Stop the flush timer (call on teardown). */ destroy(): void { this.flush(); if (this.timer) { clearInterval(this.timer); this.timer = null; } } private log(level: LogLevel, message: string, ctx?: LogContext): void { if (LEVEL_PRIORITY[level] < this.minLevel) return; this.enqueue(this.createEntry(level, message, ctx)); } private createEntry(level: LogLevel, message: string, ctx?: LogContext): LogEntry { return { level, message, timestamp: new Date().toISOString(), context: { ...this.baseContext, ...ctx }, }; } private enqueue(entry: LogEntry): void { this.buffer.push(entry); if (this.buffer.length >= this.batchSize) { this.flush(); } } private startFlushTimer(): void { if (this.flushInterval > 0 && typeof setInterval !== 'undefined') { this.timer = setInterval(() => this.flush(), this.flushInterval); } } } /** Create a logger instance. */ export function createLogger(config: LoggerConfig): Logger { return new Logger(config); }