Files
browser-cli/extension/src/core/throttle.ts
T
daniel156161 477a00db1a
Testing / remote-protocol-compat (0.9.5) (push) Successful in 48s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 47s
Build & Publish Package / publish (push) Successful in 46s
Package Extension / package-extension (push) Successful in 59s
Testing / test (push) Failing after 50s
feat(extension): add Firefox WebExtension support
- Add a neutral WebExtension API adapter that uses Firefox browser.* or Chromium chrome.* without mutating globals.
- Switch extension runtime code to the adapter and add Firefox-specific typings for tabs, windows, tab groups, storage, scripting, and native messaging ports.
- Fix Firefox temporary add-on instructions to load the packaged manifest with background.scripts instead of the Chromium service worker manifest.
- Detect Firefox in clients.list via runtime.getBrowserInfo and keep Chromium user-agent fallback support.
- Make navigate.open wait briefly for Firefox to replace initial about:blank with the requested URL.
- Add JS coverage for API selection, clients.list browser detection, and Firefox navigate.open URL polling.
- Bump package and extension version to 0.15.2.
2026-06-14 19:09:10 +02:00

119 lines
4.6 KiB
TypeScript

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<void> {
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<void> = Promise.resolve();
export async function runLargeOperation<T>(name: string, fn: () => Promise<T>): Promise<T> {
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<T>(
items: T[],
throttle: { batchSize: number; pauseMs: number },
handler: (batch: T[]) => Promise<unknown>,
progress: { job?: Job | null; phase: string; total?: number; baseCurrent?: number },
): Promise<number> {
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;
}