@hile/cache 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/AI.md +232 -0
  2. package/README.md +38 -223
  3. package/package.json +5 -4
package/AI.md ADDED
@@ -0,0 +1,232 @@
1
+ # AI Guide For @hile/cache
2
+
3
+
4
+
5
+ <!-- Generated by scripts/build-ai-context.mjs from docs/ai. Do not edit by hand. -->
6
+
7
+
8
+
9
+ Purpose: Build typed Redis read-through caches with TTL, tags, fieldable hashes, negative/stale options, and singleflight refreshes.
10
+
11
+
12
+
13
+ Use this file when an AI agent installs the npm package and needs package-local examples, package selection rules, boundaries, and verification steps.
14
+
15
+
16
+
17
+ ## Package Selection
18
+
19
+
20
+
21
+ | User asks for | Use | Also read |
22
+ |---|---|---|
23
+ | Add read-through Redis cache | `@hile/cache` | `packages/infrastructure.md`, `recipes/redis-cache-singleflight.md` |
24
+
25
+
26
+
27
+ # Infrastructure Helpers
28
+
29
+ Packages: `@hile/typeorm`, `@hile/ioredis`, `@hile/logger`, `@hile/cache`, `@hile/schedule`, `@hile/loader`.
30
+
31
+ ## Use When
32
+
33
+ Use these packages for database connections, Redis connections, structured logging, typed Redis caches, scheduled jobs, and file-system loaders.
34
+
35
+ ## Do Not Use When
36
+
37
+ - Do not use default TypeORM or Redis services for multiple connections.
38
+ - Do not use `@hile/cache` without returning `new Cache(...)` from the loader function.
39
+ - Do not use `@hile/schedule` distributed mode without Redis lock TTLs sized for job duration.
40
+
41
+ ## Install
42
+
43
+ ```bash
44
+ pnpm add @hile/typeorm @hile/ioredis @hile/logger @hile/cache @hile/schedule @hile/loader
45
+ ```
46
+
47
+ ## Imports
48
+
49
+ ```ts
50
+ import typeormService, { transaction } from '@hile/typeorm'
51
+ import redisService from '@hile/ioredis'
52
+ import { createLogger } from '@hile/logger'
53
+ import { Cache, defineCache, RedisCache } from '@hile/cache'
54
+ import { Scheduler, defineJob } from '@hile/schedule'
55
+ import { scanDirectory, compileRoutePath, toRouterPath, normalizePath, Loader } from '@hile/loader'
56
+ ```
57
+
58
+ ## Copy-Paste Example
59
+
60
+ ```ts
61
+ import { loadService } from '@hile/core'
62
+ import redisService from '@hile/ioredis'
63
+ import { Cache, defineCache, RedisCache } from '@hile/cache'
64
+
65
+ const userProfile = defineCache('user:{id:string}:profile', async ({ id }) => {
66
+ const user = await loadUser(id)
67
+ return new Cache(user).setExpire(300)
68
+ })
69
+
70
+ const redis = await loadService(redisService)
71
+ const cache = new RedisCache('app:', redis)
72
+ const users = await cache.loadCache(userProfile)
73
+ const user = await users.read({ id: 'u1' })
74
+ ```
75
+
76
+ ## More Examples
77
+
78
+ TypeORM transaction with compensation:
79
+
80
+ ```ts
81
+ await transaction(ds, async (runner, rollback) => {
82
+ const user = await runner.manager.save(User, input)
83
+ rollback(() => redis.del(`user:${user.id}`))
84
+ await runner.manager.save(AuditLog, { userId: user.id, action: 'create' })
85
+ return user
86
+ })
87
+ ```
88
+
89
+ Distributed scheduled job:
90
+
91
+ ```ts
92
+ const scheduler = new Scheduler()
93
+ scheduler.add('daily-report', '0 8 * * *', async () => {
94
+ await sendDailyReport()
95
+ }, {
96
+ distributed: {
97
+ redis,
98
+ ttl: 60_000,
99
+ namespace: 'reports',
100
+ policy: 'skip-if-locked',
101
+ },
102
+ })
103
+ ```
104
+
105
+ ## Compose With
106
+
107
+ - Use `RedisCache` with `@hile/ioredis`.
108
+ - Use `defineCache(..., { singleflight: true })` to reduce stampedes; internally it uses Redis locks.
109
+ - Use scheduler distributed mode with `@hile/redis-lock`.
110
+ - Use `Loader` only when building a new file-system based convention.
111
+
112
+ ## Runtime And Lifecycle Notes
113
+
114
+ - `Cache(undefined)` removes the key unless negative caching is configured.
115
+ - `Cache#setExpire(seconds)` uses seconds.
116
+ - `defineCache` typed placeholders support `string`, `number`, and `boolean`.
117
+ - `RedisCache.loadCache()` returns `read`, `write`, `remove`, `has`, and `multi`.
118
+ - `RedisCache.removeTag(tag)` removes tagged cache entries.
119
+ - `fieldable` caches use Redis hashes and cannot combine with stale or negative cache options.
120
+ - `Scheduler.add()` supports cron strings and `{ delay }`.
121
+ - `Scheduler.load()` reads default exports from `*.schedule.*` files produced by `defineJob()`.
122
+ - `scanDirectory()` matches `.ts`, `.js`, `.tsx`, `.jsx`, and `.mjs`.
123
+
124
+ ## Anti-Patterns
125
+
126
+ - Returning raw values from `defineCache` handlers instead of `new Cache(value)`.
127
+ - Treating cache as source of truth.
128
+ - Forgetting to destroy manually created Redis or TypeORM clients.
129
+ - Scheduling jobs without idempotency when side effects can repeat.
130
+
131
+ ## Verification Checklist
132
+
133
+ - Manual Redis clients call `disconnect()` during cleanup.
134
+ - Manual TypeORM data sources call `destroy()` during cleanup.
135
+ - Cache keys include app/tenant prefixes when shared Redis is used.
136
+ - Scheduled jobs have clear duplicate-run policy.
137
+
138
+
139
+
140
+ # Related Recipes
141
+
142
+
143
+
144
+ # Redis Cache With Singleflight
145
+
146
+ ## Complete Example
147
+
148
+ ```ts
149
+ import { loadService } from '@hile/core'
150
+ import redisService from '@hile/ioredis'
151
+ import { Cache, defineCache, RedisCache } from '@hile/cache'
152
+
153
+ const userProfileCache = defineCache(
154
+ 'user:{id:string}:profile',
155
+ async ({ id }) => {
156
+ const profile = await loadProfileFromDatabase(id)
157
+ if (!profile) return new Cache(undefined)
158
+ return new Cache(profile).setExpire(300)
159
+ },
160
+ {
161
+ singleflight: { ttl: 10_000, wait: 10_000 },
162
+ stale: { ttl: 60 },
163
+ negative: { ttl: 30 },
164
+ tags: (params, data) => data ? [`user:${params.id}`] : [],
165
+ },
166
+ )
167
+
168
+ const redis = await loadService(redisService)
169
+ const cache = new RedisCache('app:', redis)
170
+ const userProfiles = await cache.loadCache(userProfileCache)
171
+
172
+ const profile = await userProfiles.read({ id: 'u1' })
173
+ await userProfiles.remove({ id: 'u1' })
174
+ await cache.removeTag('user:u1')
175
+ ```
176
+
177
+ ## File Layout
178
+
179
+ ```text
180
+ src/
181
+ caches/user-profile.cache.ts
182
+ models/users/get-profile.model.ts
183
+ ```
184
+
185
+ ## User Intent
186
+
187
+ Use this recipe when reads are expensive and Redis should provide read-through caching with stampede protection.
188
+
189
+ ## Packages To Use
190
+
191
+ - `@hile/cache`
192
+ - `@hile/ioredis`
193
+ - `@hile/redis-lock` indirectly through cache singleflight
194
+
195
+ ## Implementation Steps
196
+
197
+ 1. Define a typed key with placeholders.
198
+ 2. Return `new Cache(value)` from the loader function.
199
+ 3. Add TTL with `setExpire(seconds)`.
200
+ 4. Enable `singleflight` for expensive reads.
201
+ 5. Remove cache entries after writes.
202
+
203
+ ## Failure And Cleanup Behavior
204
+
205
+ - Stale cache serves previous values while refresh runs.
206
+ - Negative cache stores missing values only when configured.
207
+ - Cache is not source of truth; database writes should remove or refresh related keys.
208
+
209
+ ## Verification Checklist
210
+
211
+ - Cache handler returns `Cache`.
212
+ - Key params match placeholders.
213
+ - Redis prefix is app-specific.
214
+ - Update flows call `remove()` or `removeTag()`.
215
+
216
+
217
+
218
+ # Global Guardrails
219
+
220
+
221
+
222
+ ## Never Generate These Patterns
223
+
224
+ - Do not call `loadService()` at module top level; it starts resources during import.
225
+ - Do not default-export plain functions from `*.boot.*` files; `hile start` expects a Hile service.
226
+ - Do not set `ctx.body` and also return a controller value.
227
+ - Do not assume `@hile/http` Zod validation mutates or coerces `ctx.query`, `ctx.params`, or `ctx.request.body`.
228
+ - Do not put reusable business logic only in controllers, pages, queue workers, or message handlers.
229
+ - Do not use old message examples that append a secondary response getter; current request APIs return promises directly.
230
+ - Do not claim exactly-once delivery or execution from Redis locks, queues, idempotency, or rate limits.
231
+ - Do not use queue `jobId` as the only side-effect idempotency boundary.
232
+ - Do not log the entire async context by default.
package/README.md CHANGED
@@ -1,244 +1,59 @@
1
1
  # @hile/cache
2
2
 
3
- 基于 Redis 的类型安全读穿透缓存层。依赖 `ioredis`,构造时注入已连接的 `Redis` 实例(可与 `@hile/ioredis` 配合使用)。
3
+ <!-- Generated by scripts/build-ai-context.mjs from docs/ai. Do not edit by hand. -->
4
4
 
5
- 3.0.0 开始,新增或重构的 Hile 架构包统一进入 3.x 版本线,2.x 时代结束。
5
+ Build typed Redis read-through caches with TTL, tags, fieldable hashes, negative/stale options, and singleflight refreshes.
6
6
 
7
- ## 安装
7
+ This README is intentionally short and example-first. The complete AI-facing guide ships in `AI.md` in this package.
8
8
 
9
- ```bash
10
- pnpm add @hile/cache
11
- ```
12
-
13
- 运行时需提供 `ioredis` 的 `Redis` 实例;在 Hile 应用中通常通过 `@hile/ioredis` 的 `createRedis()` 或 `loadService(ioredisService)` 获取。
14
-
15
- ## 快速开始
16
-
17
- ### 1. 定义缓存
18
-
19
- 使用 `defineCache` 定义一条缓存:指定 key 模板和回源函数。
20
-
21
- ```typescript
22
- import { defineCache, Cache } from '@hile/cache';
23
-
24
- const userCache = defineCache('user:{id:string}:info', async (params) => {
25
- // params 的类型自动推导为 { id: string }
26
- const data = await fetchUserFromDB(params.id);
27
- return new Cache(data).setExpire(300); // 5 分钟 TTL
28
- }, {
29
- singleflight: true,
30
- stale: { ttl: 60 },
31
- negative: { ttl: 30 },
32
- tags: ({ id }) => ['users', `user:${id}`],
33
- });
34
- ```
35
-
36
- ### 2. 使用缓存
37
-
38
- ```typescript
39
- import { loadService } from '@hile/core';
40
- import redisService from '@hile/ioredis';
41
- import { RedisCache } from '@hile/cache';
42
-
43
- const redis = await loadService(redisService);
44
- const cache = new RedisCache('myapp:', redis); // 前缀 + Redis 实例
45
-
46
- const { read, write, remove, has } = await cache.loadCache(userCache);
47
-
48
- // 读穿透:miss 时自动回源并写入
49
- const user = await read({ id: 'u-001' });
50
-
51
- // 强制刷新
52
- await write({ id: 'u-001' });
53
-
54
- // 判断是否存在
55
- const exists = await has({ id: 'u-001' });
56
-
57
- // 删除
58
- await remove({ id: 'u-001' });
59
-
60
- // 按 tag 批量失效
61
- await cache.removeTag('users');
62
- ```
63
-
64
- ---
65
-
66
- ## 核心概念
67
-
68
- ### 类型安全 key 模板
69
-
70
- key 模板使用 `{name:type}` 占位符,编译期自动推导参数类型:
71
-
72
- ```typescript
73
- defineCache('user:{id:string}:posts:{page:number}:{verified:boolean}', ...)
74
- // params → { id: string; page: number; verified: boolean }
75
- ```
76
-
77
- 支持的类型:`string`、`number`、`boolean`。
78
-
79
- ### Cache 类
80
-
81
- `Cache<R>` 包装回源数据,提供 TTL 控制:
82
-
83
- | 方法 | 说明 |
84
- |------|------|
85
- | `new Cache(data)` | 创建缓存数据,expire=0 永不过期 |
86
- | `.setExpire(seconds)` | 设置 TTL(秒) |
87
-
88
- ### 读穿透(Read-Through)
89
-
90
- `RedisCache._read` 的调用链路:
91
-
92
- ```
93
- read(params)
94
- → EXISTS key
95
- ├─ true → GET key → JSON.parse → 返回
96
- └─ false → write(params) → handler 回源 → SET/SETEX → 返回
97
- ```
98
-
99
- ### Singleflight
9
+ ## When To Use
100
10
 
101
- `singleflight` 使用 `@hile/redis-lock` 合并同一个 key 的并发 miss,避免热点 key 同时打到数据库。
11
+ Use these packages for database connections, Redis connections, structured logging, typed Redis caches, scheduled jobs, and file-system loaders.
102
12
 
103
- ```typescript
104
- const userCache = defineCache('user:{id:string}', fetchUser, {
105
- singleflight: {
106
- ttl: 10_000,
107
- wait: 10_000,
108
- },
109
- });
110
- ```
111
-
112
- ### Stale Cache
113
-
114
- `stale: { ttl }` 会在数据过了 `Cache#setExpire()` 的 fresh 时间后继续保留一段 stale 时间。读取 stale 数据时立即返回旧值,并在后台刷新。
115
-
116
- ### Negative Cache
117
-
118
- 默认 `new Cache(undefined)` 不写 Redis。配置 `negative: { ttl }` 后会写入一个短 TTL 哨兵,避免不存在的数据反复回源。
119
-
120
- ### Tags
121
-
122
- `tags` 会把缓存 key 记录到 Redis set,之后可用 `cache.removeTag(tag)` 批量删除。
123
-
124
- ---
125
-
126
- ## API 参考
127
-
128
- ### defineCache
129
-
130
- ```typescript
131
- function defineCache<T extends string = string, R = any>(
132
- key: T,
133
- fn: (params: ExtractParams<T>) => Promise<Cache<R>>,
134
- options?: boolean | DefineCacheOptions<T, R>
135
- ): DefineCacheResult<T, R>;
136
- ```
137
-
138
- 第三个参数仍兼容旧的 `boolean` fieldable 写法;新能力使用 options 对象。
139
-
140
- > `fieldable` 使用 Redis Hash 存储,不兼容 `stale` 和 `negative` 这类依赖字符串 payload 的策略;组合使用会在 `defineCache()` 阶段直接抛出配置错误。
141
-
142
- ### RedisCache
143
-
144
- ```typescript
145
- class RedisCache {
146
- constructor(prefix: string, redis: Redis, options?: { locks?: RedisLock });
147
-
148
- loadCache<T extends string, R>(
149
- target: DefineCacheResult<T, R>
150
- ): Promise<{
151
- write(params: ExtractParams<T>): Promise<R | undefined>;
152
- read(params: ExtractParams<T>): Promise<R | undefined>;
153
- remove(params: ExtractParams<T>): Promise<number>;
154
- has(params: ExtractParams<T>): Promise<boolean>;
155
- }>;
156
-
157
- removeTag(tag: string): Promise<number>;
158
- }
159
- ```
160
-
161
- | 方法 | 返回值 | 说明 |
162
- |------|--------|------|
163
- | `read` | `R \| undefined` | 读穿透:EXISTS → GET / miss 则回源写入 |
164
- | `write` | `R \| undefined` | 强制执行回源并写入 Redis(data=undefined 则删除 key) |
165
- | `remove` | `number` | 删除缓存,返回 0/1 |
166
- | `has` | `boolean` | 检查 key 是否存在 |
167
- | `removeTag` | `number` | 删除某个 tag 下记录的所有缓存 key |
168
-
169
- ### Redis 中的 key 结构
13
+ ## Install
170
14
 
171
- ```
172
- {prefix}{key模板渲染结果}
173
- ```
174
-
175
- 例如 `prefix = "myapp:"`、key 模板为 `user:{id:string}:info`、params 为 `{ id: "u-001" }` 时:
176
- ```
177
- myapp:user:u-001:info
15
+ ```bash
16
+ pnpm add @hile/cache
178
17
  ```
179
18
 
180
- ---
19
+ ## Copy-Paste Example
181
20
 
182
- ## 与 @hile/cli 集成
21
+ ```ts
22
+ import { loadService } from '@hile/core'
23
+ import redisService from '@hile/ioredis'
24
+ import { Cache, defineCache, RedisCache } from '@hile/cache'
183
25
 
184
- `package.json` 中配置自动加载 Redis,在 boot 或服务工厂里注入客户端:
26
+ const userProfile = defineCache('user:{id:string}:profile', async ({ id }) => {
27
+ const user = await loadUser(id)
28
+ return new Cache(user).setExpire(300)
29
+ })
185
30
 
186
- ```json
187
- {
188
- "hile": {
189
- "auto_load_packages": ["@hile/ioredis"]
190
- }
191
- }
31
+ const redis = await loadService(redisService)
32
+ const cache = new RedisCache('app:', redis)
33
+ const users = await cache.loadCache(userProfile)
34
+ const user = await users.read({ id: 'u1' })
192
35
  ```
193
36
 
194
- ```typescript
195
- import { loadService } from '@hile/core';
196
- import redisService from '@hile/ioredis';
197
- import { defineCache, Cache, RedisCache } from '@hile/cache';
37
+ ## Boundaries
198
38
 
199
- // defineService 工厂内
200
- const redis = await loadService(redisService);
201
- const cache = new RedisCache('myapp:', redis);
202
- ```
203
-
204
- ---
205
-
206
- ## 完整示例
207
-
208
- ```typescript
209
- import { loadService } from '@hile/core';
210
- import redisService from '@hile/ioredis';
211
- import { defineCache, Cache, RedisCache } from '@hile/cache';
212
-
213
- const redis = await loadService(redisService);
39
+ - Do not use default TypeORM or Redis services for multiple connections.
40
+ - Do not use `@hile/cache` without returning `new Cache(...)` from the loader function.
41
+ - Do not use `@hile/schedule` distributed mode without Redis lock TTLs sized for job duration.
214
42
 
215
- // 定义多条缓存
216
- const userCache = defineCache('user:{id:string}:info', async ({ id }) => {
217
- const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
218
- if (!user) return new Cache(undefined); // 不写入 Redis
219
- return new Cache(user).setExpire(600);
220
- });
43
+ - Returning raw values from `defineCache` handlers instead of `new Cache(value)`.
44
+ - Treating cache as source of truth.
45
+ - Forgetting to destroy manually created Redis or TypeORM clients.
46
+ - Scheduling jobs without idempotency when side effects can repeat.
221
47
 
222
- const postCache = defineCache('post:{id:string}:detail', async ({ id }) => {
223
- const post = await db.query('SELECT * FROM posts WHERE id = $1', [id]);
224
- return new Cache(post).setExpire(3600);
225
- });
226
-
227
- const cache = new RedisCache('myapp:', redis);
228
-
229
- // 批量加载
230
- const [userOps, postOps] = await Promise.all([
231
- cache.loadCache(userCache),
232
- cache.loadCache(postCache),
233
- ]);
234
-
235
- // 使用
236
- const user = await userOps.read({ id: 'u-001' });
237
- const post = await postOps.read({ id: 'p-001' });
238
- ```
48
+ ## Verify
239
49
 
240
- ---
50
+ - Manual Redis clients call `disconnect()` during cleanup.
51
+ - Manual TypeORM data sources call `destroy()` during cleanup.
52
+ - Cache keys include app/tenant prefixes when shared Redis is used.
53
+ - Scheduled jobs have clear duplicate-run policy.
241
54
 
242
- ## License
55
+ ## More Context
243
56
 
244
- MIT
57
+ - `AI.md` in this package: full package-local AI guide.
58
+ - Root `llms-full.txt`: full monorepo AI context.
59
+ - Root `references/`: source files copied from `docs/ai`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hile/cache",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -10,7 +10,8 @@
10
10
  },
11
11
  "files": [
12
12
  "dist",
13
- "README.md"
13
+ "README.md",
14
+ "AI.md"
14
15
  ],
15
16
  "license": "MIT",
16
17
  "publishConfig": {
@@ -21,8 +22,8 @@
21
22
  "vitest": "^4.0.18"
22
23
  },
23
24
  "dependencies": {
24
- "@hile/redis-lock": "^3.0.1",
25
+ "@hile/redis-lock": "^3.0.2",
25
26
  "ioredis": "^5.11.0"
26
27
  },
27
- "gitHead": "88f52fb95743f86761778776aff23631fcf9d821"
28
+ "gitHead": "0985b6f8abc1f4de0a36324063585fdc3ac1375b"
28
29
  }