import { webExtApi as api } from '../browser-api'; // Large-operation throttling, performance profile, and job-progress helpers. import type { Job, JobProgressUpdate } from '../types'; export function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } export const LARGE_OPERATION_BATCH_SIZE = 25; export const LARGE_OPERATION_PAUSE_MS = 25; export const GENTLE_OPERATION_BATCH_SIZE = 8; export const GENTLE_OPERATION_PAUSE_MS = 100; const DEBUG_LARGE_OPERATIONS = false; function debugLargeOperation(message: string) { if (DEBUG_LARGE_OPERATIONS) console.log(message); } export async function hasAudibleTabs() { const audibleTabs = await api.tabs.query({ audible: true }); return audibleTabs.some(tab => !(tab.mutedInfo && tab.mutedInfo.muted)); } let largeOperationQueue: Promise = Promise.resolve(); export async function runLargeOperation(name: string, fn: () => Promise): Promise { const run = largeOperationQueue.then(async () => { debugLargeOperation(`[browser-cli] large operation start: ${name}`); try { return await fn(); } finally { debugLargeOperation(`[browser-cli] large operation done: ${name}`); } }); largeOperationQueue = run.then(() => {}, () => {}); return run; } export async function getPerformanceProfile() { const { performanceProfile } = await api.storage.local.get<{ performanceProfile?: string }>("performanceProfile"); return performanceProfile || "auto"; } export async function setPerformanceProfile(profile: string) { const allowed = new Set(["auto", "normal", "gentle", "ultra"]); const performanceProfile = allowed.has(profile) ? profile : "auto"; await api.storage.local.set({ performanceProfile }); return { performanceProfile }; } export async function getLargeOperationThrottle(itemCount = 0, mode: string = "auto") { const audible = await hasAudibleTabs(); const storedProfile = await getPerformanceProfile(); const configuredMode = mode && mode !== "auto" ? mode : storedProfile; const gentle = configuredMode === "gentle" || configuredMode === "ultra" || (configuredMode === "auto" && audible); let batchSize = gentle ? GENTLE_OPERATION_BATCH_SIZE : LARGE_OPERATION_BATCH_SIZE; let pauseMs = gentle ? GENTLE_OPERATION_PAUSE_MS : LARGE_OPERATION_PAUSE_MS; if (configuredMode === "ultra" || itemCount >= 300) { batchSize = Math.max(3, Math.floor(batchSize / 2)); pauseMs *= 2; } else if (itemCount >= 100) { batchSize = Math.max(5, Math.floor(batchSize * 0.75)); pauseMs = Math.max(pauseMs, 75); } return { batchSize, pauseMs, gentle, audible, itemCount, mode: configuredMode }; } export function updateJobProgress(job: Job | undefined | null, { phase, current, total }: JobProgressUpdate = {}) { if (!job) return; if (phase) job.phase = phase; if (total != null) job.total = total; if (current != null) job.current = current; if (job.total) job.percent = Math.min(100, Math.round((job.current || 0) * 100 / job.total)); job.updatedAt = Date.now(); } export function throwIfJobCancelled(job: Job | undefined | null) { if (job?.cancelRequested) { throw new Error(`Job '${job.id}' cancelled`); } } export async function yieldForLargeOperation(processed: number, batchSize = LARGE_OPERATION_BATCH_SIZE, pauseMs = LARGE_OPERATION_PAUSE_MS) { if (processed > 0 && processed % batchSize === 0) { await sleep(pauseMs); } } /** * Run `handler` over `items` in throttle-sized slices, threading job progress * and cancellation. Each slice is one `handler` call (so chrome batch APIs like * tabs.remove get an array); between slices it reports progress and yields. * * `total`/`baseCurrent` let callers report against a fixed total accumulated * across several calls (e.g. merging tabs from many windows). Returns the * running processed count so the next call can continue from it. */ export async function processInBatches( items: T[], throttle: { batchSize: number; pauseMs: number }, handler: (batch: T[]) => Promise, progress: { job?: Job | null; phase: string; total?: number; baseCurrent?: number }, ): Promise { const total = progress.total ?? items.length; let done = progress.baseCurrent ?? 0; updateJobProgress(progress.job, { phase: progress.phase, current: done, total }); for (let i = 0; i < items.length; i += throttle.batchSize) { throwIfJobCancelled(progress.job); const batch = items.slice(i, i + throttle.batchSize); await handler(batch); done += batch.length; updateJobProgress(progress.job, { phase: progress.phase, current: done, total }); await yieldForLargeOperation(done, throttle.batchSize, throttle.pauseMs); } return done; }