fix(StorageCleaner): 修复 IndexedDB 清理竞态、部分成功计数与刷新后状态同步
This commit is contained in:
+83
-67
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user