All files / logging log-database.ts

4.87% Statements 2/41
0% Branches 0/42
0% Functions 0/14
5.55% Lines 2/36

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166  1x                           1x                                                                                                                                                                                                                                                                                                            
import { LogEntry } from './log-provider.interface';
import Dexie, { Table } from 'dexie';
 
/**
 * IndexedDB table entry (LogEntry with required id)
 */
interface LogTableEntry extends Omit<LogEntry, 'timestamp'> {
    id: number;
    timestamp: number; // Store as epoch for easier indexing
}
 
/**
 * Dexie database wrapper for log storage
 * Handles all IndexedDB operations
 */
export class LogDatabase extends Dexie {
    logs!: Table<LogTableEntry, number>;
 
    /** User agent string, captured once on init */
    private userAgent: string | undefined;
 
    constructor() {
        super('drevo-logs');
 
        this.version(1).stores({
            logs: '++id, timestamp, level',
        });
 
        // Capture user agent once on initialization
        if (typeof navigator !== 'undefined') {
            this.userAgent = navigator.userAgent;
        }
    }
 
    /**
     * Get the user agent string
     */
    getUserAgent(): string | undefined {
        return this.userAgent;
    }
 
    /**
     * Add a log entry to the database
     */
    async addLog(entry: LogEntry): Promise<number> {
        const tableEntry: Omit<LogTableEntry, 'id'> = {
            level: entry.level,
            message: entry.message,
            context: entry.context,
            data: entry.data,
            timestamp: entry.timestamp.getTime(),
            url: entry.url,
        };
 
        return this.logs.add(tableEntry as LogTableEntry);
    }
 
    /**
     * Add multiple log entries (for batch writes)
     */
    async addLogs(entries: LogEntry[]): Promise<void> {
        const tableEntries: Omit<LogTableEntry, 'id'>[] = entries.map(entry => ({
            level: entry.level,
            message: entry.message,
            context: entry.context,
            data: entry.data,
            timestamp: entry.timestamp.getTime(),
            url: entry.url,
        }));
 
        await this.logs.bulkAdd(tableEntries as LogTableEntry[]);
    }
 
    /**
     * Get logs with optional filtering
     */
    async getLogs(options?: {
        limit?: number;
        fromDate?: Date;
        toDate?: Date;
        levels?: string[];
    }): Promise<LogEntry[]> {
        let collection = this.logs.orderBy('timestamp').reverse();
 
        if (options?.fromDate) {
            const from = options.fromDate.getTime();
            collection = collection.filter(log => log.timestamp >= from);
        }
 
        if (options?.toDate) {
            const to = options.toDate.getTime();
            collection = collection.filter(log => log.timestamp <= to);
        }
 
        if (options?.levels && options.levels.length > 0) {
            const levels = options.levels;
            collection = collection.filter(log => levels.includes(log.level));
        }
 
        const entries = await (options?.limit ? collection.limit(options.limit).toArray() : collection.toArray());
 
        // Convert back to LogEntry format
        return entries.map(entry => ({
            id: entry.id,
            level: entry.level,
            message: entry.message,
            context: entry.context,
            data: entry.data,
            timestamp: new Date(entry.timestamp),
            url: entry.url,
        }));
    }
 
    /**
     * Clear all logs
     */
    async clearLogs(): Promise<void> {
        await this.logs.clear();
    }
 
    /**
     * Get count of log entries
     */
    async getLogCount(): Promise<number> {
        return this.logs.count();
    }
 
    /**
     * Delete oldest logs (by percentage)
     * @param percentage - Percentage of logs to delete (0-1)
     */
    async deleteOldestLogs(percentage: number): Promise<void> {
        const count = await this.getLogCount();
        const deleteCount = Math.floor(count * percentage);
 
        if (deleteCount > 0) {
            const oldestLogs = await this.logs.orderBy('timestamp').limit(deleteCount).primaryKeys();
 
            await this.logs.bulkDelete(oldestLogs);
        }
    }
 
    /**
     * Get approximate storage size using navigator.storage.estimate()
     * Falls back to rough estimate based on entry count
     */
    async getStorageSize(): Promise<number> {
        // Try using Storage API first (more accurate)
        if (typeof navigator !== 'undefined' && navigator.storage?.estimate) {
            try {
                const estimate = await navigator.storage.estimate();
                // Note: This gives total IndexedDB usage, not just our database
                // But it's the most accurate estimate available
                return estimate.usage ?? 0;
            } catch {
                // Fall through to estimate
            }
        }
 
        // Fallback: rough estimate based on average entry size
        const count = await this.getLogCount();
        // Estimate ~500 bytes per entry (conservative average)
        return count * 500;
    }
}