@jayfong/x-utils 3.3.0 → 3.4.0
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/lib/ai.d.mts +103 -0
- package/lib/ai.mjs +75 -0
- package/lib/cache.d.mts +464 -0
- package/lib/cache.mjs +531 -0
- package/lib/captcha.d.mts +259 -0
- package/lib/captcha.mjs +174 -0
- package/lib/chinese.d.mts +19 -1
- package/lib/chinese.mjs +28 -1
- package/lib/cli.d.mts +25 -37
- package/lib/cli.mjs +25 -22
- package/lib/crawler.d.mts +1 -1
- package/lib/crawler.mjs +1 -1
- package/lib/crypto.d.mts +158 -0
- package/lib/crypto.mjs +213 -0
- package/lib/data_set.mjs +3 -3
- package/lib/db_value.d.mts +213 -0
- package/lib/db_value.mjs +208 -0
- package/lib/dispose.d.mts +98 -0
- package/lib/dispose.mjs +107 -0
- package/lib/file_store.d.mts +167 -0
- package/lib/file_store.mjs +120 -0
- package/lib/fs.d.mts +64 -2
- package/lib/fs.mjs +65 -3
- package/lib/html.d.mts +103 -0
- package/lib/html.mjs +112 -0
- package/lib/jwt.d.mts +128 -0
- package/lib/jwt.mjs +104 -0
- package/lib/mail.d.mts +125 -0
- package/lib/mail.mjs +108 -0
- package/lib/pay.d.mts +394 -0
- package/lib/pay.mjs +343 -0
- package/lib/puppeteer.mjs +3 -3
- package/lib/random.d.mts +71 -0
- package/lib/random.mjs +92 -0
- package/lib/redis.d.mts +32 -0
- package/lib/redis.mjs +24 -0
- package/lib/runner.d.mts +52 -24
- package/lib/runner.mjs +31 -15
- package/lib/sensitive_words.d.mts +82 -0
- package/lib/sensitive_words.mjs +73 -0
- package/lib/sms.d.mts +133 -0
- package/lib/sms.mjs +100 -0
- package/lib/task.d.mts +1 -1
- package/lib/task.mjs +4 -4
- package/package.json +35 -3
package/lib/cache.mjs
ADDED
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
import { XRandom } from "./random.mjs";
|
|
2
|
+
import { ms } from "vtils";
|
|
3
|
+
import superjson from "superjson";
|
|
4
|
+
// src/cache.ts
|
|
5
|
+
const nativeJsonSerializer = {
|
|
6
|
+
stringify: JSON.stringify,
|
|
7
|
+
parse: JSON.parse
|
|
8
|
+
};
|
|
9
|
+
const superJsonSerializer = {
|
|
10
|
+
stringify: superjson.stringify,
|
|
11
|
+
parse: superjson.parse
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* 通用缓存服务。
|
|
15
|
+
*
|
|
16
|
+
* 基于 Redis 实现,可选叠加一层进程内存缓存。通过泛型方式描述缓存键与
|
|
17
|
+
* 值类型,从而获得类型安全的读写体验。
|
|
18
|
+
*
|
|
19
|
+
* 泛型说明:
|
|
20
|
+
* - `TData`:缓存数据描述类型,形如
|
|
21
|
+
* `{ user: (id: number) => string; totalUser: () => number }`;
|
|
22
|
+
* - `TKey`:所有缓存键名(由 `TData` 推导);
|
|
23
|
+
* - `TKeyPath`:完整缓存键路径(无参函数为键名本身,有参函数为
|
|
24
|
+
* `[键名, ...参数]` 元组);
|
|
25
|
+
* - `TKeyPathForNumberValue`:数字值对应的完整键路径,用于自增/自减;
|
|
26
|
+
* - `TKeyPartialPath`:允许参数部分缺省的键路径,用于按前缀清理。
|
|
27
|
+
*
|
|
28
|
+
* 通常无需手动传泛型,直接用 `XCache` 即可;需要强类型约束时通过
|
|
29
|
+
* {@link XCache.fork} 传入 `TData` 派生。
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* const cache = new XCache({
|
|
33
|
+
* prefix: 'app:',
|
|
34
|
+
* redis: new XRedis(),
|
|
35
|
+
* ttl: 600,
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* const userCache = cache.fork<{ user: (id: number) => string }>();
|
|
39
|
+
* await userCache.set(['user', 1], 'Jay');
|
|
40
|
+
* // 'Jay'
|
|
41
|
+
* const user = await userCache.get(['user', 1]);
|
|
42
|
+
* // 'Jay'
|
|
43
|
+
*/
|
|
44
|
+
var XCache = class XCache {
|
|
45
|
+
options;
|
|
46
|
+
prefix;
|
|
47
|
+
redis;
|
|
48
|
+
hasMemoryCache = false;
|
|
49
|
+
hasMemoryCacheOnly = false;
|
|
50
|
+
memoryCache;
|
|
51
|
+
serializer;
|
|
52
|
+
/**
|
|
53
|
+
* 创建缓存服务实例。
|
|
54
|
+
*
|
|
55
|
+
* @param options 缓存配置,含前缀、Redis 实例、默认 TTL 等。
|
|
56
|
+
*/
|
|
57
|
+
constructor(options) {
|
|
58
|
+
this.options = options;
|
|
59
|
+
this.prefix = options.prefix;
|
|
60
|
+
this.redis = options.redis;
|
|
61
|
+
this.hasMemoryCache = !!options.memory;
|
|
62
|
+
this.hasMemoryCacheOnly = options.memory === "only";
|
|
63
|
+
this.serializer = options.serializer == null || options.serializer === "nativeJson" ? nativeJsonSerializer : options.serializer === "superJson" ? superJsonSerializer : options.serializer;
|
|
64
|
+
this.memoryCache = !this.hasMemoryCache ? void 0 : /* @__PURE__ */ new Map();
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 将缓存键转换为 Redis 键。
|
|
68
|
+
*
|
|
69
|
+
* 元组键用 `_` 连接各段(如 `['user', 1]` → `user_1`),普通键直接
|
|
70
|
+
* 转为字符串,最终加上 {@link XCacheOptions.prefix} 前缀。
|
|
71
|
+
*
|
|
72
|
+
* 注意:使用 `_` 连接时,元组各段自身若含 `_`,可能与其他键发生冲突。
|
|
73
|
+
*/
|
|
74
|
+
toRedisKey(key) {
|
|
75
|
+
return `${this.prefix}${Array.isArray(key) ? key.join("_") : String(key)}`;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 序列化值。
|
|
79
|
+
*
|
|
80
|
+
* @param value 待序列化的值。
|
|
81
|
+
* @returns 序列化后的字符串。
|
|
82
|
+
*/
|
|
83
|
+
serialize(value) {
|
|
84
|
+
return this.serializer.stringify(value);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 反序列化字符串。
|
|
88
|
+
*
|
|
89
|
+
* @param value 待反序列化的字符串。
|
|
90
|
+
* @returns 反序列化后的值。
|
|
91
|
+
*/
|
|
92
|
+
deserialize(value) {
|
|
93
|
+
return this.serializer.parse(value);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* 设置缓存内容。
|
|
97
|
+
*
|
|
98
|
+
* 对象类内容建议避免缓存,以免反序列化后类型丢失等问题;确需缓存对象时
|
|
99
|
+
* 推荐使用 `'superJson'` 序列化器。
|
|
100
|
+
*
|
|
101
|
+
* 行为与分支:
|
|
102
|
+
* - 未传 `ttlOrOptions`,或传入对象但未指定 `ttl` 时,使用构造时的默认
|
|
103
|
+
* TTL({@link XCacheOptions.ttl});
|
|
104
|
+
* - `ttl` 支持函数形式,可依据写入值与默认 TTL 动态计算;
|
|
105
|
+
* - TTL 换算后小于等于 0 时不写入任何缓存(视为禁用缓存);
|
|
106
|
+
* - 非 `memory=only` 模式写入 Redis;启用内存缓存时同步写入内存。
|
|
107
|
+
*
|
|
108
|
+
* @param key 缓存键,可为字符串或 `[键名, ...参数]` 元组。
|
|
109
|
+
* @param value 缓存值。
|
|
110
|
+
* @param ttlOrOptions 缓存时间或选项;缺省时使用默认 TTL。
|
|
111
|
+
* @returns 返回写入的缓存值。
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* await cache.set('greeting', 'hello');
|
|
115
|
+
* // 'hello'
|
|
116
|
+
* await cache.set(['user', 1], 'Jay', 500);
|
|
117
|
+
* // 'Jay',500 毫秒后过期
|
|
118
|
+
*/
|
|
119
|
+
async set(key, value, ttlOrOptions) {
|
|
120
|
+
const ttl = ttlOrOptions == null ? this.options.ttl : typeof ttlOrOptions === "object" ? ttlOrOptions.ttl ?? this.options.ttl : ttlOrOptions;
|
|
121
|
+
const redisTtl = typeof ttl === "function" ? ttl(value, this.options.ttl) : ttl;
|
|
122
|
+
if (redisTtl) {
|
|
123
|
+
const redisTtlMs = ms(redisTtl);
|
|
124
|
+
if (redisTtlMs > 0) {
|
|
125
|
+
const redisKey = this.toRedisKey(key);
|
|
126
|
+
if (!this.hasMemoryCacheOnly) {
|
|
127
|
+
const redisValue = this.serialize(value);
|
|
128
|
+
await this.redis.set(redisKey, redisValue, "PX", redisTtlMs);
|
|
129
|
+
}
|
|
130
|
+
if (this.hasMemoryCache) this.memoryCache.set(redisKey, value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 存储一个值并返回其对应的键。
|
|
137
|
+
*
|
|
138
|
+
* 键由 `X__` 前缀与 CUID2 随机串组成,仅包含字母与数字,URL 友好。
|
|
139
|
+
* 值可为固定值,也可为以生成键为入参的函数,便于生成引用自身的键。
|
|
140
|
+
*
|
|
141
|
+
* @param value 待存储的值,或接收生成的键并返回值的函数。
|
|
142
|
+
* @param ttl 存活时间;缺省时使用构造时的默认 TTL。
|
|
143
|
+
* @returns 生成的缓存键。
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* const key = await cache.save('hello');
|
|
147
|
+
* // 'X__dj8fyuqt57ejnnbsnik8q1d6'
|
|
148
|
+
* await cache.get(key);
|
|
149
|
+
* // 'hello'
|
|
150
|
+
*/
|
|
151
|
+
async save(value, ttl = this.options.ttl) {
|
|
152
|
+
const key = `X__${XRandom.cuid2()}`;
|
|
153
|
+
await this.set(key, typeof value === "function" ? await value(key) : value, ttl);
|
|
154
|
+
return key;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 获取缓存内容。
|
|
158
|
+
*
|
|
159
|
+
* 查找顺序:
|
|
160
|
+
* 1. 启用内存缓存时先查内存,命中直接返回;
|
|
161
|
+
* 2. `memory=only` 模式内存未命中即返回 `null`,不回源 Redis;
|
|
162
|
+
* 3. 其余情况从 Redis 读取;命中后反序列化,并在启用内存缓存时回填内存。
|
|
163
|
+
*
|
|
164
|
+
* Redis 中无该键、值为空或反序列化失败时均返回 `null`。
|
|
165
|
+
*
|
|
166
|
+
* @param key 缓存键。
|
|
167
|
+
* @returns 缓存值;不存在时返回 `null`。
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* const user = await cache.get(['user', 1]);
|
|
171
|
+
* // 'Jay' | null
|
|
172
|
+
*/
|
|
173
|
+
async get(key) {
|
|
174
|
+
const redisKey = this.toRedisKey(key);
|
|
175
|
+
if (this.hasMemoryCache) {
|
|
176
|
+
if (this.memoryCache.has(redisKey)) return this.memoryCache.get(redisKey);
|
|
177
|
+
else if (this.hasMemoryCacheOnly) return null;
|
|
178
|
+
}
|
|
179
|
+
const gotValue = await this.redis.get(redisKey);
|
|
180
|
+
if (gotValue) try {
|
|
181
|
+
const parsedValue = this.deserialize(gotValue);
|
|
182
|
+
if (this.hasMemoryCache) this.memoryCache.set(redisKey, parsedValue);
|
|
183
|
+
return parsedValue;
|
|
184
|
+
} catch {}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 批量获取缓存内容。
|
|
189
|
+
*
|
|
190
|
+
* - 未传任何键时返回空数组;
|
|
191
|
+
* - `memory=only` 模式直接按内存缓存逐个返回;
|
|
192
|
+
* - 启用内存缓存时,内存命中的直接返回,未命中的再从 Redis 批量读取
|
|
193
|
+
* 并回填内存;
|
|
194
|
+
* - 其余情况从 Redis 批量读取。
|
|
195
|
+
*
|
|
196
|
+
* 单个键读取失败(如反序列化异常)时对应位置返回 `null`,不影响其他键。
|
|
197
|
+
*
|
|
198
|
+
* @param keys 缓存键列表。
|
|
199
|
+
* @returns 与入参顺序一致的缓存值数组,未命中的位置为 `null`。
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* await cache.getMany(['user', 1], ['user', 2]);
|
|
203
|
+
* // ['Jay', 'Tom']
|
|
204
|
+
*/
|
|
205
|
+
async getMany(...keys) {
|
|
206
|
+
const redisKeys = keys.map((key) => this.toRedisKey(key));
|
|
207
|
+
if (!redisKeys.length) return [];
|
|
208
|
+
if (this.hasMemoryCacheOnly) return redisKeys.map((redisKey) => this.memoryCache.has(redisKey) ? this.memoryCache.get(redisKey) : null);
|
|
209
|
+
if (this.hasMemoryCache) {
|
|
210
|
+
const result = new Array(keys.length).fill(null);
|
|
211
|
+
const missIndexes = [];
|
|
212
|
+
const missRedisKeys = [];
|
|
213
|
+
for (let i = 0; i < redisKeys.length; i++) if (this.memoryCache.has(redisKeys[i])) result[i] = this.memoryCache.get(redisKeys[i]);
|
|
214
|
+
else {
|
|
215
|
+
missIndexes.push(i);
|
|
216
|
+
missRedisKeys.push(redisKeys[i]);
|
|
217
|
+
}
|
|
218
|
+
if (missRedisKeys.length) (await this.redis.mget(...missRedisKeys)).forEach((value, idx) => {
|
|
219
|
+
if (value != null) try {
|
|
220
|
+
const parsed = this.deserialize(value);
|
|
221
|
+
this.memoryCache.set(missRedisKeys[idx], parsed);
|
|
222
|
+
result[missIndexes[idx]] = parsed;
|
|
223
|
+
} catch {}
|
|
224
|
+
});
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
return (await this.redis.mget(...redisKeys)).map((value) => {
|
|
228
|
+
if (value == null) return null;
|
|
229
|
+
try {
|
|
230
|
+
return this.deserialize(value);
|
|
231
|
+
} catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* 获取距离过期时间的毫秒数。
|
|
238
|
+
*
|
|
239
|
+
* - Redis 中未设置过期时间返回 `-1`,键不存在返回 `-2`;
|
|
240
|
+
* - `memory=only` 模式内存缓存无 TTL 概念,始终返回 `-1`。
|
|
241
|
+
*
|
|
242
|
+
* @param key 缓存键。
|
|
243
|
+
* @returns 距离过期的毫秒数。
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* await cache.ttl('greeting');
|
|
247
|
+
* // 还剩多少毫秒过期;未设置过期时间时返回 -1
|
|
248
|
+
*/
|
|
249
|
+
async ttl(key) {
|
|
250
|
+
if (this.hasMemoryCacheOnly) return -1;
|
|
251
|
+
const redisKey = this.toRedisKey(key);
|
|
252
|
+
return this.redis.pttl(redisKey);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 读取缓存,未命中时执行 `action` 获取值并写回缓存。
|
|
256
|
+
*
|
|
257
|
+
* 先按 {@link XCache.get} 逻辑读取;命中直接返回,否则执行 `action`,
|
|
258
|
+
* 将结果写入缓存并返回。适合「缓存优先、未命中再计算」的场景。
|
|
259
|
+
*
|
|
260
|
+
* 注意:`action` 返回 `null` 时也会写入缓存,但下次读取仍视为未命中,
|
|
261
|
+
* 会再次执行 `action`。
|
|
262
|
+
*
|
|
263
|
+
* @param key 缓存键。
|
|
264
|
+
* @param action 缓存未命中时获取内容的操作,支持同步或异步。
|
|
265
|
+
* @param ttlOrOptions 缓存时间或选项;缺省时使用默认 TTL。
|
|
266
|
+
* @returns 缓存值或 `action` 计算得到的新值。
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* const user = await cache.remember(['user', 1], async () => {
|
|
270
|
+
* return await fetchUser(1);
|
|
271
|
+
* });
|
|
272
|
+
* // 命中缓存时返回缓存值,否则返回 fetchUser(1) 的结果
|
|
273
|
+
*/
|
|
274
|
+
async remember(key, action, ttlOrOptions) {
|
|
275
|
+
let value = await this.get(key);
|
|
276
|
+
if (value === null) {
|
|
277
|
+
value = await action();
|
|
278
|
+
await this.set(key, value, ttlOrOptions);
|
|
279
|
+
}
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 批量读取缓存,未命中的元素执行 `action` 获取值并写回缓存。
|
|
284
|
+
*
|
|
285
|
+
* 先按 {@link XCache.getMany} 批量读取;将未命中的原始元素(`raws` 中
|
|
286
|
+
* 对应下标项)交给 `action` 计算新值,再逐个写回缓存,最后返回与
|
|
287
|
+
* `raws` 顺序一致的结果数组。
|
|
288
|
+
*
|
|
289
|
+
* @param raws 原始数据列表,用于推导键并作为 `action` 的入参。
|
|
290
|
+
* @param key 从原始数据推导缓存键的函数。
|
|
291
|
+
* @param action 缓存未命中的元素批量计算新值的操作,返回数组需与
|
|
292
|
+
* 未命中元素顺序一致。
|
|
293
|
+
* @param ttlOrOptions 缓存时间或选项;缺省时使用默认 TTL。
|
|
294
|
+
* @returns 与 `raws` 顺序一致的缓存值数组。
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* const users = await cache.rememberMany(
|
|
298
|
+
* [1, 2],
|
|
299
|
+
* id => ['user', id],
|
|
300
|
+
* ids => ids.map(id => fetchUser(id)),
|
|
301
|
+
* );
|
|
302
|
+
*/
|
|
303
|
+
async rememberMany(raws, key, action, ttlOrOptions) {
|
|
304
|
+
const keys = raws.map(key);
|
|
305
|
+
const values = await this.getMany(...keys);
|
|
306
|
+
const shouldUpdateIndexes = [];
|
|
307
|
+
for (let i = 0, len = values.length; i < len; i++) if (values[i] === null) shouldUpdateIndexes.push(i);
|
|
308
|
+
if (shouldUpdateIndexes.length) {
|
|
309
|
+
const nextValues = await action(shouldUpdateIndexes.map((i) => raws[i]));
|
|
310
|
+
await Promise.all(nextValues.map(async (nextValue, index) => {
|
|
311
|
+
values[shouldUpdateIndexes[index]] = nextValue;
|
|
312
|
+
await this.set(keys[shouldUpdateIndexes[index]], nextValue, ttlOrOptions);
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
return values;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* 自增。
|
|
319
|
+
*
|
|
320
|
+
* - `memory=only` 模式直接操作内存中的数字;
|
|
321
|
+
* - 其余模式调用 Redis `INCRBY`,并在启用内存缓存时删除对应内存键,
|
|
322
|
+
* 使下次读取从 Redis 回源。
|
|
323
|
+
*
|
|
324
|
+
* @param key 缓存键,对应的值类型需为 `number`。
|
|
325
|
+
* @param increment 增量,可为负数;缺省为 `1`。
|
|
326
|
+
* @returns 自增后的值。
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* await cache.increase('totalUser');
|
|
330
|
+
* // 1
|
|
331
|
+
* await cache.increase('totalUser', 10);
|
|
332
|
+
* // 11
|
|
333
|
+
*/
|
|
334
|
+
async increase(key, increment = 1) {
|
|
335
|
+
const redisKey = this.toRedisKey(key);
|
|
336
|
+
if (this.hasMemoryCacheOnly) {
|
|
337
|
+
const next = (this.memoryCache.get(redisKey) ?? 0) + increment;
|
|
338
|
+
this.memoryCache.set(redisKey, next);
|
|
339
|
+
return next;
|
|
340
|
+
}
|
|
341
|
+
const result = await this.redis.incrby(redisKey, increment);
|
|
342
|
+
if (this.hasMemoryCache) this.memoryCache.delete(redisKey);
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* 自减。
|
|
347
|
+
*
|
|
348
|
+
* 行为与 {@link XCache.increase} 一致,仅将增量取负。
|
|
349
|
+
*
|
|
350
|
+
* @param key 缓存键,对应的值类型需为 `number`。
|
|
351
|
+
* @param decrement 减量,可为负数;缺省为 `1`。
|
|
352
|
+
* @returns 自减后的值。
|
|
353
|
+
*
|
|
354
|
+
* @example
|
|
355
|
+
* await cache.decrease('totalUser');
|
|
356
|
+
* // 10
|
|
357
|
+
*/
|
|
358
|
+
async decrease(key, decrement = 1) {
|
|
359
|
+
return this.increase(key, -decrement);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* 向列表尾部追加元素。
|
|
363
|
+
*
|
|
364
|
+
* - `memory=only` 模式直接操作内存数组;
|
|
365
|
+
* - 其余模式调用 Redis `RPUSH` 逐个追加(先序列化),并在启用内存缓存时
|
|
366
|
+
* 删除对应内存键,使下次读取从 Redis 回源。
|
|
367
|
+
*
|
|
368
|
+
* @param key 缓存键。
|
|
369
|
+
* @param values 待追加的元素列表。
|
|
370
|
+
* @returns 追加后列表的长度。
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* await cache.push('queue', 'a', 'b');
|
|
374
|
+
* // 2
|
|
375
|
+
*/
|
|
376
|
+
async push(key, ...values) {
|
|
377
|
+
const redisKey = this.toRedisKey(key);
|
|
378
|
+
if (this.hasMemoryCacheOnly) {
|
|
379
|
+
const arr = this.memoryCache.get(redisKey) ?? [];
|
|
380
|
+
arr.push(...values);
|
|
381
|
+
this.memoryCache.set(redisKey, arr);
|
|
382
|
+
return arr.length;
|
|
383
|
+
}
|
|
384
|
+
const result = await this.redis.rpush(redisKey, ...values.map((value) => this.serialize(value)));
|
|
385
|
+
if (this.hasMemoryCache) this.memoryCache.delete(redisKey);
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* 从列表头部弹出元素。
|
|
390
|
+
*
|
|
391
|
+
* - `memory=only` 模式直接操作内存数组;
|
|
392
|
+
* - 其余模式逐次调用 Redis `LPOP` 弹出并反序列化,遇到空列表提前结束;
|
|
393
|
+
* 弹出过元素且启用内存缓存时,删除对应内存键使下次读取回源。
|
|
394
|
+
*
|
|
395
|
+
* @param key 缓存键。
|
|
396
|
+
* @param count 弹出元素个数;缺省为 `1`,实际返回数量不超过列表长度。
|
|
397
|
+
* @returns 弹出的元素数组,列表为空时返回空数组。
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* await cache.push('queue', 'a', 'b');
|
|
401
|
+
* await cache.shift('queue');
|
|
402
|
+
* // ['a']
|
|
403
|
+
*/
|
|
404
|
+
async shift(key, count = 1) {
|
|
405
|
+
const redisKey = this.toRedisKey(key);
|
|
406
|
+
if (this.hasMemoryCacheOnly) {
|
|
407
|
+
const arr = this.memoryCache.get(redisKey) ?? [];
|
|
408
|
+
const res = arr.splice(0, count);
|
|
409
|
+
if (arr.length) this.memoryCache.set(redisKey, arr);
|
|
410
|
+
else this.memoryCache.delete(redisKey);
|
|
411
|
+
return res;
|
|
412
|
+
}
|
|
413
|
+
const res = [];
|
|
414
|
+
for (let i = 0; i < count; i++) {
|
|
415
|
+
const item = await this.redis.lpop(redisKey);
|
|
416
|
+
if (item) res.push(this.deserialize(item));
|
|
417
|
+
else break;
|
|
418
|
+
}
|
|
419
|
+
if (this.hasMemoryCache && res.length) this.memoryCache.delete(redisKey);
|
|
420
|
+
return res;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* 删除缓存。
|
|
424
|
+
*
|
|
425
|
+
* - 未传任何键时返回 `0`;
|
|
426
|
+
* - 非 `memory=only` 模式调用 Redis `DEL`,并同步删除内存缓存;
|
|
427
|
+
* - `memory=only` 模式仅删除内存缓存,返回实际删除的内存键数量。
|
|
428
|
+
*
|
|
429
|
+
* @param keys 待删除的缓存键列表。
|
|
430
|
+
* @returns 删除的键数量。
|
|
431
|
+
*
|
|
432
|
+
* @example
|
|
433
|
+
* await cache.remove('greeting', ['user', 1]);
|
|
434
|
+
* // 2
|
|
435
|
+
*/
|
|
436
|
+
async remove(...keys) {
|
|
437
|
+
if (!keys.length) return 0;
|
|
438
|
+
const redisKeys = keys.map((key) => this.toRedisKey(key));
|
|
439
|
+
let count = 0;
|
|
440
|
+
if (!this.hasMemoryCacheOnly) count = await this.redis.del(...redisKeys);
|
|
441
|
+
if (this.hasMemoryCache) {
|
|
442
|
+
let deletedCount = 0;
|
|
443
|
+
redisKeys.forEach((redisKey) => {
|
|
444
|
+
if (this.memoryCache.delete(redisKey)) deletedCount++;
|
|
445
|
+
});
|
|
446
|
+
if (this.hasMemoryCacheOnly) count = deletedCount;
|
|
447
|
+
}
|
|
448
|
+
return count;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* 清空当前前缀下的全部缓存。
|
|
452
|
+
*
|
|
453
|
+
* - 非 `memory=only` 模式通过 `KEYS {prefix}*` 找出全部键后删除;
|
|
454
|
+
* - 启用内存缓存时清空内存缓存;`memory=only` 模式返回内存缓存大小。
|
|
455
|
+
*
|
|
456
|
+
* 注意:生产环境慎用 `KEYS`,键数量大时会阻塞 Redis。
|
|
457
|
+
*
|
|
458
|
+
* @returns 删除的键数量。
|
|
459
|
+
*
|
|
460
|
+
* @example
|
|
461
|
+
* await cache.clearAll();
|
|
462
|
+
* // 3
|
|
463
|
+
*/
|
|
464
|
+
async clearAll() {
|
|
465
|
+
let count = 0;
|
|
466
|
+
if (!this.hasMemoryCacheOnly) {
|
|
467
|
+
const keys = await this.redis.keys(`${this.prefix}*`);
|
|
468
|
+
count = keys.length ? await this.redis.del(...keys) : 0;
|
|
469
|
+
}
|
|
470
|
+
if (this.hasMemoryCache) {
|
|
471
|
+
const memoryCacheSize = this.memoryCache.size;
|
|
472
|
+
this.memoryCache.clear();
|
|
473
|
+
if (this.hasMemoryCacheOnly) count = memoryCacheSize;
|
|
474
|
+
}
|
|
475
|
+
return count;
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* 清空指定前缀的缓存。
|
|
479
|
+
*
|
|
480
|
+
* 对每个前缀执行 `KEYS {前缀}*` 找出匹配键后删除;启用内存缓存时,
|
|
481
|
+
* 按前缀匹配清理内存缓存中的键。
|
|
482
|
+
*
|
|
483
|
+
* - 未传任何前缀时返回 `0`;
|
|
484
|
+
* - `memory=only` 模式仅清理内存缓存,返回实际删除的内存键数量。
|
|
485
|
+
*
|
|
486
|
+
* @param prefixes 缓存键或键前缀列表。
|
|
487
|
+
* @returns 删除的键数量。
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* await cache.clear('user');
|
|
491
|
+
* // 删除所有以 user 开头的键
|
|
492
|
+
*/
|
|
493
|
+
async clear(...prefixes) {
|
|
494
|
+
if (!prefixes.length) return 0;
|
|
495
|
+
let count = 0;
|
|
496
|
+
if (!this.hasMemoryCacheOnly) {
|
|
497
|
+
const keys = (await Promise.all(prefixes.map((prefix) => this.redis.keys(`${this.toRedisKey(prefix)}*`)))).flat();
|
|
498
|
+
count = keys.length ? await this.redis.del(...keys) : 0;
|
|
499
|
+
}
|
|
500
|
+
if (this.hasMemoryCache) {
|
|
501
|
+
let deletedCount = 0;
|
|
502
|
+
this.memoryCache.forEach((_v, k) => {
|
|
503
|
+
if (prefixes.some((prefix) => k.startsWith(this.toRedisKey(prefix)))) {
|
|
504
|
+
if (this.memoryCache.delete(k)) deletedCount++;
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
if (this.hasMemoryCacheOnly) count = deletedCount;
|
|
508
|
+
}
|
|
509
|
+
return count;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* 派生出一个指定数据类型的缓存服务。
|
|
513
|
+
*
|
|
514
|
+
* 传入 `TNextData` 后可获得带类型约束的读写方法;不传选项时直接返回
|
|
515
|
+
* 当前实例(仅调整类型),传选项时基于当前配置合并选项创建新实例。
|
|
516
|
+
*
|
|
517
|
+
* @param options 可选的配置覆盖项,会与当前实例配置合并。
|
|
518
|
+
* @returns 指定数据类型的缓存服务。
|
|
519
|
+
*
|
|
520
|
+
* @example
|
|
521
|
+
* const userCache = cache.fork<{ user: (id: number) => string }>();
|
|
522
|
+
* await userCache.set(['user', 1], 'Jay');
|
|
523
|
+
*/
|
|
524
|
+
fork(options) {
|
|
525
|
+
return options ? new XCache({
|
|
526
|
+
...this.options,
|
|
527
|
+
...options
|
|
528
|
+
}) : this;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
export { XCache };
|