SyncState V1/V2 迁移
v3.7.0 完成了 SyncState 从 V1 嵌套结构到 V2 扁平结构的迁移,消除了 V1/V2 双写问题,并引入 V1 facade 保证向后兼容。
为什么要迁移
旧版 SyncState 使用嵌套结构存储同步状态:
V1(历史结构,v3.17 起 github 分支不再迁移,存量静默丢弃): { linuxdo: {...}, github: { meta, stars, repos, forks, gists }, bookmarks: {...} }这带来了两个问题:
- V1/V2 双写:新代码写 V2 格式,旧代码仍写 V1 格式,导致数据不一致。
- 来源子类型嵌套:旧
github.stars、github.repos等子类型曾嵌套在同一对象下,无法独立管理同步周期(GitHub 源已于 v3.17 移除)。
V2 扁平结构
V2 将所有来源类型展平为独立 key:
V2: { version: 2, sources: { linuxdo: {...}, bookmark: {...}, zhihu: {...}, generic: {...} } }
> 注(v3.15.0):`rss` 源已移除(存量由 `_load` 自动剪枝);注(v3.17):`github-*` 源已移除(存量由 `_load` 自动剪枝;历史版本结构曾含 `github: {...}` / `github-stars/...`)。每个来源类型独立管理自己的 lastSyncTime、lastSyncCount、lastError 等字段。
自动迁移
首次访问时,SyncStateV2._load() 检测到 V1 数据后自动执行 _migrateV1toV2():
- 将旧
github.meta/stars/repos/forks/gists展平为github-meta/github-stars/...(v3.17 起不再迁移,存量静默丢弃)。 - 将
bookmarks重命名为bookmark(单数形式,与其他来源类型一致)。 - 迁移是幂等的,重复执行不会产生副作用。
V1 facade
为了消除双写,V1 的公共 API 被替换为 V2 的 facade 代理层。所有现有代码调用 SyncState.getLinuxDoState() 等方法时,实际委托给 SyncStateV2(updateGitHubState 已于 v3.17 删除,仅留兼容壳):
SyncState.getLinuxDoState() → SyncStateV2.getSourceState("linuxdo")
SyncState.updateGitHubState(subtype, data) →(v3.17 已删除;兼容壳静默降级,不再迁移)这样,旧代码无需修改即可自动使用 V2 存储。
facade 委托完整性(v3.14.0)
facade 必须完整委托 V2 的全部公共方法。v3.14.0 曾因缺失 getSourceState 委托导致 renderSyncChainStatus 抛 TypeError → loadConfig 中断 → 同步链状态永不回显。修复后 facade 补全 getSourceState/updateSourceState/forceFlush 委托,并新增契约测试(tests/sync-state-facade.test.js)枚举 V2 公共方法断言 facade 存在——新增 V2 公共方法时若未同步委托,测试立即失败。
写入优化
queueMicrotask 合并
SyncStateV2._save() 使用 queueMicrotask 将同一事件循环 tick 内的多次写入合并为一次 GM_setValue 调用,减少 IPC 开销。回退链:queueMicrotask → setTimeout(flush, 0) → 同步写入。
DedupStore 批量模式
DedupStore 支持 beginBatch(sourceType) / endBatch() 批量模式。批量模式下,isDuplicate 和 markSeen 只操作内存缓存,endBatch() 时一次性写入 GM_setValue。SyncCoordinator.sync() 在同步循环中使用批量模式,避免每条记录都触发 IPC。
批量模式账本一致性(v3.14.10–11)
批量模式曾有三类账本一致性缺陷,v3.14.10–11 修复:
- batch 槽残留(#17):刷新工作区零命中时
reconcile只beginBatch不endBatch,后续markSeen仅驻内存,重载后账本丢失 → 自动去重/待导出复发。修复:无条件endBatch,并对齐 LinuxDoAdapter 与导出账本的裸topicId键空间。 - unmark 墓碑(#19):
endBatchrebase 会把已unmarkSeen的键从磁盘复活(dirtyKeys + deleted tombstones 未区分)。修复:rebase 不再复活墓碑键。 - wipe 复活(#18):
clearSeen+endBatch会把已清空的键从磁盘 rebase 回来。修复:清空后不再从磁盘回读。
配套:Zhihu/Generic clipper 成功后 markSeen;RSS allow_duplicates 用 feed 感知的 DedupStore 键;Zhihu/Generic clipper 成功后 markSeen;去重键统一走 normalizeDedupUrl(含 Discourse /t/slug/id → /t/id 归一)。
基线重置(v3.13.0)
SyncState.resetSourceState(sourceType) 将指定源的增量基线恢复为默认(watermark=null、lastOutcome=idle),下次同步退化为全量扫描。工作区洞察(统一同步中心)每张来源卡提供「重置基线」按钮。
遗留问题
V1 facade 代理了 V2 的私有方法(_clone、_load、_save),破坏了封装。外部代码可能通过 V1 facade 访问 V2 内部实现。建议:
- 移除 V1 facade 中对 V2 私有方法的代理。
- 将需要外部访问的方法提升为 V2 的公共 API。
- 或将 V1 facade 标记为
@deprecated,引导调用方迁移到 V2。