npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codejoo/storage

v0.1.2

Published

Unified, type-safe wrapper over localStorage / sessionStorage / IndexedDB with TTL, sliding expiry, namespaces, pluggable serialization, obfuscation codec and key-bound shortcuts.

Downloads

395

Readme

@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