#TypeScript#DevOps

浏览器存储配额与驱逐恢复:数据分级、写入准入和重建证明

navigator.storage.estimate 只是近似值,persist 返回 true 也不等于用户数据永不消失。本文为 Cache、IndexedDB 与 OPFS 建立 origin 级预算、驱逐检测和分级恢复合同。

本文要点

  • 主题聚焦:TypeScript、DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年6月2日·更新于 2026年7月20日

一个 origin 的 Cache Storage、IndexedDB、OPFS 等通常共享浏览器管理的存储预算。navigator.storage.estimate() 返回的是估计值,配额会随设备空间、浏览器策略和隐私填充变化;persist() 获批也挡不住用户主动清除。把“还剩 500 MB”当预留承诺,或者在 QuotaExceededError 后随机删库,都会先丢最难恢复的数据。IndexedDB 离线草稿恢复管理单个数据库迁移,本篇处理 origin 级数据类别、预算和全失恢复。

先声明哪些字节可以牺牲

发布 browserStoragePolicy.v1,按账号与 storage namespace 固定数据 class、authoritative source、最大/目标字节、最低 reserve、应用清理顺序、完整性规则、恢复 reader、保留期和用户承诺。至少区分:未同步用户事实、可从服务端恢复的镜像、可重算派生索引、版本化应用资产和临时 telemetry。除未同步事实外,各类也只能在合同前置条件满足时由应用回收;浏览器或用户仍可能清除整个 best-effort bucket,NEVER_AUTOMATIC 只约束应用自己的 shedding,不能伪装成 UA 不驱逐保证。

yaml
schema: browserStoragePolicy.v1
revision: 9
namespace: app-origin@3
reserveBytes: 104857600
classes:
  unsynced-user-data: {eviction: NEVER_AUTOMATIC, source: local-only}
  telemetry: {appEvictionOrder: 10, source: disposable}
  derived-index: {appEvictionOrder: 20, source: rebuild}
  sw-assets: {appEvictionOrder: 30, condition: UNREFERENCED_RELEASE}
  server-snapshot: {appEvictionOrder: 40, condition: SOURCE_GENERATION_VERIFIED}

estimate().usage 只参与保守准入,不作为记账真相。大对象写入前按序列化后的上界、临时副本和 reserve 评估;结果接近阈值就拒绝缓存资产或进入 shedding。每次真实写仍捕获 QuotaExceededError,不能因刚估算过就假定成功。状态为 PROBING -> READY -> PRESSURE -> SHEDDING -> DEGRADED -> RECOVERING -> READY;发现关键本地事实缺失进入 LOCAL_LOSS,不可伪装成空账号。

跨后端没有免费全局事务

Cache、IndexedDB 与 OPFS 之间不存在应用可依赖的统一提交。重建一代本地镜像时,先按内容摘要写不可变对象,在各自后端验证,再于 canonical meta store 发布 storageGeneration、inventory root 和 server watermark;reader 只采用已发布 generation。pointer 提交前崩溃留下的对象是可回收 orphan,不能被半成品 reader 猜中。

压力清理严格按 manifest 顺序:临时 telemetry、无引用派生索引、经Service Worker 发布激活证明已无 release 引用的旧 cache,最后才是仍可从已验证 source generation 重取的 server snapshot。清理器只碰带本应用 namespace 和 digest 的对象,不遍历后删除未知 cache。未同步 outbox、草稿和用户导入永不由应用自动牺牲;若它们占满空间,停止新写、尝试同步并提供受控导出,而不是声称“优化完成”。Local-first AI 应用中的本地事实必须向用户说明是否另有副本。

多个标签同时清理或恢复时,以Web Locks Leader 围栏选本机协调者,并让 canonical generation 拒绝旧 owner 的 pointer 更新。Web Lock 不可用时可以重复下载不可变对象,但只能有一个 generation commit;不要让两个页面互删对方刚写的资产。

把驱逐当作冷启动,而不是异常空数组

启动时验证 account marker、policy revision、active generation、inventory root 与关键 object digest。整个 origin 被驱逐或用户清除时,这些标记会一起消失;应用从已认证服务端取得 recovery manifest 和 watermark,先恢复最小可用事实,再后台重建派生数据。只有本地存在的未同步内容若消失,结果必须是 UNRECOVERABLE_LOCAL_LOSS,展示既有备份/导入路径,不能生成一个空 store 后覆盖服务器或告诉用户“已同步”。

navigator.storage.persisted()、estimate 与 usage details 记录为诊断信号,不进入“不丢数据”的承诺。恢复请求使用稳定 snapshot/generation,网络重试只重取相同摘要;服务端数据变化产生新 recovery generation,不覆盖已验证到一半的旧根。安全文件上传管道中的隔离对象也不能未经确认就算本地权威副本。

故障套件在每种对象写入和 pointer CAS 前后注入 quota 错误、页面终止、并发清理、estimate 突降、persist 拒绝、旧 owner 晚到、源站断线、对象摘要损坏、账号切换、完整 origin reset 和用户仅删除某类应用数据。断言未同步事实被自动删除为 0,半 generation 可见为 0,跨账号恢复为 0,重复恢复得到同一 inventory root。

发布门要求分类覆盖本地字节至少 99%、关键数据无 source 的条目 100% 明示、压力下 reserve 跌破硬线的新缓存接受数为 0、黄金 5 GB fixture shedding 后关键 root 不变、完整 reset 恢复成功率 100%、本地-only 损失误报“已恢复”为 0。先只盘点与估算,再启用派生类清理,最后启用服务端恢复。异常时关闭自动 shedding/rebuild,但保留写入准入和只读导出;旧 reader 只能读取声明兼容 generation,绝不能用 deleteDatabase() 作为回滚。

延伸阅读

Related

相关推荐