# cache **Repository Path**: brm-zero-x/cache ## Basic Information - **Project Name**: cache - **Description**: 轻量、框架无关的前端缓存包。适合缓存接口响应、配置、字典、语言包等浏览器端数据,支持持久化存储、TTL、stale-while-revalidate、请求并发去重、批量失效和容量清理。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-06-12 - **Last Updated**: 2026-06-12 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # @brmtech/cache `@brmtech/cache` 是一个轻量、框架无关的前端缓存客户端。它适合缓存接口响应、配置、字典、语言包等浏览器端数据,支持持久化存储、TTL、stale-while-revalidate、相同 key 请求去重、批量失效、容量清理、订阅通知和多种 storage adapter。 它不绑定 Vue、React、Vite、Next.js 或任何请求库。你只需要提供一个返回 `Promise` 的 `fetcher`。 ## 安装 ```bash pnpm add @brmtech/cache # 或者 npm install @brmtech/cache yarn add @brmtech/cache ``` ## 快速开始 ```ts import { createCache, indexedDBAdapter } from '@brmtech/cache' interface GameList { list: Array<{ id: string; name: string }> total: number } const cache = createCache({ namespace: 'my-app', version: '1.0.0', storage: indexedDBAdapter({ dbName: 'MY_APP_CACHE' }), defaultPolicy: { ttl: '30m', staleTime: '5m', staleWhileRevalidate: true, allowStaleOnError: true, }, }) export function getGameList(page: number) { const key = cache.key('/api/games', { page, size: 20 }, { scope: 'country:br' }) return cache.wrap( key, () => fetch(`/api/games?page=${page}&size=20`).then(res => res.json()), { tags: ['game'], scope: 'country:br', onUpdate: fresh => { // stale 后台刷新成功且数据发生变化时触发,可以在这里更新页面状态。 renderGameList(fresh) }, }, ) } ``` 这段代码会带来这些行为: - 首次调用时执行 `fetcher`,并把结果写入 IndexedDB。 - 后续同一个 key 在 TTL 内会优先返回缓存。 - 数据超过 `staleTime` 但未超过 `ttl` 时,默认先返回旧数据,同时后台刷新。 - 后台刷新拿到新数据后,如果内容发生变化,会触发 `onUpdate` 和 `subscribe`。 - 多个相同 key 的并发 `wrap()` 调用只会执行一次 `fetcher`。 - `fetcher` 失败时,如果存在旧缓存且 `allowStaleOnError` 为 `true`,会返回旧缓存兜底。 ## 公开导出 ### 可导入 API ```ts import { createCache, buildCacheKey, memoryAdapter, indexedDBAdapter, localStorageAdapter, sessionStorageAdapter, hybridAdapter, jsonSerializer, } from '@brmtech/cache' ``` | 导出 | 说明 | | --- | --- | | `createCache(options?)` | 创建缓存客户端,返回 `CacheClient`。 | | `buildCacheKey(path, params?, options?)` | 生成稳定 key 的纯函数,返回 `{ key, path, namespace?, version?, scope? }`。 | | `memoryAdapter(options?)` | 内存 adapter,适合测试、SSR 临时缓存、不需要持久化的场景。 | | `indexedDBAdapter(options?)` | IndexedDB adapter,浏览器端推荐持久化方案。 | | `localStorageAdapter(options?)` | localStorage adapter,适合小体积 fallback。 | | `sessionStorageAdapter(options?)` | sessionStorage adapter,适合当前浏览器会话内缓存。 | | `hybridAdapter(adapters, name?)` | 组合多个 adapter,读时按顺序查找,写时至少一个成功即成功。 | | `jsonSerializer` | 默认 JSON serializer,使用 `JSON.stringify` 和 `JSON.parse`。 | ### 可导入类型 ```ts import type { CacheClient, CacheOptions, CachePolicy, CacheSetOptions, CacheWrapOptions, CacheGetOptions, CacheKeyOptions, CacheCleanupOptions, CacheClearOptions, CacheEntry, CacheEntryFilter, CacheStorageAdapter, CacheSerializer, CacheStats, CacheWriteContext, CacheFetcher, CacheSubscribeListener, Duration, } from '@brmtech/cache' ``` adapter 的 options 类型会出现在函数参数提示里,但当前入口没有把 `MemoryAdapterOptions`、`IndexedDBAdapterOptions`、`LocalStorageAdapterOptions`、`SessionStorageAdapterOptions` 作为可直接 import 的命名类型导出。需要复用时可以这样写: ```ts type IDBOptions = Parameters[0] ``` ## 核心概念 | 概念 | 说明 | 示例 | | --- | --- | --- | | `key` | 缓存唯一标识。短 key 会自动加上当前实例的 `namespace/version` 前缀。 | `home:list` | | `namespace` | 项目或模块隔离维度。`clear()`、`cleanup()` 默认只处理当前 namespace。 | `admin`、`h5` | | `version` | 数据版本隔离维度,适合放应用版本或构建 hash。 | `1.0.0` | | `scope` | 用户、租户、地区等业务维度。 | `user:123`、`country:br` | | `tags` | 批量失效分组。一个 entry 可以有多个 tag。 | `product`、`activity` | | `ttl` | 最大可用时间,超过后视为 expired。`null` 表示不设置过期时间。 | `30m` | | `staleTime` | 新鲜时间,超过后视为 stale,但还未 expired。`null` 表示没有 stale 阶段。 | `5m` | 时间线: ```text updatedAt ------------- staleAt ------------- expiresAt fresh stale expired ``` | 状态 | `get()` | `getFresh()` | `wrap()` 默认行为 | | --- | --- | --- | --- | | fresh | 返回缓存 | 返回缓存 | 返回缓存 | | stale | 返回缓存 | 返回 `null` | 返回旧缓存并后台刷新 | | expired | 返回 `null` | 返回 `null` | 执行 `fetcher`,失败时可用旧缓存兜底 | 注意:如果 `wrap()` 的 `staleWhileRevalidate` 为 `false`,stale 数据在过期前仍会被直接返回,不会自动刷新。想要强制拉取最新数据,请调用 `refresh()`。 ## 创建缓存实例 ```ts import { createCache, indexedDBAdapter } from '@brmtech/cache' export const cache = createCache({ namespace: 'my-app', version: '1.0.0', storage: indexedDBAdapter({ dbName: 'MY_APP_CACHE', storeName: 'entries', }), memory: { enabled: true, maxEntries: 100, }, defaultPolicy: { ttl: '10m', staleTime: '2m', staleWhileRevalidate: true, allowStaleOnError: true, maxEntryBytes: 500 * 1024, }, cleanup: { maxEntries: 500, maxBytes: 50 * 1024 * 1024, }, touchThrottleMs: 5000, deleteExpiredOnGet: false, }) ``` ### `CacheOptions` | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `namespace` | `string` | 无 | 当前实例的命名空间。短 key 会解析成 `namespace::key` 或 `namespace::version::key`。 | | `version` | `string` | 无 | 当前实例的数据版本。短 key 会带上 version。 | | `storage` | `CacheStorageAdapter` | 有 IndexedDB 时用 `indexedDBAdapter()`,否则用内存 adapter | 主存储层。 | | `memory.enabled` | `boolean` | `true` | 是否启用内存一级缓存。 | | `memory.maxEntries` | `number` | `100` | 内存一级缓存最多保留多少条,超过后按 LRU 淘汰。 | | `defaultPolicy.ttl` | `Duration \| null` | `'5m'` | 默认 TTL。 | | `defaultPolicy.staleTime` | `Duration \| null` | `null` | 默认 stale 时间。 | | `defaultPolicy.staleWhileRevalidate` | `boolean` | `true` | `wrap()` 命中 stale 数据时是否后台刷新。 | | `defaultPolicy.allowStaleOnError` | `boolean` | `true` | `fetcher` 失败时是否返回旧缓存。 | | `defaultPolicy.maxEntryBytes` | `number` | 无 | 单条缓存最大字节数,超过则跳过写入。 | | `cleanup.maxEntries` | `number` | 无 | `cleanup()` 后最多保留多少条未过期数据。 | | `cleanup.maxBytes` | `number` | 无 | `cleanup()` 后最多保留多少字节未过期数据。 | | `cleanup.maxEntryBytes` | `number` | 无 | 全局默认单条大小限制,会参与写入策略合并。 | | `cleanup.interval` | `Duration` | 无 | 类型中保留的字段,当前核心不会自动按 interval 调度 `cleanup()`。 | | `cleanup.eviction` | `'lru'` | 无 | 类型中只允许 `lru`,当前容量清理实际按 LRU 执行。 | | `cleanup.quotaThreshold` | `number` | 无 | 类型中保留的字段,当前核心逻辑没有读取它。 | | `serializer` | `CacheSerializer` | `jsonSerializer` | 序列化器。默认要求数据可 JSON 序列化。 | | `shouldCache` | `(value, context) => boolean \| Promise` | 无 | 写入前判断是否允许缓存,返回 `false` 会跳过写入。 | | `touchThrottleMs` | `number` | `5000` | 命中缓存后写回 `lastAccessedAt/hitCount` 的节流间隔。 | | `deleteExpiredOnGet` | `boolean` | `false` | `get()` 发现 expired 数据时是否立即删除。默认保留,便于请求失败时兜底。 | 策略合并优先级从低到高是:内置默认策略、`cleanup.maxEntryBytes`、`defaultPolicy`、单次调用 options。 ## 时间参数 `Duration` `Duration` 可以是数字,也可以是带单位的字符串。数字表示毫秒。 ```ts type Duration = number | `${number}ms` | `${number}s` | `${number}m` | `${number}h` | `${number}d` ``` 可用示例: ```ts '500ms' '10s' '5m' '2h' '7d' 60000 ``` 字符串格式不合法时会抛出错误或导致写入失败,建议只使用 `ms`、`s`、`m`、`h`、`d`。 ## key 生成 ### `cache.key(path, params?, options?)` 生成稳定 key,并在当前实例中记录 `path/namespace/version/scope` 元信息,方便后续 `set()` 和 `invalidate({ pathPrefix })` 使用。 ```ts const key = cache.key('/api/list', { size: 20, page: 1 }) // my-app::1.0.0::/api/list?page=1&size=20 ``` 签名: ```ts key( path: string, params?: Record | URLSearchParams, options?: CacheKeyOptions, ): string ``` `CacheKeyOptions`: | 参数 | 类型 | 说明 | | --- | --- | --- | | `namespace` | `string` | 覆盖当前实例的 namespace。 | | `version` | `string` | 覆盖当前实例的 version。 | | `scope` | `string` | 加入 key 的业务维度。 | | `keepEmptyString` | `boolean` | 是否保留空字符串参数,默认过滤空字符串。 | 规则: - query 参数按参数名排序,同名参数再按值排序。 - `null`、`undefined` 会被忽略。 - 空字符串默认忽略,传 `keepEmptyString: true` 后保留。 - 数组会展开为多个同名 query 参数。 - 参数名和值会使用 `encodeURIComponent` 编码。 - 普通对象不会深度序列化,会走 `String(value)`,不建议把嵌套对象直接放进 params。 - `path` 已经带 query 时,新参数会用 `&` 追加。 ### `buildCacheKey(path, params?, options?)` 纯函数版本,只生成 key,不依赖缓存实例。 ```ts import { buildCacheKey } from '@brmtech/cache' const built = buildCacheKey('/api/list', { page: 1 }, { namespace: 'my-app', version: '1.0.0', }) console.log(built.key) console.log(built.path) ``` 返回值: ```ts { key: string path: string namespace?: string version?: string scope?: string } ``` 如果你用 `buildCacheKey()` 生成 key,再直接 `cache.set(built.key, value)`,当前 cache 实例不会自动记住 `built.path`。需要依赖 `pathPrefix` 失效时,请使用 `cache.key()`,或写入时手动传 `path: built.path`。 ## CacheClient API ### `cache.set(key, value, options?)` 写入一条缓存。 ```ts const success = await cache.set('config', config, { ttl: '1h', tags: ['config'], metadata: { source: 'bootstrap' }, }) ``` ```ts set(key: string, value: T, options?: CacheSetOptions): Promise ``` `CacheSetOptions`: | 参数 | 类型 | 说明 | | --- | --- | --- | | `ttl` | `Duration \| null` | 当前 entry 的 TTL。 | | `staleTime` | `Duration \| null` | 当前 entry 的 stale 时间。 | | `maxEntryBytes` | `number` | 当前 entry 最大字节数,超过会跳过写入。 | | `staleWhileRevalidate` | `boolean` | 策略字段,对 `wrap()` 有意义;单独 `set()` 不会把这个行为写入 entry。 | | `allowStaleOnError` | `boolean` | 策略字段,对 `wrap()`、`refresh()` 的错误兜底有意义。 | | `path` | `string` | 原始资源路径,用于 `invalidate({ pathPrefix })`。使用 `cache.key()` 时通常不用手动传。 | | `namespace` | `string` | 覆盖 entry 的 namespace。 | | `version` | `string` | 覆盖 entry 的 version。 | | `scope` | `string` | entry 的业务作用域。 | | `tags` | `string[]` | entry 的标签。 | | `metadata` | `Record` | 自定义元信息,会存入 entry。 | 返回 `true` 表示写入成功。返回 `false` 表示写入被跳过或失败,例如超过 `maxEntryBytes`、`shouldCache` 返回 `false`、序列化失败或 adapter 写入失败。 ### `cache.get(key, options?)` 读取未过期缓存。stale 但未 expired 的数据也会返回。 ```ts const config = await cache.get('config') ``` ```ts get(key: string, options?: CacheGetOptions): Promise ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | `allowStale` | `boolean` | 名称是 stale,但当前实现效果是允许返回 expired 缓存。普通 stale 缓存默认就会返回。请谨慎使用。 | | `maxAge` | `Duration` | 只接受 `updatedAt` 距今不超过该时间的数据。超过后返回 `null`。 | ```ts await cache.get('config', { maxAge: '10m' }) await cache.get('config', { allowStale: true }) ``` ### `cache.getFresh(key, options?)` 只读取 fresh 缓存。只要 entry 已经 stale 或 expired,就返回 `null`。 ```ts const cached = await cache.getFresh('home:list') ``` ```ts getFresh(key: string, options?: CacheGetOptions): Promise ``` `getFresh()` 中只有 `maxAge` 有实际意义。即使传 `allowStale: true`,也不会返回 stale 数据。 ### `cache.wrap(key, fetcher, options?)` 请求缓存主 API。它会先查缓存,必要时执行 `fetcher`,成功后写入缓存。 ```ts const data = await cache.wrap( 'home:list', () => fetch('/api/home').then(res => res.json()), { ttl: '30m', staleTime: '5m', tags: ['home'] }, ) ``` ```ts wrap( key: string, fetcher: () => Promise, options?: CacheWrapOptions, ): Promise ``` `CacheWrapOptions` 继承 `CacheSetOptions`,额外支持: | 参数 | 类型 | 说明 | | --- | --- | --- | | `onUpdate` | `(value: T) => void` | stale 后台刷新成功且数据变化时触发。初次请求、前台刷新和 `refresh()` 请直接使用返回值。 | | `isEqual` | `(cached: T, fresh: T) => boolean` | 判断 stale 后台刷新得到的新值是否和旧值等价。等价时只续期缓存,不触发 `onUpdate` 或 `subscribe`。默认用 serializer 序列化后比较。 | 执行流程: | 场景 | 行为 | | --- | --- | | fresh 命中 | 直接返回缓存,不执行 `fetcher`。 | | stale 命中且 `staleWhileRevalidate: true` | 返回旧缓存,同时后台执行 `fetcher` 并写入新结果。 | | stale 命中且 `staleWhileRevalidate: false` | 返回旧缓存,不自动刷新。 | | miss 或 expired | 执行 `fetcher`,成功后写入并返回新数据。 | | 同 key 并发请求 | 复用同一个进行中的 `fetcher` Promise。 | | `fetcher` 失败且有旧缓存、`allowStaleOnError: true` | 返回旧缓存,包括 expired 旧缓存。 | | `fetcher` 失败且不允许旧缓存兜底 | 抛出 `fetcher` 的错误。 | SWR 更新页面示例: ```ts const data = await cache.wrap( 'home:list', () => fetch('/api/home').then(res => res.json()), { ttl: '30m', staleTime: '5m', onUpdate: fresh => setHomeData(fresh), }, ) setHomeData(data) ``` 自定义等价判断: ```ts await cache.wrap( 'home:list', () => fetch('/api/home').then(res => res.json()), { staleTime: '5m', isEqual: (cached, fresh) => cached.version === fresh.version, onUpdate: fresh => setHomeData(fresh), }, ) ``` ### `cache.refresh(key, fetcher, options?)` 跳过缓存读取,直接执行 `fetcher`,并把结果写入缓存。 ```ts const fresh = await cache.refresh( 'home:list', () => fetch('/api/home').then(res => res.json()), { ttl: '30m', tags: ['home'] }, ) ``` ```ts refresh(key: string, fetcher: () => Promise, options?: CacheSetOptions): Promise ``` `refresh()` 适合用户手动刷新、操作成功后重新拉取最新数据。需要注意,`refresh()` 在 `fetcher` 失败时也会读取旧缓存作为 fallback;如果不希望这样,传 `{ allowStaleOnError: false }`。 ### `cache.delete(key)` 删除单条缓存。 ```ts await cache.delete('config') ``` 短 key 会按当前实例的 `namespace/version` 解析。传入 `cache.key()` 生成的完整 key 时,会直接删除对应 entry。 ### `cache.invalidate(filter)` 按条件批量删除缓存,返回删除条数。 ```ts await cache.invalidate({ tags: ['activity'] }) await cache.invalidate({ scope: `user:${userId}` }) await cache.invalidate({ pathPrefix: '/api/activity' }) await cache.invalidate({ keyPrefix: 'home:' }) await cache.invalidate({ keys: ['config', 'home:list'] }) ``` ```ts invalidate(filter: CacheEntryFilter): Promise ``` `CacheEntryFilter`: | 参数 | 类型 | 说明 | | --- | --- | --- | | `keys` | `string[]` | 指定 key 删除。短 key 会按当前 namespace/version 解析。 | | `keyPrefix` | `string` | 按 key 前缀删除。短前缀会按当前 namespace/version 解析。 | | `pathPrefix` | `string` | 按 entry.path 前缀删除,推荐配合 `cache.key()` 使用。 | | `tags` | `string[]` | entry 包含任意一个指定 tag 即命中。 | | `namespace` | `string` | 指定 namespace。默认会使用当前实例 namespace。 | | `scope` | `string` | 指定业务作用域。 | | `version` | `string` | 指定版本。 | | `expired` | `boolean` | `true` 只匹配已过期,`false` 只匹配未过期。 | 多个条件之间是 AND 关系。例如 `{ tags: ['product'], scope: 'user:1' }` 只会删除同时满足 tag 和 scope 的 entry。 ### `cache.clear(options?)` 清理当前实例 namespace 下的全部缓存。 ```ts await cache.clear() await cache.clear({ all: true }) ``` ```ts clear(options?: { all?: boolean }): Promise ``` 如果创建实例时没有配置 `namespace`,`cache.clear()` 本身就会清理整个 storage。多应用共享同一个 IndexedDB、localStorage prefix 或 adapter 时,建议务必配置 namespace。 ### `cache.cleanup()` 执行过期和容量清理,返回删除条数。 ```ts const removed = await cache.cleanup() ``` 清理范围默认是当前 namespace。没有配置 namespace 时会扫描整个 storage。 清理顺序: 1. 删除 expired 数据。 2. 如果超过 `cleanup.maxEntries`,按 LRU 删除最久未访问的数据。 3. 如果超过 `cleanup.maxBytes`,继续按 LRU 删除,直到总大小达标。 `cleanup()` 不会自动定时执行。通常可以在应用启动后低频调用: ```ts void cache.cleanup() ``` ### `cache.subscribe(key, listener)` 监听某个 key 的写入通知,返回取消订阅函数。 ```ts const unsubscribe = cache.subscribe('home:list', (value, entry) => { render(value) console.log(entry.updatedAt) }) unsubscribe() ``` ```ts subscribe( key: string, listener: (value: T, entry: CacheEntry) => void, ): () => void ``` 订阅只在当前 `CacheClient` 实例的 JS 运行上下文内生效,不会跨标签页、跨 worker 或跨另一个 cache 实例同步。 ### `cache.getStats()` 获取当前实例统计信息。 ```ts const stats = cache.getStats() ``` ```ts interface CacheStats { hits: number misses: number staleHits: number writes: number skips: number evictions: number errors: number } ``` ## Storage Adapter ### 选择建议 | Adapter | 持久化 | 适合场景 | | --- | --- | --- | | `indexedDBAdapter` | 是 | 浏览器端推荐方案,适合接口列表、大 JSON、字典、语言包等。 | | `memoryAdapter` | 否 | 测试、SSR 临时缓存、不需要持久化的数据。 | | `localStorageAdapter` | 是 | 小体积 fallback,不适合大 JSON。 | | `sessionStorageAdapter` | 会话内 | 只需要当前浏览器会话保留的数据。 | | `hybridAdapter` | 取决于子 adapter | 多存储降级兜底。 | ### `indexedDBAdapter(options?)` ```ts const storage = indexedDBAdapter({ name: 'idb-cache', dbName: 'MY_APP_CACHE', storeName: 'entries', version: 1, }) ``` | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `name` | `string` | `'indexed-db'` | adapter 名称。 | | `dbName` | `string` | `'BRM_CACHE'` | IndexedDB 数据库名。 | | `storeName` | `string` | `'entries'` | object store 名。 | | `version` | `number` | `1` | IndexedDB 数据库版本。 | | `indexedDB` | `IDBFactory` | `globalThis.indexedDB` | 自定义 IndexedDB 实现,测试中可传 `fake-indexeddb`。 | 该 adapter 会创建 keyPath 为 `key` 的 object store,并创建 `namespace`、`scope`、`version`、`path`、`updatedAt`、`expiresAt`、`lastAccessedAt` 索引。当前筛选逻辑会遍历 entry 后在 JS 中匹配 filter。 ### `memoryAdapter(options?)` ```ts const storage = memoryAdapter({ name: 'memory-cache', maxEntries: 200, }) ``` | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `name` | `string` | `'memory'` | adapter 名称。 | | `maxEntries` | `number` | 无 | 最大条数,超过后按 `lastAccessedAt` LRU 淘汰。 | ### `localStorageAdapter(options?)` ```ts const storage = localStorageAdapter({ prefix: 'my-app:', maxEntryBytes: 100 * 1024, }) ``` | 参数 | 类型 | 默认值 | 说明 | | --- | --- | --- | --- | | `name` | `string` | `'local-storage'` | adapter 名称。 | | `prefix` | `string` | `'@brm/cache:'` | 写入 localStorage 的 key 前缀。 | | `storage` | `Storage \| null` | 当前环境的 `localStorage` | 自定义 Storage,实现测试或特殊 WebView 适配。 | | `maxEntryBytes` | `number` | 无 | 单条 entry JSON 字符串最大长度,超过会抛错。当前实现按字符串长度判断。 | ### `sessionStorageAdapter(options?)` 参数和 `localStorageAdapter` 相同,只是默认使用 `sessionStorage`。 ```ts const storage = sessionStorageAdapter({ prefix: 'my-app-session:', }) ``` ### `hybridAdapter(adapters, name?)` ```ts const storage = hybridAdapter([ indexedDBAdapter(), localStorageAdapter(), memoryAdapter(), ]) ``` 行为: - `get()` 按数组顺序读取,返回第一个命中的 entry。 - `set()`、`delete()`、`clear()` 会写入所有子 adapter,至少一个成功就算成功。 - `keys()` 会合并所有可用 adapter 的 key 并去重。 - `entries()` 会合并所有支持 `entries()` 的 adapter,并按 `entry.key` 去重。 - 传空数组会抛出错误。 - 当前不会把低优先级 adapter 命中的数据自动回填到高优先级 adapter。 ### 自定义 adapter 自定义存储只需要实现 `CacheStorageAdapter`。 ```ts import type { CacheEntry, CacheEntryFilter, CacheStorageAdapter } from '@brmtech/cache' export function customAdapter(): CacheStorageAdapter { return { name: 'custom', async get(key: string): Promise | null> { return null }, async set(key: string, entry: CacheEntry): Promise { // write entry }, async delete(key: string): Promise { // delete entry }, async keys(filter?: CacheEntryFilter): Promise { return [] }, async clear(filter?: CacheEntryFilter): Promise { // clear entries }, } } ``` 可选方法: | 方法 | 说明 | | --- | --- | | `entries(filter?)` | 返回完整 entry 列表。实现后 `cleanup()` 可以少一次批量 get。 | | `getMany(keys)` | 批量读取。当前核心保留该接口。 | | `deleteMany(keys)` | 批量删除。`invalidate()` 会优先使用它。 | ## 序列化 默认 `jsonSerializer`: ```ts export const jsonSerializer = { serialize: value => JSON.stringify(value), deserialize: raw => JSON.parse(raw), } ``` 默认适合缓存普通 JSON 数据。`Date`、`Map`、`Set`、类实例、函数、循环引用等不会被自动恢复原型或可能无法序列化。需要特殊类型时,传入自定义 serializer。 ```ts import type { CacheSerializer } from '@brmtech/cache' const serializer: CacheSerializer = { serialize(value) { return JSON.stringify(value) }, deserialize(raw) { return JSON.parse(raw) }, } const cache = createCache({ serializer }) ``` ## `shouldCache` 写入过滤 `shouldCache` 会在写入前执行,可以用于跳过敏感数据、空数据或超出业务规则的数据。 ```ts const cache = createCache({ shouldCache(value, context) { if (context.tags?.includes('sensitive')) return false if (context.key.includes('token') || context.key.includes('password')) return false return value != null }, }) ``` `CacheWriteContext`: ```ts interface CacheWriteContext { key: string path?: string namespace?: string version?: string scope?: string tags?: string[] size: number } ``` ## 常见用法 ### 接口缓存 ```ts const key = cache.key('/api/products', { page: 1, size: 20 }) const products = await cache.wrap( key, () => fetch('/api/products?page=1&size=20').then(res => res.json()), { ttl: '10m', staleTime: '1m', tags: ['product'], }, ) ``` ### 数据更新后失效相关缓存 ```ts await updateProduct(productId, payload) await cache.invalidate({ tags: ['product'] }) ``` ### 用户退出后清理用户维度缓存 ```ts await cache.invalidate({ scope: `user:${userId}` }) ``` ### Vue 或 Pinia 中同步 SWR 结果 不建议把 Pinia 本身当作持久化存储 adapter。更推荐用 IndexedDB/localStorage 做缓存持久化,再用 `onUpdate` 或 `subscribe` 把刷新后的数据同步到 store。 ```ts const data = await cache.wrap( cache.key('/api/home'), () => api.getHome(), { ttl: '30m', staleTime: '5m', onUpdate: fresh => homeStore.setHomeData(fresh), }, ) homeStore.setHomeData(data) ``` ## 完整类型参考 下面是入口明确导出的类型,按源码声明展开。adapter options 字段请参考上面的 adapter 小节。 ```ts type Duration = number | `${number}ms` | `${number}s` | `${number}m` | `${number}h` | `${number}d` interface CacheEntry { key: string path?: string value: T createdAt: number updatedAt: number expiresAt: number | null staleAt: number | null lastAccessedAt: number hitCount: number size: number version?: string namespace?: string scope?: string tags?: string[] metadata?: Record } interface CacheEntryFilter { keys?: string[] keyPrefix?: string pathPrefix?: string tags?: string[] namespace?: string scope?: string version?: string expired?: boolean } interface CacheStorageAdapter { name: string get(key: string): Promise | null> set(key: string, entry: CacheEntry): Promise delete(key: string): Promise keys(filter?: CacheEntryFilter): Promise entries?(filter?: CacheEntryFilter): Promise>> clear(filter?: CacheEntryFilter): Promise getMany?(keys: string[]): Promise | null>> deleteMany?(keys: string[]): Promise } interface CacheSerializer { serialize(value: unknown): string deserialize(raw: string): T } interface CachePolicy { ttl?: Duration | null staleTime?: Duration | null staleWhileRevalidate?: boolean allowStaleOnError?: boolean maxEntryBytes?: number } interface CacheSetOptions extends CachePolicy { path?: string namespace?: string version?: string scope?: string tags?: string[] metadata?: Record } interface CacheWrapOptions extends CacheSetOptions { isEqual?: (cached: T, fresh: T) => boolean onUpdate?: (value: T) => void } interface CacheGetOptions { allowStale?: boolean maxAge?: Duration } interface CacheKeyOptions { namespace?: string version?: string scope?: string keepEmptyString?: boolean } interface CacheCleanupOptions { maxEntries?: number maxBytes?: number maxEntryBytes?: number interval?: Duration eviction?: 'lru' quotaThreshold?: number } interface CacheOptions { namespace?: string version?: string storage?: CacheStorageAdapter memory?: { enabled?: boolean maxEntries?: number } defaultPolicy?: CachePolicy cleanup?: CacheCleanupOptions serializer?: CacheSerializer shouldCache?: (value: unknown, context: CacheWriteContext) => boolean | Promise touchThrottleMs?: number deleteExpiredOnGet?: boolean } interface CacheWriteContext { key: string path?: string namespace?: string version?: string scope?: string tags?: string[] size: number } interface CacheStats { hits: number misses: number staleHits: number writes: number skips: number evictions: number errors: number } type CacheFetcher = () => Promise type CacheSubscribeListener = (value: T, entry: CacheEntry) => void interface CacheClearOptions { all?: boolean } interface CacheClient { get(key: string, options?: CacheGetOptions): Promise getFresh(key: string, options?: CacheGetOptions): Promise set(key: string, value: T, options?: CacheSetOptions): Promise delete(key: string): Promise clear(options?: CacheClearOptions): Promise wrap(key: string, fetcher: CacheFetcher, options?: CacheWrapOptions): Promise refresh(key: string, fetcher: CacheFetcher, options?: CacheSetOptions): Promise invalidate(filter: CacheEntryFilter): Promise cleanup(): Promise subscribe(key: string, listener: CacheSubscribeListener): () => void getStats(): CacheStats key(path: string, params?: Record | URLSearchParams, options?: CacheKeyOptions): string } ``` ## 注意事项 - 不要缓存 token、密码、证件号、银行卡号等敏感数据。 - 默认 serializer 只适合普通 JSON 数据。 - `pathPrefix` 依赖 entry 的 `path` 字段,接口缓存推荐用 `cache.key()`。 - IndexedDB 在隐私模式、旧浏览器或特殊 WebView 中可能不可用,可用 `hybridAdapter` 降级。 - localStorage 容量小且同步阻塞,不建议存大 JSON。 - 当前没有内置跨标签页同步,需要时可在业务层接入 `BroadcastChannel` 后调用 `invalidate()` 或 `refresh()`。 - 当前不会自动定时执行 `cleanup()`,需要在应用启动、空闲时机或业务调度中手动调用。 - 没有配置 `namespace` 时,`clear()` 和 `cleanup()` 会作用于整个 storage,多个应用共用存储时请谨慎。 ## 本地开发 ```bash pnpm install pnpm run typecheck pnpm test pnpm run build ``` 构建产物输出到 `dist/`,包含 ESM、CJS 和类型声明。