Files
testing-tool/spec/storage-cleaner/indexeddb-fix-plan.md
T
LingandRX 36b6bfd20d Develop (#74)
* feat: 生成成功改为 toast 提示
* refactor: 数据预览改为显示示例数据,简化组件逻辑
* fix: 非必填字段预览显示 null
* fix: 修复空值率与必填状态的逻辑
* fix: 切换为非必填时默认空值率 100%
* feat: 字段编辑弹窗添加「完成」按钮 去除关闭按钮,添加取消按钮
* refactor: 删除未使用的 defaultNullRate 功能
* feat: 优化规则管理功能
* fix: 更新 features 测试断言以匹配新增的 testDataGenerator
* refactor: 将 formatFileSize 重命名为 formatBytes 并移至通用工具模块
* refactor: 使用 Label 组件替换原生 label 元素,并在 DecodeResultPaper 中引入 Input 组件
* refactor: 规范化页面组件目录结构
* refactor(JsonTools): 规范化页面组件目录结构
* refactor(StorageCleaner): 规范化页面组件目录结构
* refactor(Timestamp): 规范化页面组件目录结构
* refactor(i18n): 移除 chrome.i18n 国际化,统一使用中文硬编码
* refactor(AppRoot): 移除 RouterProvider 组件,直接渲染子组件
* fix(RouterProvider): 将 goBack 重命名为 goHome,修复跨窗同步 merge 不一致
* fix(ThemeModeProvider): 修复主题快照同步与首屏闪烁问题
* refactor: 将 UI 文案改为中文并优化相关逻辑
* refactor: 清理代码注释并优化组件结构
* feat(Dashboard): 重构仪表板功能组件和逻辑
* refactor: 更新功能键名称并移除 README 文件
* refactor: 重构 TopBar 组件并更新项目结构
* feat(TopBar): 添加搜索功能组件并优化结构
* refactor: 统一使用 Button 组件替换原有 button 元素
* feat: 添加 EmptyPlaceholder 组件并更新相关页面
* feat(TextStatistics): 引入 StatCard 组件以优化统计信息展示
* feat(Timestamp): 重构时间戳转换功能,新增 ConverterForm 组件
* feat(Jwt): 重构 JWT 组件,新增常量和结果视图
* refactor(RightClickRestorer): 用状态配置表简化页面 UI 结构
* fix(business): 修复 skewedRandom 函数中的数学计算错误
* refactor(ImageUploader): 移除预览 URL 相关逻辑并简化粘贴事件处理
* feat(TestDataGenerator): 更新规则编辑提示和警告显示逻辑
* refactor(CopyButton.test): 移除冗余测试用例
* refactor(RouterProvider, chromeStorage, useContextMenuData): 更新存储获取逻辑以移除默认值参数
* refactor(代码重复): 抽取共享工具函数与 UI 组件,消除多处重复实现
* fix(RouterProvider): 修复初始加载状态逻辑,确保在加载前不覆盖 chrome.storage
* feat(TopBar): 添加搜索框快捷键提示功能及相关测试
* refactor(StorageCleaner): 移动 isRestrictedUrl 函数到独立模块并更新相关引用
* fix(StorageCleaner): 修复 IndexedDB 清理竞态、部分成功计数与刷新后状态同步
* fix: 修复路由初始化竞态、存储状态静默覆盖与存储清理误删
* refactor: 统一中文文案并简化组件结构,提升代码可读性
* refactor: 更新 Worker 消息类型以支持生成任务 ID
* fix: 修复 ruleStorage 写入失败处理与 CopyButton 成功态样式
2026-06-27 17:58:59 +08:00

16 KiB
Raw Blame History

Storage Cleaner — IndexedDB 修复方案与验收标准

创建时间: 2026-06-25 状态: Phase 3 已完成(2026-06-25 关联模块: src/utils/storageCleaner.ts 前置审查: Code ReviewstorageCleaner.ts 修改版)

背景与目标

本次修复针对 storageCleaner.ts 中 IndexedDB 清理逻辑及错误处理链路的审查结论,按优先级分三阶段实施。

问题域 现状 目标
IndexedDB fallback store.clear() 完成后立即 db.close() 等 transaction commit 后再关闭
deleteDatabase 超时 超时后仍可能触发 onsuccess,与 fallback 并发 单次删除生命周期内只 resolve 一次
部分成功 多 DB 部分失败时 count 丢失 失败结果保留已清理数量
代码结构 runScript / runCleanScript 重复 统一 executeScript 入口
测试 缺多 DB 混合场景 补单元测试覆盖

Phase 1 — 合并前必做(P1

1.1 等待 IndexedDB transaction 完成后再关闭连接

问题

clearObjectStoresPromise.all(clearStore...) 结束后立刻 db.close()。单个 clearReq.onsuccess 只表示 request 完成,transaction 可能尚未 commit,存在清空被回滚的风险。

根因

IndexedDB 规范中,transaction 的持久化以 transaction.oncomplete 为准,而非单个 request 的 onsuccess

修复方案

在注入脚本内的 clearObjectStores 中,增加 waitForTransaction 辅助函数:

const waitForTransaction = (tx: IDBTransaction): Promise<void> =>
  new Promise((resolve, reject) => {
    tx.oncomplete = () => resolve();
    tx.onerror = () => reject(tx.error ?? new Error('Transaction failed'));
    tx.onabort = () => reject(tx.error ?? new Error('Transaction aborted'));
  });

修改 openReq.onsuccess 分支:

const transaction = db.transaction(storeNames, 'readwrite');
const errors = (
  await Promise.all(
    storeNames.map((storeName) => clearStore(transaction.objectStore(storeName), storeName)),
  )
).filter((error): error is string => Boolean(error));

try {
  await waitForTransaction(transaction);
} catch {
  db.close();
  resolve({
    success: false,
    errors: [`清空 IndexedDB 失败(${dbName}),请刷新后重试`],
  });
  return;
}

db.close();
resolve({ success: errors.length === 0, errors });

涉及文件

  • src/utils/storageCleaner.tsinjectClearIndexedDBclearObjectStores

新增测试

it('should wait for transaction complete before closing db', async () => {
  // mock: clear onsuccess 先于 transaction.oncomplete 触发
  // 断言 db.close 在 transaction.oncomplete 之后调用
});

1.2 消除 deleteDatabase 超时竞态

问题

超时 resolve('timeout') 后,deleteReq.onsuccess 仍可能触发;此时 fallback 的 indexedDB.open 与进行中的 deleteDatabase 可能并发,行为未定义。

修复方案

为每个 DB 删除引入 单次 settle 状态:

const waitForDeleteDatabase = (dbName: string, timeoutMs: number) =>
  new Promise<'deleted' | 'blocked' | 'timeout' | 'error'>((resolve) => {
    let settled = false;
    const settle = (status: 'deleted' | 'blocked' | 'timeout' | 'error') => {
      if (settled) return;
      settled = true;
      clearTimeout(timeout);
      resolve(status);
    };

    const deleteReq = indexedDB.deleteDatabase(dbName);
    const timeout = setTimeout(() => {
      console.warn('IndexedDB delete timeout:', dbName);
      settle('timeout');
    }, timeoutMs);

    deleteReq.onblocked = () => {
      console.warn('IndexedDB delete blocked:', dbName);
      settle('blocked');
    };
    deleteReq.onsuccess = () => settle('deleted');
    deleteReq.onerror = () => settle('error');
  });

timeout / blocked 后的 fallback 策略

状态 行为
blocked 立即 fallback clearObjectStores(页面仍占用连接,open 通常可行)
timeout await delay(100~200ms) 再 fallback,降低与 delete 并发概率
error 不 fallback,直接报错

涉及文件

  • src/utils/storageCleaner.ts — 替换现有 new Promise 删除逻辑

新增测试

it('should ignore late onsuccess after delete timeout', async () => {
  // deleteBehavior: timeout5000ms 后 resolve timeout
  // 6000ms 后再触发 onsuccess
  // 断言:只走 fallback 一次,count 不因 late onsuccess 重复 +1
});

Phase 2 — 建议同 PR 或紧接 follow-upP2

2.1 IndexedDB 部分成功时保留 count

问题

多 DB 场景返回 { count: 2, errors: ['...'] } 时,runCleanScript 只返回 { success: false, error },用户看不到已清理 2 个库。

修复方案(推荐)

扩展失败分支类型,可选 count

// src/types/storage.d.ts
export type StorageCleanResult =
  | { success: true; count: number }
  | { success: false; error: string; count?: number }; // 部分成功时的已清理数

修改 runCleanScript

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 } : {}),
  };
}

UI 层(可选增强)

CleaningResult.tsx 失败时若 result.indexedDB?.count 存在,可展示部分成功提示(非必须,error 字符串已含 hint 即可)。

涉及文件

  • src/types/storage.d.ts
  • src/utils/storageCleaner.tsrunCleanScript
  • src/utils/__tests__/storageCleaner.test.ts
  • (可选)src/pages/StorageCleaner/components/CleaningResult.tsx

新增测试

it('should preserve partial count when some IndexedDB databases fail', async () => {
  // 3 个 DB2 成功删除,1 blocked 且 fallback 失败
  // expect: success false, count 2, error 含「已成功清理 2 个」
});

2.2 统一 executeScript 调用入口

问题

runScriptrunCleanScript 各自调用 browser.scripting.executeScript,行为不一致(吞错 vs 抛错)。

修复方案

抽取底层函数:

type ExecuteScriptMode = 'fallback' | 'throw';

async function executeInTab<T>(
  tabId: number,
  func: () => T | Promise<T>,
  options: { errorLabel: string; mode: 'fallback'; fallback: T },
): Promise<T>;
async function executeInTab<T>(
  tabId: number,
  func: () => T | Promise<T>,
  options: { errorLabel: string; mode: 'throw' },
): Promise<T>;
async function executeInTab<T>(...) {
  try {
    const [result] = await browser.scripting.executeScript({ target: { tabId }, func });
    return (result?.result as T) ?? (options.mode === 'fallback' ? options.fallback : undefined as T);
  } catch (error) {
    console.error(`Failed to ${options.errorLabel}:`, error);
    if (options.mode === 'throw') throw error;
    return options.fallback;
  }
}
  • runScriptexecuteInTab(..., { mode: 'fallback', fallback })
  • runCleanScriptexecuteInTab(..., { mode: 'throw' }) + 结果解析

涉及文件

  • src/utils/storageCleaner.ts

验收

现有 11 个测试全部通过,无行为回归。


2.3 错误信息分隔符统一

问题

runCleanScript'; ' 拼接,clearStorageresult.error'\n'UI 用 break-all 展示,多错误时可读性不一致。

修复方案

IndexedDB 内部多错误统一改为 '\n'

error: raw.errors.join('\n');

涉及文件

  • src/utils/storageCleaner.ts
  • 相关测试断言(若有 '; ' 期望)

2.4 补充多 DB 混合场景测试

用例 输入 期望
全部成功 3 DB,均 delete success success: true, count: 3
部分 fallback 成功 2 success + 1 blocked→clear OK success: true, count: 3
部分失败 2 success + 1 error success: false, count: 2, error 含失败库名
空库列表 databases() 返回 [] success: true, count: 0

涉及文件

  • src/utils/__tests__/storageCleaner.test.ts
  • 扩展 createIndexedDBMock 支持 per-db 不同 deleteBehavior

Phase 3 — 可选优化(P3

3.1 超时常量提升到模块级

// src/utils/storageCleaner.ts 或 src/pages/StorageCleaner/constants.ts
const INDEXED_DB_DELETE_TIMEOUT_MS = 5000;
const INDEXED_DB_CLEAR_STORE_TIMEOUT_MS = 5000;

注入脚本通过闭包引用(executeScript 会序列化 func,常量需在 func 外部定义并 capture,或仍写在 func 内但从模块常量赋值)。

3.2 IndexedDB 逻辑拆分(长期)

clearObjectStoreswaitForDeleteDatabase 等抽到 src/utils/indexedDbCleaner.ts 的纯函数,注入层只做:

async () => clearAllIndexedDBs(INDEXED_DB_DELETE_TIMEOUT_MS);

便于单测,不依赖 mockExecuteScriptEval 间接执行注入函数。工作量大,建议单独 PR。


实施顺序

flowchart TD
  A[1.1 transaction.oncomplete] --> B[1.2 delete settle 防竞态]
  B --> C[2.4 补多 DB 测试]
  C --> D[2.1 部分成功 count]
  D --> E[2.2 统一 executeInTab]
  E --> F[2.3 错误分隔符]
  F --> G[3.x 可选重构]
阶段 预估工作量 风险
Phase 1 0.5~1 天 低,逻辑局部
Phase 2 0.5~1 天 中,涉及类型扩展
Phase 3 1~2 天 低,可延后

验收标准

A. 自动化(CI 必须通过)

npm run test -- src/utils/__tests__/storageCleaner.test.ts
npm run typecheck
npm run lint
编号 标准
A-1 全部单元测试通过,新增测试 ≥ 3(transaction 顺序、late onsuccess、多 DB 混合)
A-2 tsc --noEmit 无错误;若扩展 StorageCleanResult,所有引用处类型正确
A-3 ESLint --max-warnings=0 通过

B. 功能行为

编号 场景 期望结果
B-1 单 DBdeleteDatabase 成功 indexedDB: { success: true, count: 1 }overallSuccess: true
B-2 单 DBdeleteDatabase blockedfallback clear 成功 success: true, count: 1transaction 在 oncompletedb.close
B-3 单 DBdelete 超时 5sfallback clear 成功 5s 内进入 fallback;不因 late onsuccess 重复计数
B-4 fallback 中某 store clear hang 5s success: falseerror 含 dbName/storeName
B-5 3 DB2 成功 + 1 失败 success: falsecount: 2Phase 2.1 后),error 含失败库名与部分成功提示
B-6 executeScript 注入失败 success: false不得 { success: true, count: 0 }
B-7 localStorage 成功 + cookies 失败 overallSuccess: falseresult.errorCookies: ...(换行分隔多项失败)

C. 回归与 UI

编号 标准
C-1 formatCleaningResult 成功路径不变
C-2 CleaningResult 失败时展示 result.error;含 \n 时多行可读(现有 leading-relaxed break-all 可接受)
C-3 reloadAfterClean=trueoverallSuccess=false 时不刷新页面(useStorageCleaner 现有逻辑)
C-4 Cookie 清理:domain 前导 . 剥离逻辑不变

D. 手动验收(扩展环境)

在 Chrome 加载 unpacked extension,选普通 HTTPS 页面:

编号 步骤 期望
D-1 页面写入 localStorage + IndexedDB,仅清 IndexedDB 成功提示或明确错误;DevTools → Application → IndexedDB 数据为空或库已删
D-2 打开 DevTools 保持 IndexedDB 面板,执行清理 若 blocked,显示中文提示;fallback 成功后数据不可见
D-3 勾选「清理后刷新」且全部成功 Toast「清理成功,即将刷新页面」,页面刷新
D-4 部分失败 不刷新;结果区红色展示错误详情

E. 代码质量

编号 标准
E-1 注入脚本内无重复 settled / timeout 逻辑(删除与 clear 各自封装)
E-2 错误文案仍为中文,含库名/store 名
E-3 any(测试文件除外)
E-4 Phase 1 合并后,P1 项在 PR 描述中标注「已修复」并附测试名

PR 检查清单

## 修复内容

- [ ] P1: transaction.oncomplete 后再 db.close
- [ ] P1: deleteDatabase settle 防竞态
- [ ] P2: 部分成功保留 count(可选)
- [ ] P2: 多 DB 混合测试
- [ ] P2: 错误信息 `\n` 分隔(可选)

## 验收

- [ ] npm run test / typecheck / lint 通过
- [ ] 新增测试覆盖 B-2、B-3、B-5
- [ ] 手动 D-1 ~ D-4 至少测 D-1、D-3

风险与边界说明

  1. fallback 清空 ≠ 删除库blocked 时只清 object store,库结构仍在;成功 count 表示「有效清理动作完成」,需在 UI/文档中说明(可选文案:「数据已清空,数据库结构可能仍存在」)。
  2. timeout 延迟 fallback:100~200ms 为经验值,无法完全消除竞态,只能降低概率;完全消除需浏览器不支持 abort delete 的前提下接受 best-effort。
  3. 类型扩展StorageCleanResult 加可选 count 为向后兼容;消费方用 'count' in result && result.count 判断即可。

相关文件索引

文件 说明
src/utils/storageCleaner.ts 核心清理逻辑
src/utils/__tests__/storageCleaner.test.ts 单元测试
src/types/storage.d.ts StorageCleanResult / CleaningResult 类型
src/pages/StorageCleaner/constants.ts 选项标签与键名
src/pages/StorageCleaner/useStorageCleaner.ts 清理流程编排
src/pages/StorageCleaner/components/CleaningResult.tsx 结果展示 UI