import { getSessions, runLargeOperation } from '../core'; import { captureCurrentSession } from './session-snapshot'; // Debounce window for autosave. A full-tab snapshot + storage write runs on // every tab open/close/move; a longer window coalesces rapid bursts (e.g. // opening or closing many tabs at once) into a single snapshot. const AUTOSAVE_DEBOUNCE_MS = 3000; export class AutoSaveManager { private autoSaveTimer: ReturnType | null = null; private autoSaveInFlight = false; private autoSavePending = false; // Bound handler references — stored so add/removeListener share identity // (removeListener must use the SAME reference that was added). readonly autoSaveHandler = async (): Promise => { await this.scheduleAutoSave(); }; readonly autoSaveUpdatedHandler = async (_tabId: number, changeInfo: chrome.tabs.OnUpdatedInfo = {}): Promise => { // Ignore noisy media/title/favicon/loading updates. Sessions only store URL and group/window structure. if (!("url" in changeInfo)) return; await this.scheduleAutoSave(); }; async setEnabled(enabled: boolean) { await chrome.storage.local.set({ autoSave: enabled }); chrome.tabs.onCreated.removeListener(this.autoSaveHandler); chrome.tabs.onRemoved.removeListener(this.autoSaveHandler); chrome.tabs.onMoved.removeListener(this.autoSaveHandler); chrome.tabs.onAttached.removeListener(this.autoSaveHandler); chrome.tabs.onDetached.removeListener(this.autoSaveHandler); chrome.tabs.onUpdated.removeListener(this.autoSaveUpdatedHandler); if (chrome.tabGroups?.onUpdated) chrome.tabGroups.onUpdated.removeListener(this.autoSaveHandler); if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer); this.autoSaveTimer = null; this.autoSavePending = false; if (enabled) { chrome.tabs.onCreated.addListener(this.autoSaveHandler); chrome.tabs.onRemoved.addListener(this.autoSaveHandler); chrome.tabs.onMoved.addListener(this.autoSaveHandler); chrome.tabs.onAttached.addListener(this.autoSaveHandler); chrome.tabs.onDetached.addListener(this.autoSaveHandler); chrome.tabs.onUpdated.addListener(this.autoSaveUpdatedHandler); if (chrome.tabGroups?.onUpdated) chrome.tabGroups.onUpdated.addListener(this.autoSaveHandler); } return { enabled }; } private async saveAutoSessionIfChanged() { const { session, signature, tabCount } = await captureCurrentSession(); const { autoSaveSignature } = await chrome.storage.local.get("autoSaveSignature"); if (autoSaveSignature === signature) return { skipped: true, tabs: tabCount }; const sessions = await getSessions(); sessions.__auto__ = session; await chrome.storage.local.set({ sessions, autoSaveSignature: signature }); return { skipped: false, tabs: tabCount }; } private async runAutoSave() { if (this.autoSaveInFlight) { this.autoSavePending = true; return; } this.autoSaveInFlight = true; try { const { autoSave } = await chrome.storage.local.get("autoSave"); if (autoSave) await runLargeOperation("session.auto_save", () => this.saveAutoSessionIfChanged()); } finally { this.autoSaveInFlight = false; if (this.autoSavePending) { this.autoSavePending = false; this.autoSaveTimer = setTimeout(() => this.runAutoSave(), AUTOSAVE_DEBOUNCE_MS); } } } private async scheduleAutoSave(delayMs = AUTOSAVE_DEBOUNCE_MS) { const { autoSave } = await chrome.storage.local.get("autoSave"); if (!autoSave) return; if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer); this.autoSaveTimer = setTimeout(() => this.runAutoSave(), delayMs); } }