@hile/cache 1.0.1 → 2.0.1
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.
- package/package.json +4 -4
- package/SKILL.md +0 -273
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hile/cache",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"vitest": "^4.0.18"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@hile/core": "^
|
|
26
|
-
"@hile/ioredis": "^
|
|
25
|
+
"@hile/core": "^2.0.1",
|
|
26
|
+
"@hile/ioredis": "^2.0.1"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "2c8011db01f2815e5ce34de964d5492640396828"
|
|
29
29
|
}
|
package/SKILL.md
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: cache
|
|
3
|
-
description: Code generation and contribution rules for @hile/cache. Use when editing this package or when the user asks about @hile/cache API, types, patterns, or features.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# @hile/cache — AI Skill Reference
|
|
7
|
-
|
|
8
|
-
本文档面向 **AI 编码模型**。在生成或修改 `@hile/cache` 代码前必读,保证与现有架构、API 约定、测试模式一致。
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## 1. 架构总览
|
|
13
|
-
|
|
14
|
-
### 依赖链
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
@hile/core (DI 容器)
|
|
18
|
-
└── @hile/ioredis (Redis 服务: 环境变量配置 + 单例 + 自动断连)
|
|
19
|
-
└── @hile/cache
|
|
20
|
-
├── define.ts — 类型安全 key 模板 + Cache 数据包装 + defineCache
|
|
21
|
-
└── index.ts — RedisCache 类: _read / _write / _remove / _has
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### 分层职责
|
|
25
|
-
|
|
26
|
-
| 模块 | 文件 | 职责 | 关键约束 |
|
|
27
|
-
|------|------|------|---------|
|
|
28
|
-
| `defineCache` | `define.ts` | 定义缓存 key 模板 + 回源 handler | 模板中 `{name:type}` 占位符;type 只支持 `string`/`number`/`boolean` |
|
|
29
|
-
| `Cache<R>` | `define.ts` | 包装回源数据,携带 TTL | `expire: 0` 表示永不过期 |
|
|
30
|
-
| `RedisCache` | `index.ts` | Redis 读写操作,读穿透/回源写入/删除/存在判断 | 每次操作通过 `loadService(ioredisService)` 获取客户端 |
|
|
31
|
-
|
|
32
|
-
### 流程
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
read(params):
|
|
36
|
-
→ EXISTS key
|
|
37
|
-
├─ false → write(params): handler(params) → SET/SETEX → 返回
|
|
38
|
-
└─ true → GET key → JSON.parse → 返回
|
|
39
|
-
|
|
40
|
-
write(params):
|
|
41
|
-
→ handler(params) → Cache<R>
|
|
42
|
-
├─ cache.data === undefined → DEL key (若存在) → return
|
|
43
|
-
└─ cache.data !== undefined →
|
|
44
|
-
├─ cache.expire > 0 → SETEX(key, expire, JSON.stringify(data))
|
|
45
|
-
└─ cache.expire === 0 → SET(key, JSON.stringify(data))
|
|
46
|
-
|
|
47
|
-
remove(params):
|
|
48
|
-
→ EXISTS key → DEL key → 返回删除数量
|
|
49
|
-
|
|
50
|
-
has(params):
|
|
51
|
-
→ EXISTS key → 返回 boolean
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## 2. 类型定义(代码生成时必须一致)
|
|
57
|
-
|
|
58
|
-
### define.ts
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
export class Cache<R> {
|
|
62
|
-
private _expire: number = 0; // 0: 永不过期
|
|
63
|
-
constructor(public readonly data: R) { }
|
|
64
|
-
public setExpire(seconds: number): this;
|
|
65
|
-
get expire(): number;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/** 字面量路径解析 `{k:type}`;`T` 为宽 `string` 时退化为宽松索引类型 */
|
|
69
|
-
export type ExtractParams<Template extends string> =
|
|
70
|
-
string extends Template
|
|
71
|
-
? Record<string, string | number | boolean>
|
|
72
|
-
: Template extends `${string}{${infer Key}:${infer Type}}${infer Rest}`
|
|
73
|
-
? {
|
|
74
|
-
[K in Key]: Type extends "string"
|
|
75
|
-
? string
|
|
76
|
-
: Type extends "number"
|
|
77
|
-
? number
|
|
78
|
-
: Type extends "boolean"
|
|
79
|
-
? boolean
|
|
80
|
-
: never
|
|
81
|
-
} & ExtractParams<Rest>
|
|
82
|
-
: {};
|
|
83
|
-
|
|
84
|
-
export type DefineCacheHandler<T extends string = string, R = any> =
|
|
85
|
-
(opts: ExtractParams<T>) => Promise<Cache<R>>;
|
|
86
|
-
|
|
87
|
-
export type DefineCacheResult<T extends string = string, R = any> = {
|
|
88
|
-
fn: DefineCacheHandler<T, R>;
|
|
89
|
-
key: string;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
export function defineCache<T extends string = string, R = any>(
|
|
93
|
-
key: T,
|
|
94
|
-
fn: DefineCacheHandler<T, R>
|
|
95
|
-
): DefineCacheResult<T, R>;
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### index.ts
|
|
99
|
-
|
|
100
|
-
```typescript
|
|
101
|
-
export class RedisCache {
|
|
102
|
-
private readonly _regexp = /\{([^\:]+):[^\}]+\}/g;
|
|
103
|
-
constructor(private readonly prefix: string) { }
|
|
104
|
-
|
|
105
|
-
public loadCache<T extends string, R>(
|
|
106
|
-
target: DefineCacheResult<T, R>
|
|
107
|
-
): Promise<{
|
|
108
|
-
write(params: ExtractParams<T>): Promise<R | undefined>;
|
|
109
|
-
read(params: ExtractParams<T>): Promise<R | undefined>;
|
|
110
|
-
remove(params: ExtractParams<T>): Promise<number>;
|
|
111
|
-
has(params: ExtractParams<T>): Promise<boolean>;
|
|
112
|
-
}>;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export { defineCache, Cache, ExtractParams, DefineCacheHandler, DefineCacheResult } from './define';
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## 3. 代码生成模板与规则
|
|
121
|
-
|
|
122
|
-
### 3.1 defineCache 模板
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// 基本模板
|
|
126
|
-
const myCache = defineCache('prefix:{id:string}:suffix', async (params) => {
|
|
127
|
-
const data = await fetchData(params.id);
|
|
128
|
-
if (!data) return new Cache(undefined); // 返回 undefined → Redis 中不存/删除
|
|
129
|
-
return new Cache(data).setExpire(300); // 5 分钟 TTL
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
// 多参数模板
|
|
133
|
-
const myCache = defineCache(
|
|
134
|
-
'user:{id:string}:posts:{page:number}:{verified:boolean}',
|
|
135
|
-
async ({ id, page, verified }) => {
|
|
136
|
-
// params 类型自动推导为 { id: string; page: number; verified: boolean }
|
|
137
|
-
const data = await db.query('SELECT * FROM posts WHERE ...');
|
|
138
|
-
return new Cache(data).setExpire(60);
|
|
139
|
-
}
|
|
140
|
-
);
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### 3.2 RedisCache 使用模板
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
// 单条缓存
|
|
147
|
-
const cache = new RedisCache('myapp:');
|
|
148
|
-
const { read, write, remove, has } = await cache.loadCache(myCache);
|
|
149
|
-
|
|
150
|
-
// 多条缓存
|
|
151
|
-
const [ops1, ops2] = await Promise.all([
|
|
152
|
-
cache.loadCache(cache1),
|
|
153
|
-
cache.loadCache(cache2),
|
|
154
|
-
]);
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### 3.3 测试模板
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
import { RedisCache, defineCache, Cache } from '@hile/cache';
|
|
161
|
-
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
162
|
-
|
|
163
|
-
// Mock Redis 的测试需要配合 @hile/ioredis 的 mock 或真实 Redis 实例
|
|
164
|
-
// 以下为使用真实 Redis 的集成测试模板:
|
|
165
|
-
describe('@hile/cache', () => {
|
|
166
|
-
const cache = new RedisCache('test:');
|
|
167
|
-
|
|
168
|
-
const testCache = defineCache('key:{id:string}', async ({ id }) => {
|
|
169
|
-
return new Cache({ id, value: `hello-${id}` }).setExpire(60);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('read returns data on cache hit', async () => {
|
|
173
|
-
const { write, read, remove } = await cache.loadCache(testCache);
|
|
174
|
-
|
|
175
|
-
// 先写入
|
|
176
|
-
const written = await write({ id: '1' });
|
|
177
|
-
expect(written).toEqual({ id: '1', value: 'hello-1' });
|
|
178
|
-
|
|
179
|
-
// 读取
|
|
180
|
-
const result = await read({ id: '1' });
|
|
181
|
-
expect(result).toEqual({ id: '1', value: 'hello-1' });
|
|
182
|
-
|
|
183
|
-
await remove({ id: '1' });
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('write with undefined data deletes existing key', async () => {
|
|
187
|
-
const undefCache = defineCache('undef:{id:string}', async ({ id }) => {
|
|
188
|
-
return new Cache(undefined);
|
|
189
|
-
});
|
|
190
|
-
const { write, has } = await cache.loadCache(undefCache);
|
|
191
|
-
await write({ id: '1' });
|
|
192
|
-
const exists = await has({ id: '1' });
|
|
193
|
-
expect(exists).toBe(false);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('remove returns 0 for non-existent key', async () => {
|
|
197
|
-
const { remove } = await cache.loadCache(testCache);
|
|
198
|
-
const count = await remove({ id: 'nonexistent' });
|
|
199
|
-
expect(count).toBe(0);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('has returns correct boolean', async () => {
|
|
203
|
-
const { write, has, remove } = await cache.loadCache(testCache);
|
|
204
|
-
await write({ id: '1' });
|
|
205
|
-
expect(await has({ id: '1' })).toBe(true);
|
|
206
|
-
await remove({ id: '1' });
|
|
207
|
-
expect(await has({ id: '1' })).toBe(false);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
---
|
|
213
|
-
|
|
214
|
-
## 4. Redis key 规则
|
|
215
|
-
|
|
216
|
-
### key 渲染
|
|
217
|
-
|
|
218
|
-
`makeKey` 使用正则 `/\{([^\:]+):[^\}]+\}/g` 匹配模板占位符并替换为实际参数值:
|
|
219
|
-
|
|
220
|
-
```
|
|
221
|
-
template: "user:{id:string}:posts:{page:number}"
|
|
222
|
-
params: { id: "u-001", page: 3 }
|
|
223
|
-
result: "user:u-001:posts:3"
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
### 真实 Redis key
|
|
227
|
-
|
|
228
|
-
```
|
|
229
|
-
{prefix}{渲染后的 key}
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
例如 `prefix = "myapp:"` + 上例 → `myapp:user:u-001:posts:3`
|
|
233
|
-
|
|
234
|
-
---
|
|
235
|
-
|
|
236
|
-
## 5. 设计约束(必须遵守)
|
|
237
|
-
|
|
238
|
-
1. **每次操作都通过 `loadService(ioredisService)` 获取 Redis 客户端** — 不要缓存 `redis` 引用,由容器的并发合并和单例保证性能
|
|
239
|
-
2. **`_write` 中的 EXISTS 检查是为了处理 `data === undefined` 的 DELETE 场景** — 正常写入(`data !== undefined`)可考虑不必先 EXISTS(SET 是幂等的),但目前实现统一检查
|
|
240
|
-
3. **`_read` 的兜底逻辑 `if (!text) return this._write(...)`** — 防御 EXISTS 和 GET 之间的竞态删除,不可移除
|
|
241
|
-
4. **序列化统一使用 `JSON.stringify` / `JSON.parse`** — 不支持 Buffer、BigInt 等特殊类型
|
|
242
|
-
5. **Cache 的 `data` 为 `undefined` 时** — `_write` 会删除 Redis 中对应的 key(如果存在),且 `_write` 返回 `undefined`
|
|
243
|
-
|
|
244
|
-
---
|
|
245
|
-
|
|
246
|
-
## 6. 反模式(禁止)
|
|
247
|
-
|
|
248
|
-
1. **不要绕过 `loadService(ioredisService)` 直接创建 Redis 连接** — 会破坏容器的单例管理和 shutdown 清理
|
|
249
|
-
2. **不要假设 key 模板中参数顺序与渲染结果相同** — `makeKey` 使用正则替换,占位符可以出现在 key 的任何位置
|
|
250
|
-
3. **不要在 `Cache` 构造后将数据存到 `Cache` 外部** — `Cache` 的 `data` 是 `public readonly`,但仅供读取;数据变更应创建新的 `Cache`
|
|
251
|
-
4. **不要手动拼接 Redis key 字符串** — 必须使用 `defineCache` 的模板 + `makeKey` 渲染,保证一致性
|
|
252
|
-
5. **不要在 loadCache 之外直接调用 `_read`/`_write`/`_remove`/`_has`** — 这些方法以 `_` 开头表示内部实现,外部统一通过 `loadCache` 返回的操作对象调用
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## 7. 文件改动范围
|
|
257
|
-
|
|
258
|
-
| 文件 | 可修改 | 说明 |
|
|
259
|
-
|------|--------|------|
|
|
260
|
-
| `packages/cache/src/index.ts` | ✅ | 核心 cache 操作 |
|
|
261
|
-
| `packages/cache/src/define.ts` | ⚠️ | 类型定义(修改需同步更新 ExtractParams 类型推导) |
|
|
262
|
-
| `packages/cache/src/index.test.ts` | ✅ | 测试 |
|
|
263
|
-
| `packages/cache/README.md` | ✅ | 用户文档 |
|
|
264
|
-
| `packages/cache/SKILL.md` | ✅ | AI 参考文档 |
|
|
265
|
-
|
|
266
|
-
---
|
|
267
|
-
|
|
268
|
-
## 8. 运行命令
|
|
269
|
-
|
|
270
|
-
```bash
|
|
271
|
-
pnpm --filter @hile/cache build # 编译,必须通过
|
|
272
|
-
pnpm --filter @hile/cache test # 测试,修改行为时必须覆盖
|
|
273
|
-
```
|