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

locstore

v1.3.0

Published

A tiny TypeScript-first browser storage library with TTL, TimeSpan, sync/async APIs, adapters, and optional encryption.

Readme

locstore

locstore 是一个极简、类型友好、支持过期时间和可选加密的前端缓存库。

它的核心目标是让缓存操作足够自然:

store.set("user", user);
store.get<User>("user");
store.del("user");

异步存储统一使用 Async 后缀:

await store.setAsync("list", list);
await store.getAsync<ListItem[]>("list");

特性

  • 极简 API: setgetdelhasclear
  • 同步和异步方法边界清晰: set / setAsync
  • 内置 TimeSpan,不依赖 dayjs
  • 支持 localStoragesessionStoragecookiememoryurlhistory
  • 支持异步存储 indexedDBCache API
  • 支持自定义同步/异步 storage adapter
  • 支持全局加密、自定义加密和异步加密
  • 读取加密数据时自动解密,不需要手动传 decrypt: true
  • 保留旧版 StoreType 兼容写法
  • TypeScript 类型由源码生成

安装

npm install locstore
pnpm add locstore
yarn add locstore

快速开始

import store, { TimeSpan } from "locstore";

store.set("name", "张三");

const name = store.get<string>("name");

store.set("code", "123456", TimeSpan.fromSeconds(60));

const code = store.get<string>("code");

带默认值:

const theme = store.get("theme", "light");

删除:

store.del("name");

清空当前存储:

store.clear();

TimeSpan

TimeSpan 表示一段时间,而不是某个绝对日期。它的设计接近 C#,但推荐使用 TypeScript/JavaScript 常见的小驼峰命名。

TimeSpan.fromMilliseconds(500);
TimeSpan.fromSeconds(60);
TimeSpan.fromMinutes(5);
TimeSpan.fromHours(2);
TimeSpan.fromDays(7);

也支持 C# 风格别名:

TimeSpan.FromSeconds(60);
TimeSpan.FromMinutes(5);

字符串解析:

TimeSpan.parse("500ms");
TimeSpan.parse("10s");
TimeSpan.parse("5m");
TimeSpan.parse("2h");
TimeSpan.parse("7d");

组合时间:

const ttl = TimeSpan.from({
  days: 1,
  hours: 2,
  minutes: 30,
});

读取总时长:

const ttl = TimeSpan.fromMinutes(5);

ttl.totalMilliseconds; // 300000
ttl.totalSeconds;      // 300
ttl.totalMinutes;      // 5

设置过期时间

最简写法:

store.set("sms-code", "123456", TimeSpan.fromSeconds(60));

完整配置:

store.set("token", token, {
  ttl: TimeSpan.fromHours(2),
});

ttl 支持三种形式:

store.set("a", 1, { ttl: 1000 });                     // 毫秒
store.set("b", 2, { ttl: "10s" });                    // 字符串
store.set("c", 3, { ttl: TimeSpan.fromMinutes(5) });  // TimeSpan

兼容旧字段 expires

store.set("legacy", "value", {
  expires: TimeSpan.FromDays(1),
});

也可以传绝对时间:

store.set("deadline", "value", {
  expires: new Date("2026-12-31T23:59:59.000Z"),
});

支持的存储

同步存储:

| 名称 | 说明 | | --- | --- | | "local" | 使用 localStorage,默认存储 | | "session" | 使用 sessionStorage | | "cookie" | 使用 document.cookie | | "memory" | 内存存储,刷新页面后丢失 | | "url" | 使用 URL query 参数保存状态 | | "history" | 使用 history.state 保存状态 |

异步存储:

| 名称 | 说明 | | --- | --- | | "indexedDB" | 使用 IndexedDB,适合较大数据 | | "cache" | 使用 Browser Cache API,适合缓存响应类数据 |

示例:

store.set("user", user, { storage: "local" });
store.set("tab", tabState, { storage: "session" });
store.set("theme", "dark", { storage: "cookie" });
store.set("draft", draft, { storage: "memory" });

异步存储需要使用 Async 方法:

await store.setAsync("articles", articles, {
  storage: "indexedDB",
  ttl: TimeSpan.fromMinutes(30),
});

const articles = await store.getAsync<Article[]>("articles", {
  storage: "indexedDB",
});

同步方法遇到异步存储会抛出明确错误:

store.set("articles", articles, { storage: "indexedDB" });
// Error: Storage "indexedDB" is asynchronous. Use the Async API.

兼容 StoreType

推荐新写法使用字符串:

store.set("name", "张三", { storage: "session" });

旧写法仍然可用:

import store, { StoreType } from "locstore";

store.set("name", "张三", {
  storage: StoreType.SESSIONSTORAGE,
});

store.get<string>("name", {
  storage: StoreType.SESSIONSTORAGE,
});

创建实例

默认导出的 store 已经可以直接使用。复杂项目推荐用 createStore 创建自己的实例:

import { createStore, TimeSpan } from "locstore";

const userStore = createStore({
  name: "user",
  storage: "local",
  ttl: TimeSpan.fromDays(7),
});

userStore.set("profile", profile);
userStore.get<UserProfile>("profile");

使用 with 派生配置:

const sessionStore = store.with({ storage: "session" });
const cookieStore = store.with({ storage: "cookie" });

使用命名空间:

const authStore = store.use("auth");

authStore.set("token", token);
authStore.get<string>("token");

命名空间会隔离 key。上面的实际 key 类似:

auth:token

命名空间可以继续嵌套:

const adminAuthStore = store.use("admin").use("auth");

同步 API

set

store.set(key, value);
store.set(key, value, ttl);
store.set(key, value, options);

示例:

store.set("user", user);

store.set("code", "123456", TimeSpan.fromSeconds(60));

store.set("token", token, {
  ttl: "2h",
  storage: "session",
  encrypt: true,
});

get

store.get<T>(key);
store.get<T>(key, defaultValue);
store.get<T>(key, options);
store.get<T>(key, defaultValue, options);

示例:

const user = store.get<User>("user");

const theme = store.get("theme", "light");

const token = store.get<string>("token", {
  storage: "session",
});

const lang = store.get("lang", "zh-CN", {
  storage: "cookie",
});

del / remove

store.del("token");
store.remove("token"); // alias

指定存储:

store.del("token", { storage: "session" });

has

if (store.has("token")) {
  // ...
}

keys

const keys = store.keys();

指定存储:

const keys = store.keys({ storage: "session" });

clear

store.clear();

清空指定存储:

store.clear({ storage: "session" });

如果实例设置了命名空间,clear 只清理当前命名空间。

ttl

获取剩余过期时间,单位为毫秒:

const ttl = store.ttl("token");

返回值:

  • number: 剩余毫秒数
  • null: 不会过期
  • undefined: key 不存在或已经过期

expire

设置或刷新过期时间:

store.expire("token", TimeSpan.fromHours(1));

设置为永不过期:

store.expire("token", null);

meta

读取缓存元信息:

const meta = store.meta("token");

返回示例:

{
  key: "token",
  storage: "local",
  namespace: "auth",
  v: 1,
  createdAt: 1779780000000,
  updatedAt: 1779780000000,
  expiresAt: 1779783600000,
  encrypted: true,
  ttl: 3600000
}

update

基于旧值更新:

store.update<number>("count", (value) => (value ?? 0) + 1);

getOrSet

缓存不存在时创建:

const config = store.getOrSet("config", () => {
  return createDefaultConfig();
});

setMany / getMany / delMany

store.setMany({
  token: "a",
  refreshToken: "b",
});

const values = store.getMany<string>(["token", "refreshToken"]);

store.delMany(["token", "refreshToken"]);

clearExpired

清理已经过期的缓存:

const count = store.clearExpired();

返回被清理的数量。

异步 API

异步方法和同步方法一一对应,统一加 Async 后缀。

await store.setAsync("key", value);
await store.getAsync<T>("key");
await store.delAsync("key");
await store.hasAsync("key");
await store.keysAsync();
await store.clearAsync();
await store.ttlAsync("key");
await store.expireAsync("key", TimeSpan.fromMinutes(10));
await store.metaAsync("key");

批量异步方法:

await store.setManyAsync({
  a: 1,
  b: 2,
});

const values = await store.getManyAsync<number>(["a", "b"]);

await store.delManyAsync(["a", "b"]);

setAsync/getAsync 可以操作同步存储,也可以操作异步存储:

await store.setAsync("user", user, { storage: "local" });
await store.setAsync("articles", articles, { storage: "indexedDB" });

加密

写入时声明 encrypt: true,读取时自动解密。

import { createStore } from "locstore";

const secureStore = createStore({
  crypto: {
    secret: "your-secret",
  },
});

secureStore.set("token", token, {
  encrypt: true,
});

const token = secureStore.get<string>("token");

如果写入时设置了 encrypt: true,但没有配置加密器或密钥,会抛出错误:

No crypto provider configured.

也就是说,locstore 保留默认 AES 能力,但不会内置任何默认密钥。必须由用户显式传入 secret,或提供自定义 encrypt / decrypt 方法后,才能启用加密。

安全边界

locstore 的加密能力用于降低本地存储中明文暴露的风险,但不能提供绝对安全。

只要数据需要在浏览器中被解密并使用,拥有前端运行环境控制权的攻击者理论上就可能获取明文或调用解密流程。

对于真正敏感的数据,请优先由服务端保存;并由后端完成权限校验、过期控制和风控。

自定义同步加密

const store = createStore({
  crypto: {
    secret: "app-secret",

    encrypt(text, ctx) {
      return customEncrypt(text, ctx.secret!);
    },

    decrypt(text, ctx) {
      return customDecrypt(text, ctx.secret!);
    },
  },
});

store.set("profile", profile, {
  encrypt: true,
});

const profile = store.get<UserProfile>("profile");

ctx 包含当前 key、存储类型、命名空间、密钥和自定义元信息:

type CryptoContext = {
  key: string;
  storage: "local" | "session" | "cookie" | "memory" | "url" | "history" | "indexedDB" | "cache" | "custom";
  namespace?: string;
  secret?: string;
  meta?: Record<string, unknown>;
};

自定义异步加密

异步加密只能通过 Async 方法使用。

const store = createStore({
  cryptoAsync: {
    async encrypt(text, ctx) {
      return webCryptoEncrypt(text, ctx.secret!);
    },

    async decrypt(text, ctx) {
      return webCryptoDecrypt(text, ctx.secret!);
    },
  },
});

await store.setAsync("token", token, {
  encrypt: true,
});

const token = await store.getAsync<string>("token");

规则:

  • set/get 只使用同步加密器
  • setAsync/getAsync 优先使用异步加密器
  • 如果没有异步加密器,setAsync/getAsync 可以使用同步加密器
  • 加密数据读取失败时返回默认值,不会让业务直接崩溃

Cookie 配置

store.set("theme", "dark", {
  storage: "cookie",
  ttl: TimeSpan.fromDays(7),
  cookie: {
    path: "/",
    sameSite: "lax",
    secure: true,
  },
});

支持字段:

type CookieOptions = {
  path?: string;
  domain?: string;
  sameSite?: "strict" | "lax" | "none";
  secure?: boolean;
};

自定义存储 Adapter

同步 adapter:

import type { SyncAdapter } from "locstore";

const adapter: SyncAdapter = {
  type: "sync",
  name: "custom",
  get(key) {
    return myStorage.get(key);
  },
  set(key, value) {
    myStorage.set(key, value);
  },
  del(key) {
    myStorage.delete(key);
  },
  clear(prefix) {
    myStorage.clear(prefix);
  },
  keys(prefix) {
    return myStorage.keys(prefix);
  },
};

const store = createStore({
  storage: adapter,
});

异步 adapter:

import type { AsyncAdapter } from "locstore";

const adapter: AsyncAdapter = {
  type: "async",
  name: "custom",
  async get(key) {
    return await myAsyncStorage.get(key);
  },
  async set(key, value) {
    await myAsyncStorage.set(key, value);
  },
  async del(key) {
    await myAsyncStorage.delete(key);
  },
  async clear(prefix) {
    await myAsyncStorage.clear(prefix);
  },
  async keys(prefix) {
    return await myAsyncStorage.keys(prefix);
  },
};

const store = createStore({
  storage: adapter,
});

await store.setAsync("key", "value");

内部数据格式

locstore 会统一保存为 payload:

type CachePayload<T = unknown> = {
  v: 1;
  value: T;
  createdAt: number;
  updatedAt: number;
  expiresAt: number | null;
  encrypted: boolean;
  meta?: Record<string, unknown>;
};

如果 encrypt: true,默认只加密 value,保留基础元信息用于判断过期和调试。

SSR 和降级

在没有浏览器 API 的环境中:

  • localStorage / sessionStorage 不可用时会降级到内存存储
  • cookie 不可用时会降级到内存存储
  • indexedDB / Cache API 不可用时会降级到内存存储

这让库在 SSR、测试环境、隐私模式等场景里更稳定。

从旧版迁移

旧写法:

import store, { StoreType, TimeSpan } from "locstore";

store.set("name", "张三", {
  storage: StoreType.LOCALSTORAGE,
  expires: TimeSpan.FromDays(1),
  encrypt: true,
});

store.get<string>("name", {
  decrypt: true,
});

推荐新写法:

import store, { TimeSpan } from "locstore";

store.set("name", "张三", {
  storage: "local",
  ttl: TimeSpan.fromDays(1),
  encrypt: true,
});

store.get<string>("name");

变化点:

  • expires 推荐改为 ttl
  • TimeSpan.FromDays 推荐改为 TimeSpan.fromDays
  • decrypt 不再需要,读取时会自动根据 payload 解密
  • remove 仍然可用,但推荐使用 del
  • StoreType 仍然可用,但推荐使用字符串存储名

开发

安装依赖:

pnpm install

类型检查:

pnpm typecheck

测试:

pnpm test

构建:

pnpm build

License

MIT