npm.io
0.1.1 • Published 6d ago

@codejoo/storage

Licence
Version
0.1.1
Deps
0
Size
193 kB
Vulns
0
Weekly
16

@codejoo/storage

English | 简体中文

localStorage / sessionStorage / IndexedDB 的轻量、类型安全封装,统一一套 API:TTL 与绝对过期、滑动续期、命名空间、可插拔序列化(含 Date / Map / Set / bigint)、可选混淆 codec、按需开启的内存缓存,以及绑定 key 的快捷访问器。同步后端返回值,异步的 IndexedDB 后端返回 Promise —— 由泛型区分,一套实现

  • 零依赖 · ESM · sideEffects: false · 附带按模块拆分、可 tree-shake 的产物(dist/esm/)。
  • 原生 API 不可用时(隐私模式、沙箱 iframe 等)自动回退到内存存储。

安装

pnpm add @codejoo/storage

快速开始

import { factory } from "@codejoo/storage";

const { ls, ss } = factory();

ls.set("token", "abc"); // localStorage
ls.get("token"); // "abc"
ls.get("missing", "default"); // "default"
ls.set("session", 1, 60_000); // 60s 后过期(ttl 毫秒)
ls.remove("token");
ls.clear();
ls.length; // 条目数

使用 IndexedDB(异步、容量大)。Idb 默认不打包,需自行导入:

import { factory, Idb } from "@codejoo/storage";

const { db } = factory({ db: new Idb() });

await db.set("user", { id: 1 }); // Promise<void>
await db.get("user"); // Promise<{ id: 1 }>

API

factory(options?)

返回 { ls, ss, db, destroy, setNamespace },分别是对 localStoragesessionStorage 和传入的 IndexedDB 实例的处理器。ls/ss同步db异步(返回 Promise)。三者共享同一套选项行为。destroy() 一次性释放所有层(清空各层 memo 读缓存并断开 db 的 IndexedDB 连接),返回 Promise不删除已落盘数据setNamespace(username?) 原地切换三层前缀(适合按账号隔离、登入/登出时调用)——已持有的引用自动生效;它只做隔离,不会清除上个命名空间的落盘数据。

参数 类型 必填 默认 说明
options BaseStorageOptions {} 实例级配置,应用于所有层。
BaseStorageOptions
选项 类型 必填 默认 说明
memoized boolean false 启用内存读缓存:写入双写、读取缓存优先、删除双删。按需开启(非全量镜像),内存随使用增长。
cloned boolean false 与 memo 缓存共享的对象按深拷贝(structuredClone)返回,隔离调用方修改对缓存的污染;默认共享引用(零开销)。
serialize (entity: StorageEntity) => string JSON.stringify 自定义 entity → 字符串序列化。
deserialize (raw: string) => StorageEntity JSON.parse 自定义字符串 → entity 反序列化(需与 serialize 配对)。
codeable boolean false 是否调用 codec。便于按环境(开发/生产)开关编解码。
codec Codec 对序列化字符串做编解码(混淆/压缩)。仅 codeable 为 true 时生效。
sliding boolean false 滑动过期:每次读命中按原始 ttl 续期(适合会话/登录态)。剩余寿命超过 90% 时跳过续期回写,高频读不产生写放大。
namespace string "" key 前缀(namespace:key),隔离同源下不同应用/模块。
raw boolean false 直接存原始值,跳过 entity 信封(无 ttl/codec)。用于与外部数据互通。
force boolean true 容量不足时清理过期项后重试写入,否则记录日志并放弃。仅同步后端生效。
readonly boolean false 只写一次:仅当键为空(不存在/已过期)才写入,否则丢弃本次写入。
enckey boolean false 是否对也混淆:配置了 codec 时,存储键经 codec 确定性混淆(隐藏明文键名)。仅用 codec 混淆键名、非安全防护;且需配合 codec,否则告警并降级为明文键。
onError (info: { op: "set"; key: string; error: unknown }) => void 写入失败回调(配额超限、force 重试仍失败等)。提供时取代默认的 console.error,使调用方可感知失败(set 返回 void,失败本不可见)。批量 set 下每个失败键各回调一次。
db AsyncStorage IndexedDB 实例(如 new Idb()),暴露为 factory().db。未传却使用 db 会抛错提示。
处理器方法(ls / ss / db

R<T> 在同步后端(ls/ss)为 T,在异步后端(db)为 Promise<T>

方法 返回 说明
get<T>(key) R<T | null> 读取;不存在 → null
get(key, defaultValue) R<T> 读取;不存在/已过期/解不开 → defaultValue
set(key, value, ttl?) R<void> 写入;ttl 毫秒。非法 ttl0 / 负数 / NaN / Infinity)告警并忽略——数据照常持久化(不会写入即被删,也不会变永不过期)。
set(key, value, options?) R<void> 写入;StorageOptions(ttl / expireAt / memoized)。开启 memo 现仅能经对象传入(set(k, v, { memoized: true }))。
remove(key) R<void> 删除(缓存 + 后端)。
get(keys, defaults?) R<元组> 批量读取:传键数组,返回等长元组;defaults 逐位对应并逐位联动类型(get(["a","b"],[1,false])[number, boolean]as const 保留字面量)。
set(keys, values, options?) R<void> 批量写入:逐位配对;第三参对全部键生效。values 偏短时缺位键跳过(告警)。
remove(keys) R<void> 批量删除。批量 get/set/remove 在键数组上逐键复用单键逻辑实现(异步后端为逐键一事务)。
keys() R<string[]> 本实例管辖范围内的全部逻辑键(已解密、去命名空间前缀)。
purge() R<void> 主动清理过期条目(仅管辖内、本库写入的数据)。平时为惰性过期,长期不被读取的过期数据靠它回收配额。
clear() R<void> 配置了 namespaceenckey 时仅清本实例管辖的键(不波及其他命名空间/外部数据);否则整库清空。
destroy() R<void> 释放资源:清空 memo 读缓存并断开可关闭后端(IndexedDB 连接)。保留已落盘数据。
key(index) R<string|null> 第 index 个逻辑键(已解密、去命名空间前缀)。
length R<number> 条目数(getter)。配置 namespaceenckey 时只数本实例管辖的键(与 keys()/clear() 作用域一致);否则返回后端全局条目数。
namespace string 命名空间前缀(形如 "ns:",无则为 "")。
setNamespace(ns?) void 原地切换前缀(如按 username 隔离账号);清空 memo 读缓存,已持有的引用自动生效。
StorageOptionsset 的按次选项)

仅以下三项按次生效(codec / sliding / raw 等其余配置均为实例级,见 BaseStorageOptions):

选项 类型 必填 默认 说明
ttl number 存活时间(毫秒,相对)。设置 expireAt = now + ttl。非法值(0 / 负数 / NaN / Infinity)告警并忽略,数据照常持久化且不带过期。
expireAt number | string | Date 绝对过期(时间戳/日期字符串/Date)。若早于当前且无法按 sliding + ttl 续期,告警并放弃写入。
memoized boolean 本次写入是否同步存入 memo 读缓存(覆盖实例级 memoized)。
fast(target, key)

绑定一个处理器和一个 key,返回 { get, set, remove },免去反复写 key。同步/异步返回类型跟随 target。值类型在 fast<V>(...) 指定一次即可。

参数 类型 必填 默认 说明
target ls / ss / db 处理器 factory() 返回的处理器。
key string 要绑定的键。
const token = fast<string>(ls, "token");
token.set("abc"); // 值必须是 string
token.get(); // string | null
token.get("def"); // string
token.remove();

访问器形态 —— SyncAccessor<V>(同步)/ AsyncAccessor<V>(异步):

方法 返回 说明
get() R<V | null> 读取。
get(defaultValue) R<V> 带默认值读取。
set(value, options?) R<void> 写入;options = ttl/memoized/选项。
remove() R<void> 删除。
lazy(target, key)

fast 类似,但返回一个 getter:首次调用才创建访问器并缓存。配合 /*#__PURE__*/ 注释,未使用的导出会被 tree-shake —— 适合在集中式 cache.ts 里登记大量 key。

export const token = /*#__PURE__*/ lazy<string>(ls, "token");
token().get(); // 首次使用才创建,之后复用
batchFast(target, keys)

一次绑定多个 key;返回以各 key 为属性名的对象,每个属性是对应 key 的快捷访问器(键名通过 const 泛型保留;值类型 V 对所有 key 统一,默认 unknown)。

const { token, user } = batchFast(ls, ["token", "user"]);
token.set("abc");
user.get();
JSONX

JSON 同名 API,额外可逆地支持 bigint / Date / Map / Set。方法不依赖 this,可直接作为 serialize/deserialize 传入。

方法 返回 说明
JSONX.stringify(value, space?) string 序列化,保留富类型。
JSONX.parse(text) any 反序列化,还原富类型。
const { ls } = factory({ serialize: JSONX.stringify, deserialize: JSONX.parse });
ls.set("x", { when: new Date(), ids: new Set([1n, 2n]) }); // 完整还原

不支持循环引用(沿用 JSON.stringify 行为 —— 会抛错)。

混淆 codec —— codec / codecBase64 / codecAtob

三个轻量混淆 codec(避免明文直接暴露在 devtools——不是强加密,password 随包发布)。均接受可选 password(不传用内置默认值);改 password 后旧数据无法解出——decode 返回 null,读取时按损坏清除。配合 { codeable: true, codec: codec("pw") } 使用。

导出 方案 适用
codec(password?)(默认) UTF-16 码元 10 位 XOR(零分支),输出 = 原文 + 1 码元 体积最优(零膨胀——中文配额仅为 base64 系 1/3)、延迟最低、无运行时要求
codecBase64(password?) 原生 Uint8Array.toBase64(base64url、无 padding、旋转),旧运行时自动回退 atob/btoa(同格式) 大体量 ASCII 吞吐最高(原生 SIMD);体积 +33%(中文 3 倍)
codecAtob(password?) 全程 TextEncoder + atob/btoa 行为处处一致(无特性检测);与 codecBase64 同格式、可互解

Codec 形态:

方法 返回 说明
encode(value) string 混淆字符串。
decode(value) string | null 还原;密钥不符/损坏时返回 null(不抛错)。
Idb(name?)

基于 IndexedDB 的异步 Storage 风格后端。不维护全量内存镜像(内存恒定、利于 GC)。传给 factory({ db })。IndexedDB 不可用或运行时 open() 失败时自动回退内存。

参数 类型 必填 默认 说明
name string "@codejoo/storage" IndexedDB 数据库名。

方法(均返回 Promise):get(key)set(key, value)remove(key)clear()key(index)keys()length()destroy()(关闭连接;保留数据)。处理器层的批量操作在这些单键方法上循环实现(异步后端逐键一事务)——不再有 getMany/setMany/removeMany 批量原语。

crossTab(handler, channel?)

独立插件(未用到即被 tree-shake)。仅在纯内存模式(原生 storage 不可用:隐私模式、沙箱 iframe)下启用:经 BroadcastChannelset/remove/clear 回放到同源其他标签页,保持各标签内存数据一致。原生 storage 可用时为空操作(数据本就共享);重复挂载同一 handler 也为空操作。本地写入先于广播生效,广播失败(值不可结构化克隆)仅告警;setNamespace 不参与同步,需各标签自行切换。返回停止函数。

import { factory, crossTab } from "@codejoo/storage";
const { ls } = factory();
const stop = crossTab(ls);
debug(handler)

独立辅助函数,经独立子路径@codejoo/storage/debug)发布——不在主入口中,单文件产物(dist/index.mjs / index.min.js)物理上不含它。读出 handler 全部条目的解密后明文,返回 { "命名空间:键": 值 } 快照(保留命名空间)。它是纯读取、无副作用——不会把快照写回存储,因此不会污染 keys()/length。用于查看以 codeable/enckey 写入的数据。

import { factory, codec } from "@codejoo/storage";
import { debug } from "@codejoo/storage/debug";

const { ls, db } = factory({ codeable: true, codec: codec("pw"), enckey: true });
debug(ls); // 同步 → { "key": value, ... }
await debug(db); // 异步后端 → Promise

说明

  • 同步 vs 异步 由后端类型经泛型决定:ls.get(k) 返回值,db.get(k) 返回 Promise。一套 proxy 实现同时服务两者。
  • db 的特性ttl / expireAt / codec / namespace / sliding / memoizeddb 同样生效(只是要 await)。force 容量清理目前仅对同步后端生效。
  • memo 按 factory() 实例隔离:每次 factory() 调用各自独立的内存读缓存,不同实例互不共享 memo(不会跨实例串读)。
  • Tree-shaking:包的 import 指向 dist/esm/(每模块一个文件)。配合 sideEffects: false,未用到的模块/导出会被打包器删除。

与原生 localStorage 的差异

  • 默认存的是 entity 信封{ value, createdAt, ... }),而非裸字符串。绕过本库直接用原生 localStorage.getItem 读到的是 JSON 信封,而非你的原始值(raw 模式除外)。
  • set 失败不抛配额错(原生会抛 QuotaExceededError)。配额满时仅记录日志 / 回调 onError 后放弃本次写入——需要感知写入失败请用 onError
  • lengthclear() 受命名空间作用域影响:配置 namespace/enckey 时仅覆盖本实例管辖的键,与原生的全局语义不同。
  • 过期是惰性的:过期项不读取也不主动删除,靠 purge() 或配额压力回收。

构建产物

路径 格式 用途
dist/esm/*.mjs 按模块 ESM 默认 import,可 tree-shake。
dist/index.mjs 单文件 ESM bundle 整体引入。
dist/index.min.js 压缩 ESM ./min 子路径。

测试

pnpm test

会在真实 Chromium(Playwright,vitest browser 模式)中跑完整集成套件(test/*.browser.test.ts),使用真实的 localStorage / sessionStorage / IndexedDB / BroadcastChannel(非 jsdom 模拟),覆盖同步后端、异步 IDB 事务、跨标签同步等真实浏览器行为。

交互式 playground 仍保留在 test/manual.html:运行 pnpm dev 后打开 /test/manual.html

许可

MIT

Keywords