@hile/cache 1.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/README.md +186 -0
- package/SKILL.md +273 -0
- package/dist/define.d.ts +17 -0
- package/dist/define.js +27 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +78 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# @hile/cache
|
|
2
|
+
|
|
3
|
+
基于 Redis 的类型安全读穿透缓存层,构建在 `@hile/core`(DI 容器)与 `@hile/ioredis`(Redis 客户端)之上。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @hile/cache
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
依赖:`@hile/core`、`@hile/ioredis`。
|
|
12
|
+
|
|
13
|
+
## 快速开始
|
|
14
|
+
|
|
15
|
+
### 1. 定义缓存
|
|
16
|
+
|
|
17
|
+
使用 `defineCache` 定义一条缓存:指定 key 模板和回源函数。
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { defineCache, Cache } from '@hile/cache';
|
|
21
|
+
|
|
22
|
+
const userCache = defineCache('user:{id:string}:info', async (params) => {
|
|
23
|
+
// params 的类型自动推导为 { id: string }
|
|
24
|
+
const data = await fetchUserFromDB(params.id);
|
|
25
|
+
return new Cache(data).setExpire(300); // 5 分钟 TTL
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 2. 使用缓存
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { RedisCache } from '@hile/cache';
|
|
33
|
+
|
|
34
|
+
const cache = new RedisCache('myapp:'); // 统一前缀
|
|
35
|
+
|
|
36
|
+
const { read, write, remove, has } = await cache.loadCache(userCache);
|
|
37
|
+
|
|
38
|
+
// 读穿透:miss 时自动回源并写入
|
|
39
|
+
const user = await read({ id: 'u-001' });
|
|
40
|
+
|
|
41
|
+
// 强制刷新
|
|
42
|
+
await write({ id: 'u-001' });
|
|
43
|
+
|
|
44
|
+
// 判断是否存在
|
|
45
|
+
const exists = await has({ id: 'u-001' });
|
|
46
|
+
|
|
47
|
+
// 删除
|
|
48
|
+
await remove({ id: 'u-001' });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 核心概念
|
|
54
|
+
|
|
55
|
+
### 类型安全 key 模板
|
|
56
|
+
|
|
57
|
+
key 模板使用 `{name:type}` 占位符,编译期自动推导参数类型:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
defineCache('user:{id:string}:posts:{page:number}:{verified:boolean}', ...)
|
|
61
|
+
// params → { id: string; page: number; verified: boolean }
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
支持的类型:`string`、`number`、`boolean`。
|
|
65
|
+
|
|
66
|
+
### Cache 类
|
|
67
|
+
|
|
68
|
+
`Cache<R>` 包装回源数据,提供 TTL 控制:
|
|
69
|
+
|
|
70
|
+
| 方法 | 说明 |
|
|
71
|
+
|------|------|
|
|
72
|
+
| `new Cache(data)` | 创建缓存数据,expire=0 永不过期 |
|
|
73
|
+
| `.setExpire(seconds)` | 设置 TTL(秒) |
|
|
74
|
+
|
|
75
|
+
### 读穿透(Read-Through)
|
|
76
|
+
|
|
77
|
+
`RedisCache._read` 的调用链路:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
read(params)
|
|
81
|
+
→ EXISTS key
|
|
82
|
+
├─ true → GET key → JSON.parse → 返回
|
|
83
|
+
└─ false → write(params) → handler 回源 → SET/SETEX → 返回
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## API 参考
|
|
89
|
+
|
|
90
|
+
### defineCache
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
function defineCache<T extends string = string, R = any>(
|
|
94
|
+
key: T,
|
|
95
|
+
fn: (params: ExtractParams<T>) => Promise<Cache<R>>
|
|
96
|
+
): DefineCacheResult<T, R>;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### RedisCache
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
class RedisCache {
|
|
103
|
+
constructor(prefix: string);
|
|
104
|
+
|
|
105
|
+
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
|
+
|
|
116
|
+
| 方法 | 返回值 | 说明 |
|
|
117
|
+
|------|--------|------|
|
|
118
|
+
| `read` | `R \| undefined` | 读穿透:EXISTS → GET / miss 则回源写入 |
|
|
119
|
+
| `write` | `R \| undefined` | 强制执行回源并写入 Redis(data=undefined 则删除 key) |
|
|
120
|
+
| `remove` | `number` | 删除缓存,返回 0/1 |
|
|
121
|
+
| `has` | `boolean` | 检查 key 是否存在 |
|
|
122
|
+
|
|
123
|
+
### Redis 中的 key 结构
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
{prefix}{key模板渲染结果}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
例如 `prefix = "myapp:"`、key 模板为 `user:{id:string}:info`、params 为 `{ id: "u-001" }` 时:
|
|
130
|
+
```
|
|
131
|
+
myapp:user:u-001:info
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 与 @hile/cli 集成
|
|
137
|
+
|
|
138
|
+
可在 `package.json` 中配置自动加载 Redis:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"hile": {
|
|
143
|
+
"auto_load_packages": ["@hile/ioredis"]
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
然后任何地方通过 `loadService(ioredisService)` 获取 Redis 客户端,`@hile/cache` 内部已自动处理。
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## 完整示例
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { defineCache, Cache, RedisCache } from '@hile/cache';
|
|
156
|
+
|
|
157
|
+
// 定义多条缓存
|
|
158
|
+
const userCache = defineCache('user:{id:string}:info', async ({ id }) => {
|
|
159
|
+
const user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
|
|
160
|
+
if (!user) return new Cache(undefined); // 不写入 Redis
|
|
161
|
+
return new Cache(user).setExpire(600);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const postCache = defineCache('post:{id:string}:detail', async ({ id }) => {
|
|
165
|
+
const post = await db.query('SELECT * FROM posts WHERE id = $1', [id]);
|
|
166
|
+
return new Cache(post).setExpire(3600);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const cache = new RedisCache('myapp:');
|
|
170
|
+
|
|
171
|
+
// 批量加载
|
|
172
|
+
const [userOps, postOps] = await Promise.all([
|
|
173
|
+
cache.loadCache(userCache),
|
|
174
|
+
cache.loadCache(postCache),
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
// 使用
|
|
178
|
+
const user = await userOps.read({ id: 'u-001' });
|
|
179
|
+
const post = await postOps.read({ id: 'p-001' });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
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
|
+
```
|
package/dist/define.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class Cache<R> {
|
|
2
|
+
readonly data: R;
|
|
3
|
+
private _expire;
|
|
4
|
+
constructor(data: R);
|
|
5
|
+
setExpire(seconds: number): this;
|
|
6
|
+
get expire(): number;
|
|
7
|
+
}
|
|
8
|
+
/** 字面量路径解析 `{k:type}`;`T` 为宽 `string` 时 `params` 退化为宽松索引类型。 */
|
|
9
|
+
export type ExtractParams<Template extends string> = string extends Template ? Record<string, string | number | boolean> : Template extends `${string}{${infer Key}:${infer Type}}${infer Rest}` ? {
|
|
10
|
+
[K in Key]: Type extends "string" ? string : Type extends "number" ? number : Type extends "boolean" ? boolean : never;
|
|
11
|
+
} & ExtractParams<Rest> : {};
|
|
12
|
+
export type DefineCacheHandler<T extends string = string, R = any> = (opts: ExtractParams<T>) => Promise<Cache<R>>;
|
|
13
|
+
export type DefineCacheResult<T extends string = string, R = any> = {
|
|
14
|
+
fn: DefineCacheHandler<T, R>;
|
|
15
|
+
key: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function defineCache<T extends string = string, R = any>(key: T, fn: DefineCacheHandler<T, R>): DefineCacheResult<T, R>;
|
package/dist/define.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class Cache {
|
|
2
|
+
data;
|
|
3
|
+
_expire = 0; // 0: 永不过期
|
|
4
|
+
constructor(data) {
|
|
5
|
+
this.data = data;
|
|
6
|
+
}
|
|
7
|
+
setExpire(seconds) {
|
|
8
|
+
this._expire = seconds;
|
|
9
|
+
return this;
|
|
10
|
+
}
|
|
11
|
+
get expire() {
|
|
12
|
+
return this._expire;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function defineCache(key, fn) {
|
|
16
|
+
return {
|
|
17
|
+
fn,
|
|
18
|
+
key,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// defineCache('user:{id:string}:ddd:{x:number}:idsaf:{y:boolean}', async (params) => {
|
|
22
|
+
// return new Cache({
|
|
23
|
+
// id: params.id,
|
|
24
|
+
// x: params.x,
|
|
25
|
+
// y: params.y,
|
|
26
|
+
// }).setExpire(60);
|
|
27
|
+
// });
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DefineCacheResult, ExtractParams } from './define.js';
|
|
2
|
+
export * from './define.js';
|
|
3
|
+
export declare class RedisCache {
|
|
4
|
+
private readonly prefix;
|
|
5
|
+
private readonly _regexp;
|
|
6
|
+
constructor(prefix: string);
|
|
7
|
+
private makeKey;
|
|
8
|
+
private _write;
|
|
9
|
+
private _read;
|
|
10
|
+
private _remove;
|
|
11
|
+
private _has;
|
|
12
|
+
loadCache<T extends string, R>(target: DefineCacheResult<T, R>): Promise<{
|
|
13
|
+
write: (params: ExtractParams<T>) => Promise<R | undefined>;
|
|
14
|
+
read: (params: ExtractParams<T>) => Promise<R | undefined>;
|
|
15
|
+
remove: (params: ExtractParams<T>) => Promise<number>;
|
|
16
|
+
has: (params: ExtractParams<T>) => Promise<boolean>;
|
|
17
|
+
}>;
|
|
18
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { loadService } from "@hile/core";
|
|
2
|
+
import ioredisService from "@hile/ioredis";
|
|
3
|
+
import { Cache } from './define.js';
|
|
4
|
+
export * from './define.js';
|
|
5
|
+
export class RedisCache {
|
|
6
|
+
prefix;
|
|
7
|
+
_regexp = /\{([^\:]+):[^\}]+\}/g;
|
|
8
|
+
constructor(prefix) {
|
|
9
|
+
this.prefix = prefix;
|
|
10
|
+
}
|
|
11
|
+
makeKey(key, options) {
|
|
12
|
+
return this.prefix + key.replace(this._regexp, (_, key) => String(options[key]));
|
|
13
|
+
}
|
|
14
|
+
async _write(target, params) {
|
|
15
|
+
const key = this.makeKey(target.key, params);
|
|
16
|
+
const cache = await target.fn(params);
|
|
17
|
+
if (!(cache instanceof Cache)) {
|
|
18
|
+
throw new Error('Cache result must be an instance of Cache');
|
|
19
|
+
}
|
|
20
|
+
const redis = await loadService(ioredisService);
|
|
21
|
+
const exists = await redis.exists(key);
|
|
22
|
+
if (cache.data === undefined) {
|
|
23
|
+
if (exists) {
|
|
24
|
+
await redis.del(key);
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const payload = JSON.stringify(cache.data);
|
|
29
|
+
if (cache.expire > 0) {
|
|
30
|
+
await redis.setex(key, cache.expire, payload);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
await redis.set(key, payload);
|
|
34
|
+
}
|
|
35
|
+
return cache.data;
|
|
36
|
+
}
|
|
37
|
+
async _read(target, params) {
|
|
38
|
+
const key = this.makeKey(target.key, params);
|
|
39
|
+
const redis = await loadService(ioredisService);
|
|
40
|
+
const exists = await redis.exists(key);
|
|
41
|
+
if (!exists)
|
|
42
|
+
return await this._write(target, params);
|
|
43
|
+
const text = await redis.get(key);
|
|
44
|
+
if (!text)
|
|
45
|
+
return await this._write(target, params);
|
|
46
|
+
return JSON.parse(text);
|
|
47
|
+
}
|
|
48
|
+
async _remove(target, params) {
|
|
49
|
+
const key = this.makeKey(target.key, params);
|
|
50
|
+
const redis = await loadService(ioredisService);
|
|
51
|
+
const exists = await redis.exists(key);
|
|
52
|
+
if (!exists)
|
|
53
|
+
return 0;
|
|
54
|
+
return await redis.del(key);
|
|
55
|
+
}
|
|
56
|
+
async _has(target, params) {
|
|
57
|
+
const key = this.makeKey(target.key, params);
|
|
58
|
+
const redis = await loadService(ioredisService);
|
|
59
|
+
const exists = await redis.exists(key);
|
|
60
|
+
return !!exists;
|
|
61
|
+
}
|
|
62
|
+
async loadCache(target) {
|
|
63
|
+
return {
|
|
64
|
+
write: async (params) => {
|
|
65
|
+
return await this._write(target, params);
|
|
66
|
+
},
|
|
67
|
+
read: async (params) => {
|
|
68
|
+
return await this._read(target, params);
|
|
69
|
+
},
|
|
70
|
+
remove: async (params) => {
|
|
71
|
+
return await this._remove(target, params);
|
|
72
|
+
},
|
|
73
|
+
has: async (params) => {
|
|
74
|
+
return await this._has(target, params);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hile/cache",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc -b && fix-esm-import-path --preserve-import-type ./dist",
|
|
8
|
+
"dev": "tsc -b --watch",
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md",
|
|
14
|
+
"SKILL.md"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"fix-esm-import-path": "^1.10.3",
|
|
22
|
+
"vitest": "^4.0.18"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@hile/core": "^1.1.2",
|
|
26
|
+
"@hile/ioredis": "^1.1.2"
|
|
27
|
+
},
|
|
28
|
+
"gitHead": "35dc91ded2eb802fdd5edd1c6868b3b205a128bc"
|
|
29
|
+
}
|