@tracer-kit/core-storage
v0.1.0
Published
Generic IndexedDB record storage with bounded capacity and stable pagination.
Maintainers
Readme
@tracer-kit/core-storage
npm install @tracer-kit/core-storage模块角色
Core Storage 是 Tracer 的通用浏览器持久化层,位于 Domain 与 IndexedDB 之间。上游传入已经准备好的泛型记录,本模块负责存储、稳定分页、容量治理、淘汰和跨上下文变更通知。
本模块不知道日志消息、业务字段或加密算法。它只处理 StorageRecord<TStored>,因此也可被其他浏览器端记录场景复用。
flowchart LR
Domain[上游调用方] -->|StorageRecord T| Storage[Core Storage]
Storage --> Estimate[浏览器配额估算]
Storage --> Records[(IndexedDB records)]
Storage --> Meta[(IndexedDB capacity meta)]
Storage -->|结构化变更事件| Channel[BroadcastChannel]
Channel --> Consumer[其他页面或扩展]
Estimate -->|有效上限| Storage
Storage -->|超限时淘汰最旧记录| Records公共导出
createStorage<T>(options?):异步创建并初始化可用的 IndexedDB 存储实例。DEFAULT_STORAGE_OPTIONS:默认数据库名与容量配置。createStorageNotifier(channel, options?):创建基于BroadcastChannel的结构化变更通知器;单个监听器异常会被隔离并交给可选onError。validateCapacityConfig(config):校验容量配置。calculateEffectiveCapacity(config, totalBytes, estimate?):计算当前有效上限与低水位。- 类型:
RecordStorage、StorageOptions、StorageInput、StorageRecord、PutResult、StorageQuery、StorageListOptions、StorageIterateOptions、StoragePage、CapacityConfig、CapacityState、CapacityMeta、StorageDatabase、StorageNotifier、StorageChange等。
所有类型统一定义在 src/types.ts,并由 src/index.ts 使用 export * from './types' 全部公开。新增或删除类型时必须同步更新本节和 public-import 测试。
基本用法
import { createStorage } from '@tracer-kit/core-storage';
// TStored 由上游决定;Core Storage 不理解 value 内的业务字段。
const storage = await createStorage<MyValue>({
// 可选:内部恢复运行期错误后向诊断系统报告,不会从 put() 抛出。
onError: console.error,
});
// 上游不传 byteSize,Core Storage 使用统一策略计算完整记录大小。
const result = await storage.add(value);
// add() 自动生成 ID 和时间;结果与 put() 相同,不额外返回 ID 或时间。
// 普通读取返回有限的记录数组,默认 100 条,最多 500 条。
const records = await storage.list({ limit: 100 });
// 导入、恢复或幂等覆盖时使用高级写入接口。
await storage.put({ id: '1', occurredAt: Date.now(), value });
// 高级分页查询按 occurredAt + id 升序返回;下一页使用 page.nextCursor。
const page = await storage.query({ limit: 100 });
// 批量遍历由 iterate() 内部稳定翻页,不向调用方暴露 cursor。
for await (const record of storage.iterate({ pageSize: 100 })) {
consume(record);
}
// 宿主销毁时关闭 IndexedDB 与 BroadcastChannel。
storage.close();未传配置时使用数据库 tracer-storage,容量上限为 50 MiB,为同 Origin 的其他数据预留 10 MiB,淘汰低水位为有效容量的 80%。StorageOptions 支持逐项覆盖这些默认值,以及 estimate、notificationChannel、measureRecord 和 onError;测试或受控运行环境还可注入 idSource 与 clock,生产默认使用 crypto.randomUUID() 和 Date.now()。
内部实现
indexeddb-storage.ts 使用 Dexie 建立 records 和 meta 两个对象仓库。记录通过复合索引 [occurredAt+id] 排序,游标同时包含时间与 ID,从而在时间相同的情况下仍能稳定分页。
每次写入先读取浏览器配额估算,并取“配置上限”和“浏览器可用空间”中的较小值作为有效容量。超限时按最旧记录优先淘汰,直到低水位能容纳新记录。遇到 QuotaExceededError 时重新估算、淘汰并只重试一次。
flowchart TD
Put[put StorageInput] --> Measure[内部计算完整记录 byteSize]
Measure --> EstimateNow[读取配额并计算有效容量]
EstimateNow --> Oversized{单条记录超过上限?}
Oversized -->|是| Reject[拒绝并增加计数]
Oversized -->|否| Fits{当前空间足够?}
Fits -->|否| Evict[按 occurredAt + id 淘汰到低水位]
Fits -->|是| Persist[事务写入记录和容量元数据]
Evict --> Persist
Persist --> Notify[发布 record-committed]
Persist -. QuotaExceededError .-> Retry[重新估算并只重试一次]
Retry --> Persist写入成功后发布 { type: 'record-committed', id };删除或清空发布 { type: 'records-invalidated' }。通知只表示数据已变化,数据本身仍从 IndexedDB 查询。
本实例监听器逐个隔离,并允许返回 void | Promise<void>:同步抛错和异步拒绝都不会让已经提交的 put() 变成失败,也不会阻断后续监听器或 BroadcastChannel.postMessage()。发布过程不等待监听器 Promise,只挂拒绝处理以避免 unhandled rejection。监听器、频道和错误上报器自身的异常都由存储层吸收;配置的 onError 只用于留痕。
失败与生命周期
createStorage()只在数据库打开、Schema 和容量元数据初始化完成后 resolve;初始化失败或容量配置非法时 reject,并关闭半初始化实例。- 单条记录大于有效容量时返回
{ status: 'rejected' }并增加拒绝计数。 - 实例创建后的 IndexedDB、计量或配额重试失败返回
{ status: 'failed' },并通过可选onError留痕。 - 所有操作共享同一次并发安全的自动初始化,不要求上游严格安排初始化时序。
close()关闭 BroadcastChannel 和数据库连接,调用方销毁时必须执行。- IndexedDB 不可用时不会回退到 localStorage,避免容量、事务和查询语义悄然改变。
测试与维护
test/capacity.test.ts:容量计算与配置边界。test/indexeddb-storage.test.ts:读写、游标分页、淘汰、配额重试和通知。test/public-import.test.ts:公共出口可用性。
修改本模块时必须同步维护本 README 和相关测试;通知协议变化还要同步 DevTools 扩展及 docs/modules/core-storage.md。
