fix(StorageCleaner): 修复 IndexedDB 清理竞态、部分成功计数与刷新后状态同步

This commit is contained in:
2026-06-25 23:31:44 +08:00
parent ebf9f7a153
commit edd4cf266c
13 changed files with 1545 additions and 128 deletions
+83 -67
View File
@@ -1,12 +1,22 @@
import type { CleaningResult, StorageCleanerOptions, StorageCleanResult } from '@/types/storage';
import { CLEAN_OPTION_KEYS, OPTION_LABELS } from '@/pages/StorageCleaner/constants';
import {
clearAllIndexedDBs,
INDEXED_DB_CLEAR_STORE_TIMEOUT_MS,
INDEXED_DB_DELETE_FALLBACK_DELAY_MS,
INDEXED_DB_DELETE_TIMEOUT_MS,
type IndexedDBCleanResult,
} from '@/utils/indexedDbCleaner';
import { browser } from 'wxt/browser';
type CleanScriptResult = IndexedDBCleanResult;
export async function getCurrentTab() {
// For popup pages, we need to get the active tab from the browser window that triggered the popup.
// We should ONLY care about the currently active tab in the last focused window.
// If it's a restricted URL, we return it anyway and let the caller handle the error display.
const [tab] = await chrome.tabs.query({
const [tab] = await browser.tabs.query({
active: true,
lastFocusedWindow: true,
});
@@ -16,7 +26,7 @@ export async function getCurrentTab() {
}
// Fallback for cases where lastFocusedWindow might not work as expected (e.g. certain sidepanel scenarios)
const [fallbackTab] = await chrome.tabs.query({
const [fallbackTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
@@ -26,7 +36,7 @@ export async function getCurrentTab() {
export async function getCookieSize(url: string): Promise<number> {
try {
const cookies = await chrome.cookies.getAll({ url });
const cookies = await browser.cookies.getAll({ url });
const encoder = new TextEncoder();
// 估算:名称 + 值 + 域名 + 路径 的 UTF-8 字节数
return cookies.reduce(
@@ -44,29 +54,49 @@ export async function getCookieSize(url: string): Promise<number> {
}
}
/**
* 通用辅助:在指定标签页中执行脚本并返回结果
*
* @param tabId 标签页 ID
* @param func 在页面上下文中执行的函数
* @param errorLabel 错误日志前缀
* @param fallback 执行失败时的回退值
*/
async function runScript<T>(
type ExecuteInTabOptions<T> =
| { errorLabel: string; mode: 'fallback'; fallback: T }
| { errorLabel: string; mode: 'throw' };
async function executeInTab<T, A extends unknown[] = []>(
tabId: number,
func: () => T | Promise<T>,
errorLabel: string,
fallback: T,
func: (...args: A) => T | Promise<T>,
options: ExecuteInTabOptions<T>,
args?: A,
): Promise<T> {
try {
const [result] = await chrome.scripting.executeScript({ target: { tabId }, func });
return (result?.result as T) ?? fallback;
const [result] = await browser.scripting.executeScript({
target: { tabId },
func,
...(args ? { args } : {}),
});
const value = result?.result as T | undefined;
if (value !== undefined && value !== null) {
return value;
}
if (options.mode === 'fallback') {
return options.fallback;
}
return undefined as T;
} catch (error) {
console.error(`Failed to ${errorLabel}:`, error);
return fallback;
console.error(`Failed to ${options.errorLabel}:`, error);
if (options.mode === 'throw') {
throw error;
}
return options.fallback;
}
}
async function runScript<T, A extends unknown[] = []>(
tabId: number,
func: (...args: A) => T | Promise<T>,
errorLabel: string,
fallback: T,
args?: A,
): Promise<T> {
return executeInTab(tabId, func, { errorLabel, mode: 'fallback', fallback }, args);
}
export async function getLocalStorageSize(tabId: number): Promise<number> {
return runScript(
tabId,
@@ -164,12 +194,12 @@ export async function getServiceWorkerCount(tabId: number): Promise<number> {
export async function clearCookies(url: string): Promise<StorageCleanResult> {
try {
const cookies = await chrome.cookies.getAll({ url });
const cookies = await browser.cookies.getAll({ url });
for (const cookie of cookies) {
const protocol = cookie.secure ? 'https:' : 'http:';
const domain = cookie.domain.startsWith('.') ? cookie.domain.slice(1) : cookie.domain;
const cookieUrl = `${protocol}//${domain}${cookie.path}`;
await chrome.cookies.remove({
await browser.cookies.remove({
url: cookieUrl,
name: cookie.name,
storeId: cookie.storeId,
@@ -188,16 +218,33 @@ export async function clearCookies(url: string): Promise<StorageCleanResult> {
* @param func 在页面上下文中执行的清理函数
* @param errorLabel 错误日志前缀
*/
async function runCleanScript(
async function runCleanScript<A extends unknown[] = []>(
tabId: number,
func: () => { count: number } | Promise<{ count: number }>,
func: (...args: A) => CleanScriptResult | Promise<CleanScriptResult>,
errorLabel: string,
args?: A,
): Promise<StorageCleanResult> {
const raw = await runScript(tabId, func, errorLabel, { count: 0 });
if (raw && typeof raw === 'object' && 'count' in raw) {
try {
const raw = await executeInTab(tabId, func, { errorLabel, mode: 'throw' }, args);
if (!raw || typeof raw !== 'object' || !('count' in raw)) {
return { success: false, error: 'No result returned' };
}
if (raw.errors?.length) {
const errorMsg = raw.errors.join('\n');
const partialHint = raw.count > 0 ? `(已成功清理 ${raw.count} 个数据库,但部分失败)\n` : '';
return {
success: false,
error: partialHint + errorMsg,
...(raw.count > 0 ? { count: raw.count } : {}),
};
}
return { success: true, count: raw.count };
} catch (error) {
console.error(`Failed to ${errorLabel}:`, error);
return { success: false, error: String(error) };
}
return { success: false, error: 'No result returned' };
}
async function injectClearLocalStorage(tabId: number): Promise<StorageCleanResult> {
@@ -225,47 +272,11 @@ async function injectClearSessionStorage(tabId: number): Promise<StorageCleanRes
}
async function injectClearIndexedDB(tabId: number): Promise<StorageCleanResult> {
return runCleanScript(
tabId,
async () => {
if (typeof indexedDB.databases !== 'function') {
return { count: 0 };
}
const databases = await indexedDB.databases();
let count = 0;
for (const db of databases) {
if (!db.name) continue;
const dbName = db.name;
try {
await new Promise<void>((resolve, reject) => {
const deleteReq = indexedDB.deleteDatabase(dbName);
const timeout = setTimeout(() => {
console.warn('IndexedDB delete timeout:', dbName);
resolve();
}, 5000);
deleteReq.onblocked = () => {
console.warn('IndexedDB delete blocked:', dbName);
clearTimeout(timeout);
resolve();
};
deleteReq.onsuccess = () => {
clearTimeout(timeout);
resolve();
};
deleteReq.onerror = () => {
clearTimeout(timeout);
reject(new Error(`Failed to delete ${dbName}`));
};
});
count++;
} catch (e) {
console.error('Delete DB error:', e);
}
}
return { count };
},
'clear IndexedDB',
);
return runCleanScript(tabId, clearAllIndexedDBs, 'clear IndexedDB', [
INDEXED_DB_DELETE_TIMEOUT_MS,
INDEXED_DB_CLEAR_STORE_TIMEOUT_MS,
INDEXED_DB_DELETE_FALLBACK_DELAY_MS,
]);
}
async function injectClearCacheStorage(tabId: number): Promise<StorageCleanResult> {
@@ -333,6 +344,11 @@ export async function clearStorage(
);
if (failures.length > 0) {
result.overallSuccess = false;
result.error = CLEAN_OPTION_KEYS.flatMap((key) => {
const itemResult = result[key];
if (!itemResult || itemResult.success) return [];
return `${OPTION_LABELS[key]}: ${itemResult.error}`;
}).join('\n');
}
return result;