src/lib/logger/log-store.ts

Total Symbols
13
Lines of Code
213
Avg Complexity
3.0
Avg Coverage
100.0%

File Relationships

graph LR initLogStore["initLogStore"] hasSessionStorage["hasSessionStorage"] notifyListeners["notifyListeners"] addLogEntry["addLogEntry"] scheduleFlush["scheduleFlush"] loadLogEntries["loadLogEntries"] clearLogEntries["clearLogEntries"] flushToStorage["flushToStorage"] initLogStore -->|calls| hasSessionStorage initLogStore -->|calls| notifyListeners addLogEntry -->|calls| notifyListeners addLogEntry -->|calls| scheduleFlush loadLogEntries -->|calls| hasSessionStorage clearLogEntries -->|calls| hasSessionStorage clearLogEntries -->|calls| notifyListeners flushToStorage -->|calls| hasSessionStorage click initLogStore "../symbols/e592afd94ce8e81f.html" click hasSessionStorage "../symbols/a7e74519d0293203.html" click notifyListeners "../symbols/907bc8a226709b64.html" click addLogEntry "../symbols/b5eb9ecc61a3eb82.html" click scheduleFlush "../symbols/142b73cec109660e.html" click loadLogEntries "../symbols/62c6eec3d1dbde2c.html" click clearLogEntries "../symbols/b955aae3387005ad.html" click flushToStorage "../symbols/6a2d1616005381ca.html"

Symbols by Kind

function 11
interface 1
type 1

All Symbols

Name Kind Visibility Status Lines Signature
LogEntry interface exported- 14-23 interface LogEntry
LogStoreListener type - 25-25 type LogStoreListener
initLogStore function exported- 52-81 initLogStore(): : Promise<void>
addLogEntry function exported- 87-96 addLogEntry(entry: LogEntry): : void
getLogEntries function exported- 102-104 getLogEntries(): : LogEntry[]
loadLogEntries function exported- 109-122 loadLogEntries(): : Promise<LogEntry[]>
configureLogStore function exported- 127-131 configureLogStore(options: { maxEntries?: number }): : void
clearLogEntries function exported- 135-153 clearLogEntries(): : Promise<void>
onLogUpdate function exported- 160-165 onLogUpdate(listener: LogStoreListener): : () => void
hasSessionStorage function - 169-175 hasSessionStorage(): : boolean
notifyListeners function - 177-185 notifyListeners(): : void
scheduleFlush function - 187-190 scheduleFlush(): : void
flushToStorage function - 192-212 flushToStorage(): : Promise<void>

Full Source

/**
 * Persistent Log Store
 *
 * Stores log entries in chrome.storage.session so they are accessible
 * across all extension contexts (content script, background, devtools, options, popup).
 *
 * - Entries are buffered in memory and flushed to storage periodically (500ms debounce).
 * - Max 1000 entries with FIFO eviction.
 * - Listeners can subscribe to real-time log updates.
 */

import type { LogLevel } from "./index";

export interface LogEntry {
  /** ISO timestamp */
  ts: string;
  /** Log level */
  level: LogLevel;
  /** Logger namespace (e.g. "FormFiller", "ChromeAI") */
  ns: string;
  /** Formatted message text */
  msg: string;
}

type LogStoreListener = (entries: LogEntry[]) => void;

const STORAGE_KEY = "fill_all_log_entries";
let maxEntries = 1000;
const FLUSH_INTERVAL_MS = 500;

/** In-memory mirror of all entries (local context) */
let localEntries: LogEntry[] = [];

/** Entries pending write to storage */
let pendingEntries: LogEntry[] = [];

/** Debounce timer handle */
let flushTimer: ReturnType<typeof setTimeout> | null = null;

/** Subscribers notified on every new batch of entries */
const listeners: Set<LogStoreListener> = new Set();

/** Whether the store has been initialized (loaded from storage) */
let initialized = false;

// ── Public API ─────────────────────────────────────────────────────────────────

/**
 * Initializes the log store by loading existing entries from chrome.storage.session
 * and subscribing to cross-context changes.
 */
export async function initLogStore(): Promise<void> {
  if (initialized) return;
  initialized = true;

  if (!hasSessionStorage()) return;

  try {
    const result = await chrome.storage.session.get(STORAGE_KEY);
    const stored = result[STORAGE_KEY] as LogEntry[] | undefined;
    if (Array.isArray(stored)) {
      localEntries = stored.slice(-maxEntries);
    }
  } catch {
    // Storage unavailable — stay with empty local entries
  }

  // Listen for changes from other contexts
  try {
    chrome.storage.onChanged.addListener((changes, area) => {
      if (area !== "session" || !changes[STORAGE_KEY]) return;
      const newVal = changes[STORAGE_KEY].newValue as LogEntry[] | undefined;
      if (Array.isArray(newVal)) {
        localEntries = newVal.slice(-maxEntries);
        notifyListeners();
      }
    });
  } catch {
    // Listener setup failed — cross-context sync won't work
  }
}

/**
 * Adds a log entry to the store.
 * Synchronous — queues an async flush to chrome.storage.session.
 */
export function addLogEntry(entry: LogEntry): void {
  localEntries.push(entry);
  if (localEntries.length > maxEntries) {
    localEntries = localEntries.slice(-maxEntries);
  }

  pendingEntries.push(entry);
  notifyListeners();
  scheduleFlush();
}

/**
 * Returns all log entries currently in memory..
 * Call `initLogStore()` first to ensure cross-context entries are loaded.
 */
export function getLogEntries(): LogEntry[] {
  return localEntries;
}

/**
 * Loads entries from chrome.storage.session (useful for late-opening UIs).
 */
export async function loadLogEntries(): Promise<LogEntry[]> {
  if (!hasSessionStorage()) return localEntries;

  try {
    const result = await chrome.storage.session.get(STORAGE_KEY);
    const stored = result[STORAGE_KEY] as LogEntry[] | undefined;
    if (Array.isArray(stored)) {
      localEntries = stored.slice(-maxEntries);
    }
  } catch {
    // Ignore — use local entries
  }
  return localEntries;
}
/**
 * Configures the log store at runtime.
 * Must be called before entries are added for the limit to take effect.
 */
export function configureLogStore(options: { maxEntries?: number }): void {
  if (options.maxEntries !== undefined) {
    maxEntries = Math.max(50, Math.min(5000, options.maxEntries));
  }
}
/**
 * Clears all log entries from memory and storage.
 */
export async function clearLogEntries(): Promise<void> {
  localEntries = [];
  pendingEntries = [];

  if (flushTimer) {
    clearTimeout(flushTimer);
    flushTimer = null;
  }

  if (hasSessionStorage()) {
    try {
      await chrome.storage.session.set({ [STORAGE_KEY]: [] });
    } catch {
      // Ignore
    }
  }

  notifyListeners();
}

/**
 * Subscribes to log entry updates.
 * The listener receives all current entries on every change.
 * @returns An unsubscribe function.
 */
export function onLogUpdate(listener: LogStoreListener): () => void {
  listeners.add(listener);
  return () => {
    listeners.delete(listener);
  };
}

// ── Internals ──────────────────────────────────────────────────────────────────

function hasSessionStorage(): boolean {
  return (
    typeof chrome !== "undefined" &&
    !!chrome.storage &&
    !!chrome.storage.session
  );
}

function notifyListeners(): void {
  for (const fn of listeners) {
    try {
      fn(localEntries);
    } catch {
      // Listener error — ignore
    }
  }
}

function scheduleFlush(): void {
  if (flushTimer) return;
  flushTimer = setTimeout(flushToStorage, FLUSH_INTERVAL_MS);
}

async function flushToStorage(): Promise<void> {
  flushTimer = null;
  if (pendingEntries.length === 0) return;

  const toFlush = pendingEntries;
  pendingEntries = [];

  if (!hasSessionStorage()) return;

  try {
    const result = await chrome.storage.session.get(STORAGE_KEY);
    let all: LogEntry[] = (result[STORAGE_KEY] as LogEntry[] | undefined) ?? [];
    all.push(...toFlush);
    if (all.length > maxEntries) {
      all = all.slice(-maxEntries);
    }
    await chrome.storage.session.set({ [STORAGE_KEY]: all });
  } catch {
    // Storage write failed — entries remain in local memory only
  }
}