@h-ai/cache 0.1.0-alpha5
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/LICENSE +202 -0
- package/README.md +150 -0
- package/dist/index.d.ts +476 -0
- package/dist/index.js +1571 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1571 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { core, err, ok } from '@h-ai/core';
|
|
3
|
+
import Redis from 'ioredis';
|
|
4
|
+
|
|
5
|
+
// src/cache-config.ts
|
|
6
|
+
var RedisClusterNodeSchema = z.object({
|
|
7
|
+
/** 节点主机地址 */
|
|
8
|
+
host: z.string(),
|
|
9
|
+
/** 节点端口号(1~65535) */
|
|
10
|
+
port: z.number().int().min(1).max(65535)
|
|
11
|
+
});
|
|
12
|
+
var RedisSentinelConfigSchema = z.object({
|
|
13
|
+
/** 哨兵节点列表(至少 1 个) */
|
|
14
|
+
sentinels: z.array(RedisClusterNodeSchema).min(1),
|
|
15
|
+
/** 哨兵监控的主节点名称 */
|
|
16
|
+
name: z.string()
|
|
17
|
+
});
|
|
18
|
+
var MemoryConfigSchema = z.object({
|
|
19
|
+
type: z.literal("memory")
|
|
20
|
+
});
|
|
21
|
+
var RedisConfigSchema = z.object({
|
|
22
|
+
type: z.literal("redis"),
|
|
23
|
+
// 连接(优先级:url > cluster > sentinel > host)
|
|
24
|
+
/** Redis 连接 URL(如 redis://user:pass@host:6379/0),优先级最高 */
|
|
25
|
+
url: z.string().optional(),
|
|
26
|
+
/** 主机地址;默认 'localhost' */
|
|
27
|
+
host: z.string().default("localhost"),
|
|
28
|
+
/** 端口号;默认 6379 */
|
|
29
|
+
port: z.number().int().min(1).max(65535).default(6379),
|
|
30
|
+
/** 认证密码 */
|
|
31
|
+
password: z.string().optional(),
|
|
32
|
+
/** 数据库编号(0~15);默认 0 */
|
|
33
|
+
db: z.number().int().min(0).max(15).default(0),
|
|
34
|
+
/** 集群节点列表;配置后以集群模式连接 */
|
|
35
|
+
cluster: z.array(RedisClusterNodeSchema).optional(),
|
|
36
|
+
/** 哨兵配置;配置后以哨兵模式连接 */
|
|
37
|
+
sentinel: RedisSentinelConfigSchema.optional(),
|
|
38
|
+
// 通用选项
|
|
39
|
+
/** 连接超时时间(毫秒);默认 10000 */
|
|
40
|
+
connectTimeout: z.number().int().min(0).default(1e4),
|
|
41
|
+
/** 命令执行超时(毫秒);默认 5000 */
|
|
42
|
+
commandTimeout: z.number().int().min(0).default(5e3),
|
|
43
|
+
/** 键前缀(自动加在所有键前) */
|
|
44
|
+
keyPrefix: z.string().optional(),
|
|
45
|
+
/** 是否开启 TLS;默认 false */
|
|
46
|
+
tls: z.boolean().default(false),
|
|
47
|
+
/** 最大重试次数;默认 3 */
|
|
48
|
+
maxRetries: z.number().int().min(0).default(3),
|
|
49
|
+
/** 重试间隔基数(毫秒);实际间隔 = retryDelay × 重试次数;默认 50 */
|
|
50
|
+
retryDelay: z.number().int().min(0).default(50),
|
|
51
|
+
/** 是否只读模式;默认 false */
|
|
52
|
+
readOnly: z.boolean().default(false)
|
|
53
|
+
});
|
|
54
|
+
var CacheConfigSchema = z.discriminatedUnion("type", [
|
|
55
|
+
MemoryConfigSchema,
|
|
56
|
+
RedisConfigSchema
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// messages/en-US.json
|
|
60
|
+
var en_US_default = {
|
|
61
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
62
|
+
cache_notInitialized: "Cache not initialized, please call cache.init() first",
|
|
63
|
+
cache_operationFailed: "Cache operation failed: {error}",
|
|
64
|
+
cache_redisConnectionFailed: "Redis connection failed: {error}",
|
|
65
|
+
cache_initFailed: "Cache initialization failed: {error}",
|
|
66
|
+
cache_unsupportedType: "Unsupported cache type: {type}",
|
|
67
|
+
cache_valueNotNumber: "Value is not a number",
|
|
68
|
+
cache_keyNotFound: "Key not found",
|
|
69
|
+
cache_indexOutOfRange: "Index out of range",
|
|
70
|
+
cache_configError: "Cache config validation failed: {error}"
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// messages/zh-CN.json
|
|
74
|
+
var zh_CN_default = {
|
|
75
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
76
|
+
cache_notInitialized: "\u7F13\u5B58\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 cache.init()",
|
|
77
|
+
cache_operationFailed: "\u7F13\u5B58\u64CD\u4F5C\u5931\u8D25\uFF1A{error}",
|
|
78
|
+
cache_redisConnectionFailed: "Redis \u8FDE\u63A5\u5931\u8D25\uFF1A{error}",
|
|
79
|
+
cache_initFailed: "\u7F13\u5B58\u521D\u59CB\u5316\u5931\u8D25\uFF1A{error}",
|
|
80
|
+
cache_unsupportedType: "\u4E0D\u652F\u6301\u7684\u7F13\u5B58\u7C7B\u578B\uFF1A{type}",
|
|
81
|
+
cache_valueNotNumber: "\u503C\u4E0D\u662F\u6570\u5B57",
|
|
82
|
+
cache_keyNotFound: "\u952E\u4E0D\u5B58\u5728",
|
|
83
|
+
cache_indexOutOfRange: "\u7D22\u5F15\u8D8A\u754C",
|
|
84
|
+
cache_configError: "\u7F13\u5B58\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A{error}"
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/cache-i18n.ts
|
|
88
|
+
var cacheM = core.i18n.createMessageGetter({
|
|
89
|
+
"zh-CN": zh_CN_default,
|
|
90
|
+
"en-US": en_US_default
|
|
91
|
+
});
|
|
92
|
+
var CacheErrorInfo = {
|
|
93
|
+
CONNECTION_FAILED: "001:500",
|
|
94
|
+
OPERATION_FAILED: "002:500",
|
|
95
|
+
SERIALIZATION_FAILED: "003:500",
|
|
96
|
+
DESERIALIZATION_FAILED: "004:500",
|
|
97
|
+
KEY_NOT_FOUND: "005:404",
|
|
98
|
+
TIMEOUT: "006:504",
|
|
99
|
+
NOT_INITIALIZED: "010:500",
|
|
100
|
+
UNSUPPORTED_TYPE: "011:400",
|
|
101
|
+
CONFIG_ERROR: "012:500"
|
|
102
|
+
};
|
|
103
|
+
var HaiCacheError = core.error.buildHaiErrorsDef("cache", CacheErrorInfo);
|
|
104
|
+
function serializeValue(value) {
|
|
105
|
+
return JSON.stringify(value);
|
|
106
|
+
}
|
|
107
|
+
function deserializeValue(str) {
|
|
108
|
+
return JSON.parse(str);
|
|
109
|
+
}
|
|
110
|
+
function globToRegex(pattern) {
|
|
111
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
112
|
+
const regexStr = escaped.replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
|
|
113
|
+
return new RegExp(`^${regexStr}$`);
|
|
114
|
+
}
|
|
115
|
+
function createMemoryProvider() {
|
|
116
|
+
const store = /* @__PURE__ */ new Map();
|
|
117
|
+
const hashStore = /* @__PURE__ */ new Map();
|
|
118
|
+
const listStore = /* @__PURE__ */ new Map();
|
|
119
|
+
const setStore = /* @__PURE__ */ new Map();
|
|
120
|
+
const zsetStore = /* @__PURE__ */ new Map();
|
|
121
|
+
let connected = false;
|
|
122
|
+
let cleanupTimer = null;
|
|
123
|
+
function isExpired(entry) {
|
|
124
|
+
return entry.expiresAt != null && Date.now() > entry.expiresAt;
|
|
125
|
+
}
|
|
126
|
+
function cleanup() {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
for (const [key, entry] of store.entries()) {
|
|
129
|
+
if (entry.expiresAt && entry.expiresAt < now) {
|
|
130
|
+
store.delete(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function getValidEntry(key) {
|
|
135
|
+
const entry = store.get(key);
|
|
136
|
+
if (!entry)
|
|
137
|
+
return null;
|
|
138
|
+
if (isExpired(entry)) {
|
|
139
|
+
store.delete(key);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return entry;
|
|
143
|
+
}
|
|
144
|
+
function calculateExpiry(options) {
|
|
145
|
+
if (!options)
|
|
146
|
+
return void 0;
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
if (options.ex)
|
|
149
|
+
return now + options.ex * 1e3;
|
|
150
|
+
if (options.px)
|
|
151
|
+
return now + options.px;
|
|
152
|
+
if (options.exat)
|
|
153
|
+
return options.exat * 1e3;
|
|
154
|
+
if (options.pxat)
|
|
155
|
+
return options.pxat;
|
|
156
|
+
return void 0;
|
|
157
|
+
}
|
|
158
|
+
const kv = {
|
|
159
|
+
async get(key) {
|
|
160
|
+
const entry = getValidEntry(key);
|
|
161
|
+
return ok(entry ? entry.value : null);
|
|
162
|
+
},
|
|
163
|
+
async set(key, value, options) {
|
|
164
|
+
const existing = getValidEntry(key) !== null;
|
|
165
|
+
if (options?.nx && existing)
|
|
166
|
+
return ok(void 0);
|
|
167
|
+
if (options?.xx && !existing)
|
|
168
|
+
return ok(void 0);
|
|
169
|
+
const currentEntry = getValidEntry(key);
|
|
170
|
+
const expiresAt = options?.keepTtl && currentEntry ? currentEntry.expiresAt : calculateExpiry(options);
|
|
171
|
+
store.set(key, { value, expiresAt });
|
|
172
|
+
return ok(void 0);
|
|
173
|
+
},
|
|
174
|
+
async del(...keys) {
|
|
175
|
+
let count = 0;
|
|
176
|
+
for (const key of keys) {
|
|
177
|
+
if (store.delete(key))
|
|
178
|
+
count++;
|
|
179
|
+
hashStore.delete(key);
|
|
180
|
+
listStore.delete(key);
|
|
181
|
+
setStore.delete(key);
|
|
182
|
+
zsetStore.delete(key);
|
|
183
|
+
}
|
|
184
|
+
return ok(count);
|
|
185
|
+
},
|
|
186
|
+
async exists(...keys) {
|
|
187
|
+
let count = 0;
|
|
188
|
+
for (const key of keys) {
|
|
189
|
+
if (getValidEntry(key))
|
|
190
|
+
count++;
|
|
191
|
+
}
|
|
192
|
+
return ok(count);
|
|
193
|
+
},
|
|
194
|
+
async expire(key, seconds) {
|
|
195
|
+
const entry = getValidEntry(key);
|
|
196
|
+
if (!entry)
|
|
197
|
+
return ok(false);
|
|
198
|
+
entry.expiresAt = Date.now() + seconds * 1e3;
|
|
199
|
+
return ok(true);
|
|
200
|
+
},
|
|
201
|
+
async expireAt(key, timestamp) {
|
|
202
|
+
const entry = getValidEntry(key);
|
|
203
|
+
if (!entry)
|
|
204
|
+
return ok(false);
|
|
205
|
+
entry.expiresAt = timestamp * 1e3;
|
|
206
|
+
return ok(true);
|
|
207
|
+
},
|
|
208
|
+
async ttl(key) {
|
|
209
|
+
const entry = store.get(key);
|
|
210
|
+
if (!entry)
|
|
211
|
+
return ok(-2);
|
|
212
|
+
if (!entry.expiresAt)
|
|
213
|
+
return ok(-1);
|
|
214
|
+
if (isExpired(entry)) {
|
|
215
|
+
store.delete(key);
|
|
216
|
+
return ok(-2);
|
|
217
|
+
}
|
|
218
|
+
return ok(Math.ceil((entry.expiresAt - Date.now()) / 1e3));
|
|
219
|
+
},
|
|
220
|
+
async persist(key) {
|
|
221
|
+
const entry = getValidEntry(key);
|
|
222
|
+
if (!entry)
|
|
223
|
+
return ok(false);
|
|
224
|
+
delete entry.expiresAt;
|
|
225
|
+
return ok(true);
|
|
226
|
+
},
|
|
227
|
+
async incr(key) {
|
|
228
|
+
const entry = getValidEntry(key);
|
|
229
|
+
const current = entry ? Number(entry.value) : 0;
|
|
230
|
+
if (Number.isNaN(current)) {
|
|
231
|
+
return err(HaiCacheError.OPERATION_FAILED, cacheM("cache_valueNotNumber"));
|
|
232
|
+
}
|
|
233
|
+
const newValue = current + 1;
|
|
234
|
+
store.set(key, { value: newValue, expiresAt: entry?.expiresAt });
|
|
235
|
+
return ok(newValue);
|
|
236
|
+
},
|
|
237
|
+
async incrBy(key, increment) {
|
|
238
|
+
const entry = getValidEntry(key);
|
|
239
|
+
const current = entry ? Number(entry.value) : 0;
|
|
240
|
+
if (Number.isNaN(current)) {
|
|
241
|
+
return err(HaiCacheError.OPERATION_FAILED, cacheM("cache_valueNotNumber"));
|
|
242
|
+
}
|
|
243
|
+
const newValue = current + increment;
|
|
244
|
+
store.set(key, { value: newValue, expiresAt: entry?.expiresAt });
|
|
245
|
+
return ok(newValue);
|
|
246
|
+
},
|
|
247
|
+
async decr(key) {
|
|
248
|
+
return kv.incrBy(key, -1);
|
|
249
|
+
},
|
|
250
|
+
async decrBy(key, decrement) {
|
|
251
|
+
return kv.incrBy(key, -decrement);
|
|
252
|
+
},
|
|
253
|
+
async mget(...keys) {
|
|
254
|
+
const results = [];
|
|
255
|
+
for (const key of keys) {
|
|
256
|
+
const entry = getValidEntry(key);
|
|
257
|
+
results.push(entry ? entry.value : null);
|
|
258
|
+
}
|
|
259
|
+
return ok(results);
|
|
260
|
+
},
|
|
261
|
+
async mset(entries) {
|
|
262
|
+
for (const [key, value] of entries) {
|
|
263
|
+
store.set(key, { value });
|
|
264
|
+
}
|
|
265
|
+
return ok(void 0);
|
|
266
|
+
},
|
|
267
|
+
async scan(cursor, options) {
|
|
268
|
+
const allKeys = Array.from(store.keys()).filter((key) => {
|
|
269
|
+
const entry = store.get(key);
|
|
270
|
+
return entry && !isExpired(entry);
|
|
271
|
+
});
|
|
272
|
+
const regex = options?.match ? globToRegex(options.match) : null;
|
|
273
|
+
const filtered = regex ? allKeys.filter((k) => regex.test(k)) : allKeys;
|
|
274
|
+
const count = options?.count || 10;
|
|
275
|
+
const start = cursor;
|
|
276
|
+
const end = Math.min(start + count, filtered.length);
|
|
277
|
+
const keys = filtered.slice(start, end);
|
|
278
|
+
const nextCursor = end >= filtered.length ? 0 : end;
|
|
279
|
+
return ok([nextCursor, keys]);
|
|
280
|
+
},
|
|
281
|
+
async keys(pattern) {
|
|
282
|
+
const regex = globToRegex(pattern);
|
|
283
|
+
const result = [];
|
|
284
|
+
for (const [key] of store.entries()) {
|
|
285
|
+
if (regex.test(key) && getValidEntry(key)) {
|
|
286
|
+
result.push(key);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return ok(result);
|
|
290
|
+
},
|
|
291
|
+
async type(key) {
|
|
292
|
+
if (getValidEntry(key))
|
|
293
|
+
return ok("string");
|
|
294
|
+
if (hashStore.has(key))
|
|
295
|
+
return ok("hash");
|
|
296
|
+
if (listStore.has(key))
|
|
297
|
+
return ok("list");
|
|
298
|
+
if (setStore.has(key))
|
|
299
|
+
return ok("set");
|
|
300
|
+
if (zsetStore.has(key))
|
|
301
|
+
return ok("zset");
|
|
302
|
+
return ok("none");
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
const hash = {
|
|
306
|
+
async hget(key, field) {
|
|
307
|
+
const map = hashStore.get(key);
|
|
308
|
+
if (!map)
|
|
309
|
+
return ok(null);
|
|
310
|
+
const val = map.get(field);
|
|
311
|
+
return ok(val !== void 0 ? deserializeValue(val) : null);
|
|
312
|
+
},
|
|
313
|
+
hset: (async (key, fieldOrData, value) => {
|
|
314
|
+
let map = hashStore.get(key);
|
|
315
|
+
if (!map) {
|
|
316
|
+
map = /* @__PURE__ */ new Map();
|
|
317
|
+
hashStore.set(key, map);
|
|
318
|
+
}
|
|
319
|
+
if (typeof fieldOrData === "string") {
|
|
320
|
+
const existed = map.has(fieldOrData);
|
|
321
|
+
map.set(fieldOrData, serializeValue(value));
|
|
322
|
+
return ok(existed ? 0 : 1);
|
|
323
|
+
}
|
|
324
|
+
let count = 0;
|
|
325
|
+
for (const [f, v] of Object.entries(fieldOrData)) {
|
|
326
|
+
if (!map.has(f))
|
|
327
|
+
count++;
|
|
328
|
+
map.set(f, serializeValue(v));
|
|
329
|
+
}
|
|
330
|
+
return ok(count);
|
|
331
|
+
}),
|
|
332
|
+
async hdel(key, ...fields) {
|
|
333
|
+
const map = hashStore.get(key);
|
|
334
|
+
if (!map)
|
|
335
|
+
return ok(0);
|
|
336
|
+
let count = 0;
|
|
337
|
+
for (const field of fields) {
|
|
338
|
+
if (map.delete(field))
|
|
339
|
+
count++;
|
|
340
|
+
}
|
|
341
|
+
return ok(count);
|
|
342
|
+
},
|
|
343
|
+
async hexists(key, field) {
|
|
344
|
+
const map = hashStore.get(key);
|
|
345
|
+
return ok(map?.has(field) ?? false);
|
|
346
|
+
},
|
|
347
|
+
async hgetall(key) {
|
|
348
|
+
const map = hashStore.get(key);
|
|
349
|
+
if (!map)
|
|
350
|
+
return ok({});
|
|
351
|
+
const result = {};
|
|
352
|
+
for (const [f, v] of map.entries()) {
|
|
353
|
+
result[f] = deserializeValue(v);
|
|
354
|
+
}
|
|
355
|
+
return ok(result);
|
|
356
|
+
},
|
|
357
|
+
async hkeys(key) {
|
|
358
|
+
const map = hashStore.get(key);
|
|
359
|
+
return ok(map ? Array.from(map.keys()) : []);
|
|
360
|
+
},
|
|
361
|
+
async hvals(key) {
|
|
362
|
+
const map = hashStore.get(key);
|
|
363
|
+
if (!map)
|
|
364
|
+
return ok([]);
|
|
365
|
+
return ok(Array.from(map.values()).map((v) => deserializeValue(v)));
|
|
366
|
+
},
|
|
367
|
+
async hlen(key) {
|
|
368
|
+
const map = hashStore.get(key);
|
|
369
|
+
return ok(map?.size ?? 0);
|
|
370
|
+
},
|
|
371
|
+
async hmget(key, ...fields) {
|
|
372
|
+
const map = hashStore.get(key);
|
|
373
|
+
const results = fields.map((field) => {
|
|
374
|
+
const val = map?.get(field);
|
|
375
|
+
return val !== void 0 ? deserializeValue(val) : null;
|
|
376
|
+
});
|
|
377
|
+
return ok(results);
|
|
378
|
+
},
|
|
379
|
+
async hincrBy(key, field, increment) {
|
|
380
|
+
let map = hashStore.get(key);
|
|
381
|
+
if (!map) {
|
|
382
|
+
map = /* @__PURE__ */ new Map();
|
|
383
|
+
hashStore.set(key, map);
|
|
384
|
+
}
|
|
385
|
+
const current = map.has(field) ? Number(deserializeValue(map.get(field))) : 0;
|
|
386
|
+
const newValue = current + increment;
|
|
387
|
+
map.set(field, serializeValue(newValue));
|
|
388
|
+
return ok(newValue);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const list = {
|
|
392
|
+
async lpush(key, ...values) {
|
|
393
|
+
let arr = listStore.get(key);
|
|
394
|
+
if (!arr) {
|
|
395
|
+
arr = [];
|
|
396
|
+
listStore.set(key, arr);
|
|
397
|
+
}
|
|
398
|
+
for (const value of values) {
|
|
399
|
+
arr.unshift(serializeValue(value));
|
|
400
|
+
}
|
|
401
|
+
return ok(arr.length);
|
|
402
|
+
},
|
|
403
|
+
async rpush(key, ...values) {
|
|
404
|
+
let arr = listStore.get(key);
|
|
405
|
+
if (!arr) {
|
|
406
|
+
arr = [];
|
|
407
|
+
listStore.set(key, arr);
|
|
408
|
+
}
|
|
409
|
+
arr.push(...values.map(serializeValue));
|
|
410
|
+
return ok(arr.length);
|
|
411
|
+
},
|
|
412
|
+
async lpop(key) {
|
|
413
|
+
const arr = listStore.get(key);
|
|
414
|
+
if (!arr || arr.length === 0)
|
|
415
|
+
return ok(null);
|
|
416
|
+
const val = arr.shift();
|
|
417
|
+
return ok(deserializeValue(val));
|
|
418
|
+
},
|
|
419
|
+
async rpop(key) {
|
|
420
|
+
const arr = listStore.get(key);
|
|
421
|
+
if (!arr || arr.length === 0)
|
|
422
|
+
return ok(null);
|
|
423
|
+
const val = arr.pop();
|
|
424
|
+
return ok(deserializeValue(val));
|
|
425
|
+
},
|
|
426
|
+
async llen(key) {
|
|
427
|
+
const arr = listStore.get(key);
|
|
428
|
+
return ok(arr?.length ?? 0);
|
|
429
|
+
},
|
|
430
|
+
async lrange(key, start, stop) {
|
|
431
|
+
const arr = listStore.get(key);
|
|
432
|
+
if (!arr)
|
|
433
|
+
return ok([]);
|
|
434
|
+
const len = arr.length;
|
|
435
|
+
const s = start < 0 ? Math.max(len + start, 0) : start;
|
|
436
|
+
const e = stop < 0 ? len + stop + 1 : stop + 1;
|
|
437
|
+
return ok(arr.slice(s, e).map((v) => deserializeValue(v)));
|
|
438
|
+
},
|
|
439
|
+
async lindex(key, index) {
|
|
440
|
+
const arr = listStore.get(key);
|
|
441
|
+
if (!arr)
|
|
442
|
+
return ok(null);
|
|
443
|
+
const i = index < 0 ? arr.length + index : index;
|
|
444
|
+
const val = arr[i];
|
|
445
|
+
return ok(val !== void 0 ? deserializeValue(val) : null);
|
|
446
|
+
},
|
|
447
|
+
async lset(key, index, value) {
|
|
448
|
+
const arr = listStore.get(key);
|
|
449
|
+
if (!arr) {
|
|
450
|
+
return err(HaiCacheError.KEY_NOT_FOUND, cacheM("cache_keyNotFound"));
|
|
451
|
+
}
|
|
452
|
+
const i = index < 0 ? arr.length + index : index;
|
|
453
|
+
if (i < 0 || i >= arr.length) {
|
|
454
|
+
return err(HaiCacheError.OPERATION_FAILED, cacheM("cache_indexOutOfRange"));
|
|
455
|
+
}
|
|
456
|
+
arr[i] = serializeValue(value);
|
|
457
|
+
return ok(void 0);
|
|
458
|
+
},
|
|
459
|
+
async ltrim(key, start, stop) {
|
|
460
|
+
const arr = listStore.get(key);
|
|
461
|
+
if (!arr)
|
|
462
|
+
return ok(void 0);
|
|
463
|
+
const len = arr.length;
|
|
464
|
+
const s = start < 0 ? Math.max(len + start, 0) : start;
|
|
465
|
+
const e = stop < 0 ? len + stop + 1 : stop + 1;
|
|
466
|
+
listStore.set(key, arr.slice(s, e));
|
|
467
|
+
return ok(void 0);
|
|
468
|
+
},
|
|
469
|
+
/**
|
|
470
|
+
* Memory 版本的 blpop 为“非阻塞模拟”:忽略 timeout,立即按 keys 顺序尝试弹出。
|
|
471
|
+
*/
|
|
472
|
+
async blpop(_timeout, ...keys) {
|
|
473
|
+
for (const key of keys) {
|
|
474
|
+
const arr = listStore.get(key);
|
|
475
|
+
if (arr && arr.length > 0) {
|
|
476
|
+
const val = arr.shift();
|
|
477
|
+
return ok([key, deserializeValue(val)]);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return ok(null);
|
|
481
|
+
},
|
|
482
|
+
/**
|
|
483
|
+
* Memory 版本的 brpop 为“非阻塞模拟”:忽略 timeout,立即按 keys 顺序尝试弹出。
|
|
484
|
+
*/
|
|
485
|
+
async brpop(_timeout, ...keys) {
|
|
486
|
+
for (const key of keys) {
|
|
487
|
+
const arr = listStore.get(key);
|
|
488
|
+
if (arr && arr.length > 0) {
|
|
489
|
+
const val = arr.pop();
|
|
490
|
+
return ok([key, deserializeValue(val)]);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return ok(null);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
const set_ = {
|
|
497
|
+
async sadd(key, ...members) {
|
|
498
|
+
let s = setStore.get(key);
|
|
499
|
+
if (!s) {
|
|
500
|
+
s = /* @__PURE__ */ new Set();
|
|
501
|
+
setStore.set(key, s);
|
|
502
|
+
}
|
|
503
|
+
let count = 0;
|
|
504
|
+
for (const member of members) {
|
|
505
|
+
const str = serializeValue(member);
|
|
506
|
+
if (!s.has(str)) {
|
|
507
|
+
s.add(str);
|
|
508
|
+
count++;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return ok(count);
|
|
512
|
+
},
|
|
513
|
+
async srem(key, ...members) {
|
|
514
|
+
const s = setStore.get(key);
|
|
515
|
+
if (!s)
|
|
516
|
+
return ok(0);
|
|
517
|
+
let count = 0;
|
|
518
|
+
for (const member of members) {
|
|
519
|
+
if (s.delete(serializeValue(member)))
|
|
520
|
+
count++;
|
|
521
|
+
}
|
|
522
|
+
return ok(count);
|
|
523
|
+
},
|
|
524
|
+
async smembers(key) {
|
|
525
|
+
const s = setStore.get(key);
|
|
526
|
+
if (!s)
|
|
527
|
+
return ok([]);
|
|
528
|
+
return ok(Array.from(s).map((v) => deserializeValue(v)));
|
|
529
|
+
},
|
|
530
|
+
async sismember(key, member) {
|
|
531
|
+
const s = setStore.get(key);
|
|
532
|
+
return ok(s?.has(serializeValue(member)) ?? false);
|
|
533
|
+
},
|
|
534
|
+
async scard(key) {
|
|
535
|
+
const s = setStore.get(key);
|
|
536
|
+
return ok(s?.size ?? 0);
|
|
537
|
+
},
|
|
538
|
+
async srandmember(key, count) {
|
|
539
|
+
const s = setStore.get(key);
|
|
540
|
+
if (!s || s.size === 0)
|
|
541
|
+
return ok(null);
|
|
542
|
+
const arr = Array.from(s);
|
|
543
|
+
if (count === void 0) {
|
|
544
|
+
const idx = Math.floor(Math.random() * arr.length);
|
|
545
|
+
return ok(deserializeValue(arr[idx]));
|
|
546
|
+
}
|
|
547
|
+
const result = [];
|
|
548
|
+
for (let i = 0; i < Math.min(Math.abs(count), arr.length); i++) {
|
|
549
|
+
const idx = Math.floor(Math.random() * arr.length);
|
|
550
|
+
result.push(deserializeValue(arr[idx]));
|
|
551
|
+
}
|
|
552
|
+
return ok(result);
|
|
553
|
+
},
|
|
554
|
+
async spop(key, count) {
|
|
555
|
+
const s = setStore.get(key);
|
|
556
|
+
if (!s || s.size === 0)
|
|
557
|
+
return ok(null);
|
|
558
|
+
const arr = Array.from(s);
|
|
559
|
+
if (count === void 0) {
|
|
560
|
+
const idx = Math.floor(Math.random() * arr.length);
|
|
561
|
+
const val = arr[idx];
|
|
562
|
+
s.delete(val);
|
|
563
|
+
return ok(deserializeValue(val));
|
|
564
|
+
}
|
|
565
|
+
const result = [];
|
|
566
|
+
for (let i = 0; i < Math.min(count, arr.length); i++) {
|
|
567
|
+
const idx = Math.floor(Math.random() * arr.length);
|
|
568
|
+
const val = arr.splice(idx, 1)[0];
|
|
569
|
+
s.delete(val);
|
|
570
|
+
result.push(deserializeValue(val));
|
|
571
|
+
}
|
|
572
|
+
return ok(result);
|
|
573
|
+
},
|
|
574
|
+
async sinter(...keys) {
|
|
575
|
+
if (keys.length === 0)
|
|
576
|
+
return ok([]);
|
|
577
|
+
const sets = keys.map((k) => setStore.get(k));
|
|
578
|
+
if (sets.some((s) => !s))
|
|
579
|
+
return ok([]);
|
|
580
|
+
const [first, ...rest] = sets;
|
|
581
|
+
const result = Array.from(first).filter((v) => rest.every((s) => s.has(v)));
|
|
582
|
+
return ok(result.map((v) => deserializeValue(v)));
|
|
583
|
+
},
|
|
584
|
+
async sunion(...keys) {
|
|
585
|
+
const union = /* @__PURE__ */ new Set();
|
|
586
|
+
for (const key of keys) {
|
|
587
|
+
const s = setStore.get(key);
|
|
588
|
+
if (s) {
|
|
589
|
+
for (const v of s) union.add(v);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return ok(Array.from(union).map((v) => deserializeValue(v)));
|
|
593
|
+
},
|
|
594
|
+
async sdiff(...keys) {
|
|
595
|
+
if (keys.length === 0)
|
|
596
|
+
return ok([]);
|
|
597
|
+
const first = setStore.get(keys[0]);
|
|
598
|
+
if (!first)
|
|
599
|
+
return ok([]);
|
|
600
|
+
const rest = keys.slice(1).map((k) => setStore.get(k)).filter(Boolean);
|
|
601
|
+
const result = Array.from(first).filter((v) => !rest.some((s) => s.has(v)));
|
|
602
|
+
return ok(result.map((v) => deserializeValue(v)));
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
const zset = {
|
|
606
|
+
async zadd(key, ...members) {
|
|
607
|
+
let map = zsetStore.get(key);
|
|
608
|
+
if (!map) {
|
|
609
|
+
map = /* @__PURE__ */ new Map();
|
|
610
|
+
zsetStore.set(key, map);
|
|
611
|
+
}
|
|
612
|
+
let count = 0;
|
|
613
|
+
for (const { score, member } of members) {
|
|
614
|
+
if (!map.has(member))
|
|
615
|
+
count++;
|
|
616
|
+
map.set(member, score);
|
|
617
|
+
}
|
|
618
|
+
return ok(count);
|
|
619
|
+
},
|
|
620
|
+
async zrem(key, ...members) {
|
|
621
|
+
const map = zsetStore.get(key);
|
|
622
|
+
if (!map)
|
|
623
|
+
return ok(0);
|
|
624
|
+
let count = 0;
|
|
625
|
+
for (const member of members) {
|
|
626
|
+
if (map.delete(member))
|
|
627
|
+
count++;
|
|
628
|
+
}
|
|
629
|
+
return ok(count);
|
|
630
|
+
},
|
|
631
|
+
async zscore(key, member) {
|
|
632
|
+
const map = zsetStore.get(key);
|
|
633
|
+
const score = map?.get(member);
|
|
634
|
+
return ok(score !== void 0 ? score : null);
|
|
635
|
+
},
|
|
636
|
+
async zrank(key, member) {
|
|
637
|
+
const map = zsetStore.get(key);
|
|
638
|
+
if (!map || !map.has(member))
|
|
639
|
+
return ok(null);
|
|
640
|
+
const sorted = Array.from(map.entries()).sort((a, b) => a[1] - b[1]);
|
|
641
|
+
const idx = sorted.findIndex(([m]) => m === member);
|
|
642
|
+
return ok(idx >= 0 ? idx : null);
|
|
643
|
+
},
|
|
644
|
+
async zrevrank(key, member) {
|
|
645
|
+
const map = zsetStore.get(key);
|
|
646
|
+
if (!map || !map.has(member))
|
|
647
|
+
return ok(null);
|
|
648
|
+
const sorted = Array.from(map.entries()).sort((a, b) => b[1] - a[1]);
|
|
649
|
+
const idx = sorted.findIndex(([m]) => m === member);
|
|
650
|
+
return ok(idx >= 0 ? idx : null);
|
|
651
|
+
},
|
|
652
|
+
async zrange(key, start, stop, withScores) {
|
|
653
|
+
const map = zsetStore.get(key);
|
|
654
|
+
if (!map)
|
|
655
|
+
return ok([]);
|
|
656
|
+
const sorted = Array.from(map.entries()).sort((a, b) => a[1] - b[1]);
|
|
657
|
+
const len = sorted.length;
|
|
658
|
+
const s = start < 0 ? Math.max(len + start, 0) : start;
|
|
659
|
+
const e = stop < 0 ? len + stop + 1 : stop + 1;
|
|
660
|
+
const slice = sorted.slice(s, e);
|
|
661
|
+
if (withScores) {
|
|
662
|
+
return ok(slice.map(([member, score]) => ({ member, score })));
|
|
663
|
+
}
|
|
664
|
+
return ok(slice.map(([member]) => member));
|
|
665
|
+
},
|
|
666
|
+
async zrevrange(key, start, stop, withScores) {
|
|
667
|
+
const map = zsetStore.get(key);
|
|
668
|
+
if (!map)
|
|
669
|
+
return ok([]);
|
|
670
|
+
const sorted = Array.from(map.entries()).sort((a, b) => b[1] - a[1]);
|
|
671
|
+
const len = sorted.length;
|
|
672
|
+
const s = start < 0 ? Math.max(len + start, 0) : start;
|
|
673
|
+
const e = stop < 0 ? len + stop + 1 : stop + 1;
|
|
674
|
+
const slice = sorted.slice(s, e);
|
|
675
|
+
if (withScores) {
|
|
676
|
+
return ok(slice.map(([member, score]) => ({ member, score })));
|
|
677
|
+
}
|
|
678
|
+
return ok(slice.map(([member]) => member));
|
|
679
|
+
},
|
|
680
|
+
async zrangeByScore(key, min, max, options) {
|
|
681
|
+
const map = zsetStore.get(key);
|
|
682
|
+
if (!map)
|
|
683
|
+
return ok([]);
|
|
684
|
+
const minVal = min === "-inf" ? Number.NEGATIVE_INFINITY : Number(min);
|
|
685
|
+
const maxVal = max === "+inf" ? Number.POSITIVE_INFINITY : Number(max);
|
|
686
|
+
let sorted = Array.from(map.entries()).filter(([, score]) => score >= minVal && score <= maxVal).sort((a, b) => a[1] - b[1]);
|
|
687
|
+
if (options?.offset !== void 0 || options?.count !== void 0) {
|
|
688
|
+
const offset = options.offset ?? 0;
|
|
689
|
+
const count = options.count ?? sorted.length;
|
|
690
|
+
sorted = sorted.slice(offset, offset + count);
|
|
691
|
+
}
|
|
692
|
+
if (options?.withScores) {
|
|
693
|
+
return ok(sorted.map(([member, score]) => ({ member, score })));
|
|
694
|
+
}
|
|
695
|
+
return ok(sorted.map(([member]) => member));
|
|
696
|
+
},
|
|
697
|
+
async zcard(key) {
|
|
698
|
+
const map = zsetStore.get(key);
|
|
699
|
+
return ok(map?.size ?? 0);
|
|
700
|
+
},
|
|
701
|
+
async zcount(key, min, max) {
|
|
702
|
+
const map = zsetStore.get(key);
|
|
703
|
+
if (!map)
|
|
704
|
+
return ok(0);
|
|
705
|
+
const minVal = min === "-inf" ? Number.NEGATIVE_INFINITY : Number(min);
|
|
706
|
+
const maxVal = max === "+inf" ? Number.POSITIVE_INFINITY : Number(max);
|
|
707
|
+
let count = 0;
|
|
708
|
+
for (const score of map.values()) {
|
|
709
|
+
if (score >= minVal && score <= maxVal)
|
|
710
|
+
count++;
|
|
711
|
+
}
|
|
712
|
+
return ok(count);
|
|
713
|
+
},
|
|
714
|
+
async zincrBy(key, increment, member) {
|
|
715
|
+
let map = zsetStore.get(key);
|
|
716
|
+
if (!map) {
|
|
717
|
+
map = /* @__PURE__ */ new Map();
|
|
718
|
+
zsetStore.set(key, map);
|
|
719
|
+
}
|
|
720
|
+
const current = map.get(member) ?? 0;
|
|
721
|
+
const newScore = current + increment;
|
|
722
|
+
map.set(member, newScore);
|
|
723
|
+
return ok(newScore);
|
|
724
|
+
},
|
|
725
|
+
async zremRangeByRank(key, start, stop) {
|
|
726
|
+
const map = zsetStore.get(key);
|
|
727
|
+
if (!map)
|
|
728
|
+
return ok(0);
|
|
729
|
+
const sorted = Array.from(map.entries()).sort((a, b) => a[1] - b[1]);
|
|
730
|
+
const len = sorted.length;
|
|
731
|
+
const s = start < 0 ? Math.max(len + start, 0) : start;
|
|
732
|
+
const e = stop < 0 ? len + stop + 1 : stop + 1;
|
|
733
|
+
const toRemove = sorted.slice(s, e);
|
|
734
|
+
for (const [member] of toRemove) {
|
|
735
|
+
map.delete(member);
|
|
736
|
+
}
|
|
737
|
+
return ok(toRemove.length);
|
|
738
|
+
},
|
|
739
|
+
async zremRangeByScore(key, min, max) {
|
|
740
|
+
const map = zsetStore.get(key);
|
|
741
|
+
if (!map)
|
|
742
|
+
return ok(0);
|
|
743
|
+
const minVal = min === "-inf" ? Number.NEGATIVE_INFINITY : Number(min);
|
|
744
|
+
const maxVal = max === "+inf" ? Number.POSITIVE_INFINITY : Number(max);
|
|
745
|
+
let count = 0;
|
|
746
|
+
for (const [member, score] of map.entries()) {
|
|
747
|
+
if (score >= minVal && score <= maxVal) {
|
|
748
|
+
map.delete(member);
|
|
749
|
+
count++;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return ok(count);
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
const LOCK_PREFIX = "__lock:";
|
|
756
|
+
const lock = {
|
|
757
|
+
async acquire(key, options) {
|
|
758
|
+
const lockKey = `${LOCK_PREFIX}${key}`;
|
|
759
|
+
const ttl = options?.ttl ?? 30;
|
|
760
|
+
const owner = options?.owner ?? "default";
|
|
761
|
+
const existing = getValidEntry(lockKey);
|
|
762
|
+
if (existing) {
|
|
763
|
+
return ok(false);
|
|
764
|
+
}
|
|
765
|
+
store.set(lockKey, {
|
|
766
|
+
value: owner,
|
|
767
|
+
expiresAt: Date.now() + ttl * 1e3
|
|
768
|
+
});
|
|
769
|
+
return ok(true);
|
|
770
|
+
},
|
|
771
|
+
async release(key, owner) {
|
|
772
|
+
const lockKey = `${LOCK_PREFIX}${key}`;
|
|
773
|
+
const entry = getValidEntry(lockKey);
|
|
774
|
+
if (!entry) {
|
|
775
|
+
return ok(false);
|
|
776
|
+
}
|
|
777
|
+
if (owner !== void 0 && entry.value !== owner) {
|
|
778
|
+
return ok(false);
|
|
779
|
+
}
|
|
780
|
+
store.delete(lockKey);
|
|
781
|
+
return ok(true);
|
|
782
|
+
},
|
|
783
|
+
async isLocked(key) {
|
|
784
|
+
const lockKey = `${LOCK_PREFIX}${key}`;
|
|
785
|
+
const entry = getValidEntry(lockKey);
|
|
786
|
+
return ok(entry !== null);
|
|
787
|
+
},
|
|
788
|
+
async extend(key, ttl, owner) {
|
|
789
|
+
const lockKey = `${LOCK_PREFIX}${key}`;
|
|
790
|
+
const entry = getValidEntry(lockKey);
|
|
791
|
+
if (!entry) {
|
|
792
|
+
return ok(false);
|
|
793
|
+
}
|
|
794
|
+
if (owner !== void 0 && entry.value !== owner) {
|
|
795
|
+
return ok(false);
|
|
796
|
+
}
|
|
797
|
+
entry.expiresAt = Date.now() + ttl * 1e3;
|
|
798
|
+
return ok(true);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
return {
|
|
802
|
+
name: "memory",
|
|
803
|
+
async connect(_config) {
|
|
804
|
+
if (connected)
|
|
805
|
+
return ok(void 0);
|
|
806
|
+
cleanupTimer = setInterval(cleanup, 6e4);
|
|
807
|
+
connected = true;
|
|
808
|
+
return ok(void 0);
|
|
809
|
+
},
|
|
810
|
+
async close() {
|
|
811
|
+
if (cleanupTimer) {
|
|
812
|
+
clearInterval(cleanupTimer);
|
|
813
|
+
cleanupTimer = null;
|
|
814
|
+
}
|
|
815
|
+
store.clear();
|
|
816
|
+
hashStore.clear();
|
|
817
|
+
listStore.clear();
|
|
818
|
+
setStore.clear();
|
|
819
|
+
zsetStore.clear();
|
|
820
|
+
connected = false;
|
|
821
|
+
},
|
|
822
|
+
isConnected: () => connected,
|
|
823
|
+
kv,
|
|
824
|
+
hash,
|
|
825
|
+
list,
|
|
826
|
+
set_,
|
|
827
|
+
zset,
|
|
828
|
+
lock,
|
|
829
|
+
async ping() {
|
|
830
|
+
return ok("PONG");
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
var logger = core.logger.child({ module: "cache", scope: "redis" });
|
|
835
|
+
function sanitizeRedisUrl(url) {
|
|
836
|
+
try {
|
|
837
|
+
const u = new URL(url);
|
|
838
|
+
if (u.password)
|
|
839
|
+
u.password = "***";
|
|
840
|
+
if (u.username)
|
|
841
|
+
u.username = "***";
|
|
842
|
+
return u.toString();
|
|
843
|
+
} catch {
|
|
844
|
+
return "(invalid url)";
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
var RELEASE_LOCK_SCRIPT = `
|
|
848
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
849
|
+
return redis.call("del", KEYS[1])
|
|
850
|
+
else
|
|
851
|
+
return 0
|
|
852
|
+
end
|
|
853
|
+
`;
|
|
854
|
+
var EXTEND_LOCK_SCRIPT = `
|
|
855
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
856
|
+
return redis.call("expire", KEYS[1], ARGV[2])
|
|
857
|
+
else
|
|
858
|
+
return 0
|
|
859
|
+
end
|
|
860
|
+
`;
|
|
861
|
+
function createRedisProvider() {
|
|
862
|
+
let client = null;
|
|
863
|
+
function serialize(value) {
|
|
864
|
+
return JSON.stringify(value);
|
|
865
|
+
}
|
|
866
|
+
function deserialize(value) {
|
|
867
|
+
if (value === null)
|
|
868
|
+
return null;
|
|
869
|
+
try {
|
|
870
|
+
return JSON.parse(value);
|
|
871
|
+
} catch {
|
|
872
|
+
return value;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function wrapOperation(operation) {
|
|
876
|
+
if (!client) {
|
|
877
|
+
return err(
|
|
878
|
+
HaiCacheError.NOT_INITIALIZED,
|
|
879
|
+
cacheM("cache_notInitialized")
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
const result = await operation();
|
|
884
|
+
return ok(result);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
return err(
|
|
887
|
+
HaiCacheError.OPERATION_FAILED,
|
|
888
|
+
cacheM("cache_operationFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
889
|
+
error
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const kv = {
|
|
894
|
+
async get(key) {
|
|
895
|
+
return wrapOperation(async () => {
|
|
896
|
+
const value = await client.get(key);
|
|
897
|
+
return deserialize(value);
|
|
898
|
+
});
|
|
899
|
+
},
|
|
900
|
+
async set(key, value, options) {
|
|
901
|
+
return wrapOperation(async () => {
|
|
902
|
+
const args = [key, serialize(value)];
|
|
903
|
+
if (options?.ex)
|
|
904
|
+
args.push("EX", options.ex);
|
|
905
|
+
else if (options?.px)
|
|
906
|
+
args.push("PX", options.px);
|
|
907
|
+
else if (options?.exat)
|
|
908
|
+
args.push("EXAT", options.exat);
|
|
909
|
+
else if (options?.pxat)
|
|
910
|
+
args.push("PXAT", options.pxat);
|
|
911
|
+
if (options?.nx)
|
|
912
|
+
args.push("NX");
|
|
913
|
+
else if (options?.xx)
|
|
914
|
+
args.push("XX");
|
|
915
|
+
if (options?.keepTtl)
|
|
916
|
+
args.push("KEEPTTL");
|
|
917
|
+
await client.set(...args);
|
|
918
|
+
});
|
|
919
|
+
},
|
|
920
|
+
async del(...keys) {
|
|
921
|
+
return wrapOperation(() => client.del(...keys));
|
|
922
|
+
},
|
|
923
|
+
async exists(...keys) {
|
|
924
|
+
return wrapOperation(() => client.exists(...keys));
|
|
925
|
+
},
|
|
926
|
+
async expire(key, seconds) {
|
|
927
|
+
return wrapOperation(async () => {
|
|
928
|
+
const result = await client.expire(key, seconds);
|
|
929
|
+
return result === 1;
|
|
930
|
+
});
|
|
931
|
+
},
|
|
932
|
+
async expireAt(key, timestamp) {
|
|
933
|
+
return wrapOperation(async () => {
|
|
934
|
+
const result = await client.expireat(key, timestamp);
|
|
935
|
+
return result === 1;
|
|
936
|
+
});
|
|
937
|
+
},
|
|
938
|
+
async ttl(key) {
|
|
939
|
+
return wrapOperation(() => client.ttl(key));
|
|
940
|
+
},
|
|
941
|
+
async persist(key) {
|
|
942
|
+
return wrapOperation(async () => {
|
|
943
|
+
const result = await client.persist(key);
|
|
944
|
+
return result === 1;
|
|
945
|
+
});
|
|
946
|
+
},
|
|
947
|
+
async incr(key) {
|
|
948
|
+
return wrapOperation(() => client.incr(key));
|
|
949
|
+
},
|
|
950
|
+
async incrBy(key, increment) {
|
|
951
|
+
return wrapOperation(() => client.incrby(key, increment));
|
|
952
|
+
},
|
|
953
|
+
async decr(key) {
|
|
954
|
+
return wrapOperation(() => client.decr(key));
|
|
955
|
+
},
|
|
956
|
+
async decrBy(key, decrement) {
|
|
957
|
+
return wrapOperation(() => client.decrby(key, decrement));
|
|
958
|
+
},
|
|
959
|
+
async mget(...keys) {
|
|
960
|
+
return wrapOperation(async () => {
|
|
961
|
+
const values = await client.mget(...keys);
|
|
962
|
+
return values.map((v) => deserialize(v));
|
|
963
|
+
});
|
|
964
|
+
},
|
|
965
|
+
async mset(entries) {
|
|
966
|
+
return wrapOperation(async () => {
|
|
967
|
+
const args = [];
|
|
968
|
+
for (const [key, value] of entries) {
|
|
969
|
+
args.push(key, serialize(value));
|
|
970
|
+
}
|
|
971
|
+
await client.mset(...args);
|
|
972
|
+
});
|
|
973
|
+
},
|
|
974
|
+
async scan(cursor, options) {
|
|
975
|
+
return wrapOperation(async () => {
|
|
976
|
+
let result;
|
|
977
|
+
if (options?.match && options?.count) {
|
|
978
|
+
result = await client.scan(cursor, "MATCH", options.match, "COUNT", options.count);
|
|
979
|
+
} else if (options?.match) {
|
|
980
|
+
result = await client.scan(cursor, "MATCH", options.match);
|
|
981
|
+
} else if (options?.count) {
|
|
982
|
+
result = await client.scan(cursor, "COUNT", options.count);
|
|
983
|
+
} else {
|
|
984
|
+
result = await client.scan(cursor);
|
|
985
|
+
}
|
|
986
|
+
const [nextCursor, keys] = result;
|
|
987
|
+
return [Number.parseInt(nextCursor, 10), keys];
|
|
988
|
+
});
|
|
989
|
+
},
|
|
990
|
+
async keys(pattern) {
|
|
991
|
+
return wrapOperation(() => client.keys(pattern));
|
|
992
|
+
},
|
|
993
|
+
async type(key) {
|
|
994
|
+
return wrapOperation(() => client.type(key));
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
const hash = {
|
|
998
|
+
async hget(key, field) {
|
|
999
|
+
return wrapOperation(async () => {
|
|
1000
|
+
const value = await client.hget(key, field);
|
|
1001
|
+
return deserialize(value);
|
|
1002
|
+
});
|
|
1003
|
+
},
|
|
1004
|
+
async hset(key, fieldOrData, value) {
|
|
1005
|
+
return wrapOperation(async () => {
|
|
1006
|
+
if (typeof fieldOrData === "string" && value !== void 0) {
|
|
1007
|
+
return client.hset(key, fieldOrData, serialize(value));
|
|
1008
|
+
}
|
|
1009
|
+
if (typeof fieldOrData === "object") {
|
|
1010
|
+
const serialized = {};
|
|
1011
|
+
for (const [k, v] of Object.entries(fieldOrData)) {
|
|
1012
|
+
serialized[k] = serialize(v);
|
|
1013
|
+
}
|
|
1014
|
+
return client.hset(key, serialized);
|
|
1015
|
+
}
|
|
1016
|
+
return 0;
|
|
1017
|
+
});
|
|
1018
|
+
},
|
|
1019
|
+
async hdel(key, ...fields) {
|
|
1020
|
+
return wrapOperation(() => client.hdel(key, ...fields));
|
|
1021
|
+
},
|
|
1022
|
+
async hexists(key, field) {
|
|
1023
|
+
return wrapOperation(async () => {
|
|
1024
|
+
const result = await client.hexists(key, field);
|
|
1025
|
+
return result === 1;
|
|
1026
|
+
});
|
|
1027
|
+
},
|
|
1028
|
+
async hgetall(key) {
|
|
1029
|
+
return wrapOperation(async () => {
|
|
1030
|
+
const data = await client.hgetall(key);
|
|
1031
|
+
const result = {};
|
|
1032
|
+
for (const [k, v] of Object.entries(data)) {
|
|
1033
|
+
result[k] = deserialize(v);
|
|
1034
|
+
}
|
|
1035
|
+
return result;
|
|
1036
|
+
});
|
|
1037
|
+
},
|
|
1038
|
+
async hkeys(key) {
|
|
1039
|
+
return wrapOperation(() => client.hkeys(key));
|
|
1040
|
+
},
|
|
1041
|
+
async hvals(key) {
|
|
1042
|
+
return wrapOperation(async () => {
|
|
1043
|
+
const values = await client.hvals(key);
|
|
1044
|
+
return values.map((v) => deserialize(v));
|
|
1045
|
+
});
|
|
1046
|
+
},
|
|
1047
|
+
async hlen(key) {
|
|
1048
|
+
return wrapOperation(() => client.hlen(key));
|
|
1049
|
+
},
|
|
1050
|
+
async hmget(key, ...fields) {
|
|
1051
|
+
return wrapOperation(async () => {
|
|
1052
|
+
const values = await client.hmget(key, ...fields);
|
|
1053
|
+
return values.map((v) => deserialize(v));
|
|
1054
|
+
});
|
|
1055
|
+
},
|
|
1056
|
+
async hincrBy(key, field, increment) {
|
|
1057
|
+
return wrapOperation(() => client.hincrby(key, field, increment));
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
const list = {
|
|
1061
|
+
async lpush(key, ...values) {
|
|
1062
|
+
return wrapOperation(() => client.lpush(key, ...values.map(serialize)));
|
|
1063
|
+
},
|
|
1064
|
+
async rpush(key, ...values) {
|
|
1065
|
+
return wrapOperation(() => client.rpush(key, ...values.map(serialize)));
|
|
1066
|
+
},
|
|
1067
|
+
async lpop(key) {
|
|
1068
|
+
return wrapOperation(async () => {
|
|
1069
|
+
const value = await client.lpop(key);
|
|
1070
|
+
return deserialize(value);
|
|
1071
|
+
});
|
|
1072
|
+
},
|
|
1073
|
+
async rpop(key) {
|
|
1074
|
+
return wrapOperation(async () => {
|
|
1075
|
+
const value = await client.rpop(key);
|
|
1076
|
+
return deserialize(value);
|
|
1077
|
+
});
|
|
1078
|
+
},
|
|
1079
|
+
async llen(key) {
|
|
1080
|
+
return wrapOperation(() => client.llen(key));
|
|
1081
|
+
},
|
|
1082
|
+
async lrange(key, start, stop) {
|
|
1083
|
+
return wrapOperation(async () => {
|
|
1084
|
+
const values = await client.lrange(key, start, stop);
|
|
1085
|
+
return values.map((v) => deserialize(v));
|
|
1086
|
+
});
|
|
1087
|
+
},
|
|
1088
|
+
async lindex(key, index) {
|
|
1089
|
+
return wrapOperation(async () => {
|
|
1090
|
+
const value = await client.lindex(key, index);
|
|
1091
|
+
return deserialize(value);
|
|
1092
|
+
});
|
|
1093
|
+
},
|
|
1094
|
+
async lset(key, index, value) {
|
|
1095
|
+
return wrapOperation(async () => {
|
|
1096
|
+
await client.lset(key, index, serialize(value));
|
|
1097
|
+
});
|
|
1098
|
+
},
|
|
1099
|
+
async ltrim(key, start, stop) {
|
|
1100
|
+
return wrapOperation(async () => {
|
|
1101
|
+
await client.ltrim(key, start, stop);
|
|
1102
|
+
});
|
|
1103
|
+
},
|
|
1104
|
+
async blpop(timeout, ...keys) {
|
|
1105
|
+
return wrapOperation(async () => {
|
|
1106
|
+
const result = await client.blpop(...keys, timeout);
|
|
1107
|
+
if (!result)
|
|
1108
|
+
return null;
|
|
1109
|
+
return [result[0], deserialize(result[1])];
|
|
1110
|
+
});
|
|
1111
|
+
},
|
|
1112
|
+
async brpop(timeout, ...keys) {
|
|
1113
|
+
return wrapOperation(async () => {
|
|
1114
|
+
const result = await client.brpop(...keys, timeout);
|
|
1115
|
+
if (!result)
|
|
1116
|
+
return null;
|
|
1117
|
+
return [result[0], deserialize(result[1])];
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
const set_ = {
|
|
1122
|
+
async sadd(key, ...members) {
|
|
1123
|
+
return wrapOperation(() => client.sadd(key, ...members.map(serialize)));
|
|
1124
|
+
},
|
|
1125
|
+
async srem(key, ...members) {
|
|
1126
|
+
return wrapOperation(() => client.srem(key, ...members.map(serialize)));
|
|
1127
|
+
},
|
|
1128
|
+
async smembers(key) {
|
|
1129
|
+
return wrapOperation(async () => {
|
|
1130
|
+
const members = await client.smembers(key);
|
|
1131
|
+
return members.map((m) => deserialize(m));
|
|
1132
|
+
});
|
|
1133
|
+
},
|
|
1134
|
+
async sismember(key, member) {
|
|
1135
|
+
return wrapOperation(async () => {
|
|
1136
|
+
const result = await client.sismember(key, serialize(member));
|
|
1137
|
+
return result === 1;
|
|
1138
|
+
});
|
|
1139
|
+
},
|
|
1140
|
+
async scard(key) {
|
|
1141
|
+
return wrapOperation(() => client.scard(key));
|
|
1142
|
+
},
|
|
1143
|
+
async srandmember(key, count) {
|
|
1144
|
+
return wrapOperation(async () => {
|
|
1145
|
+
if (count !== void 0) {
|
|
1146
|
+
const members = await client.srandmember(key, count);
|
|
1147
|
+
return members.map((m) => deserialize(m));
|
|
1148
|
+
}
|
|
1149
|
+
const member = await client.srandmember(key);
|
|
1150
|
+
return deserialize(member);
|
|
1151
|
+
});
|
|
1152
|
+
},
|
|
1153
|
+
async spop(key, count) {
|
|
1154
|
+
return wrapOperation(async () => {
|
|
1155
|
+
if (count !== void 0) {
|
|
1156
|
+
const members = await client.spop(key, count);
|
|
1157
|
+
return members.map((m) => deserialize(m));
|
|
1158
|
+
}
|
|
1159
|
+
const member = await client.spop(key);
|
|
1160
|
+
return deserialize(member);
|
|
1161
|
+
});
|
|
1162
|
+
},
|
|
1163
|
+
async sinter(...keys) {
|
|
1164
|
+
return wrapOperation(async () => {
|
|
1165
|
+
const members = await client.sinter(...keys);
|
|
1166
|
+
return members.map((m) => deserialize(m));
|
|
1167
|
+
});
|
|
1168
|
+
},
|
|
1169
|
+
async sunion(...keys) {
|
|
1170
|
+
return wrapOperation(async () => {
|
|
1171
|
+
const members = await client.sunion(...keys);
|
|
1172
|
+
return members.map((m) => deserialize(m));
|
|
1173
|
+
});
|
|
1174
|
+
},
|
|
1175
|
+
async sdiff(...keys) {
|
|
1176
|
+
return wrapOperation(async () => {
|
|
1177
|
+
const members = await client.sdiff(...keys);
|
|
1178
|
+
return members.map((m) => deserialize(m));
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
const zset = {
|
|
1183
|
+
async zadd(key, ...members) {
|
|
1184
|
+
return wrapOperation(async () => {
|
|
1185
|
+
const args = [];
|
|
1186
|
+
for (const { score, member } of members) {
|
|
1187
|
+
args.push(score, member);
|
|
1188
|
+
}
|
|
1189
|
+
return client.zadd(key, ...args);
|
|
1190
|
+
});
|
|
1191
|
+
},
|
|
1192
|
+
async zrem(key, ...members) {
|
|
1193
|
+
return wrapOperation(() => client.zrem(key, ...members));
|
|
1194
|
+
},
|
|
1195
|
+
async zscore(key, member) {
|
|
1196
|
+
return wrapOperation(async () => {
|
|
1197
|
+
const score = await client.zscore(key, member);
|
|
1198
|
+
return score !== null ? Number.parseFloat(score) : null;
|
|
1199
|
+
});
|
|
1200
|
+
},
|
|
1201
|
+
async zrank(key, member) {
|
|
1202
|
+
return wrapOperation(() => client.zrank(key, member));
|
|
1203
|
+
},
|
|
1204
|
+
async zrevrank(key, member) {
|
|
1205
|
+
return wrapOperation(() => client.zrevrank(key, member));
|
|
1206
|
+
},
|
|
1207
|
+
async zrange(key, start, stop, withScores) {
|
|
1208
|
+
return wrapOperation(async () => {
|
|
1209
|
+
if (withScores) {
|
|
1210
|
+
const result = await client.zrange(key, start, stop, "WITHSCORES");
|
|
1211
|
+
const members = [];
|
|
1212
|
+
for (let i = 0; i < result.length; i += 2) {
|
|
1213
|
+
members.push({ member: result[i], score: Number.parseFloat(result[i + 1]) });
|
|
1214
|
+
}
|
|
1215
|
+
return members;
|
|
1216
|
+
}
|
|
1217
|
+
return client.zrange(key, start, stop);
|
|
1218
|
+
});
|
|
1219
|
+
},
|
|
1220
|
+
async zrevrange(key, start, stop, withScores) {
|
|
1221
|
+
return wrapOperation(async () => {
|
|
1222
|
+
if (withScores) {
|
|
1223
|
+
const result = await client.zrevrange(key, start, stop, "WITHSCORES");
|
|
1224
|
+
const members = [];
|
|
1225
|
+
for (let i = 0; i < result.length; i += 2) {
|
|
1226
|
+
members.push({ member: result[i], score: Number.parseFloat(result[i + 1]) });
|
|
1227
|
+
}
|
|
1228
|
+
return members;
|
|
1229
|
+
}
|
|
1230
|
+
return client.zrevrange(key, start, stop);
|
|
1231
|
+
});
|
|
1232
|
+
},
|
|
1233
|
+
async zrangeByScore(key, min, max, options) {
|
|
1234
|
+
return wrapOperation(async () => {
|
|
1235
|
+
const args = [key, String(min), String(max)];
|
|
1236
|
+
if (options?.withScores)
|
|
1237
|
+
args.push("WITHSCORES");
|
|
1238
|
+
if (options?.offset !== void 0 && options?.count !== void 0) {
|
|
1239
|
+
args.push("LIMIT", options.offset, options.count);
|
|
1240
|
+
}
|
|
1241
|
+
const result = await client.zrangebyscore.call(client, ...args);
|
|
1242
|
+
if (options?.withScores) {
|
|
1243
|
+
const members = [];
|
|
1244
|
+
for (let i = 0; i < result.length; i += 2) {
|
|
1245
|
+
members.push({ member: result[i], score: Number.parseFloat(result[i + 1]) });
|
|
1246
|
+
}
|
|
1247
|
+
return members;
|
|
1248
|
+
}
|
|
1249
|
+
return result;
|
|
1250
|
+
});
|
|
1251
|
+
},
|
|
1252
|
+
async zcard(key) {
|
|
1253
|
+
return wrapOperation(() => client.zcard(key));
|
|
1254
|
+
},
|
|
1255
|
+
async zcount(key, min, max) {
|
|
1256
|
+
return wrapOperation(() => client.zcount(key, min, max));
|
|
1257
|
+
},
|
|
1258
|
+
async zincrBy(key, increment, member) {
|
|
1259
|
+
return wrapOperation(async () => {
|
|
1260
|
+
const result = await client.zincrby(key, increment, member);
|
|
1261
|
+
return Number.parseFloat(result);
|
|
1262
|
+
});
|
|
1263
|
+
},
|
|
1264
|
+
async zremRangeByRank(key, start, stop) {
|
|
1265
|
+
return wrapOperation(() => client.zremrangebyrank(key, start, stop));
|
|
1266
|
+
},
|
|
1267
|
+
async zremRangeByScore(key, min, max) {
|
|
1268
|
+
return wrapOperation(() => client.zremrangebyscore(key, min, max));
|
|
1269
|
+
}
|
|
1270
|
+
};
|
|
1271
|
+
const LOCK_PREFIX = "__lock:";
|
|
1272
|
+
const lock = {
|
|
1273
|
+
async acquire(key, options) {
|
|
1274
|
+
return wrapOperation(async () => {
|
|
1275
|
+
const lockKey = `${LOCK_PREFIX}${key}`;
|
|
1276
|
+
const ttl = options?.ttl ?? 30;
|
|
1277
|
+
const owner = options?.owner ?? "default";
|
|
1278
|
+
const result = await client.set(lockKey, owner, "EX", ttl, "NX");
|
|
1279
|
+
return result === "OK";
|
|
1280
|
+
});
|
|
1281
|
+
},
|
|
1282
|
+
async release(key, owner) {
|
|
1283
|
+
return wrapOperation(async () => {
|
|
1284
|
+
const lockKey = `${LOCK_PREFIX}${key}`;
|
|
1285
|
+
if (owner !== void 0) {
|
|
1286
|
+
const result = await client.eval(RELEASE_LOCK_SCRIPT, 1, lockKey, owner);
|
|
1287
|
+
return result === 1;
|
|
1288
|
+
}
|
|
1289
|
+
const deleted = await client.del(lockKey);
|
|
1290
|
+
return deleted === 1;
|
|
1291
|
+
});
|
|
1292
|
+
},
|
|
1293
|
+
async isLocked(key) {
|
|
1294
|
+
return wrapOperation(async () => {
|
|
1295
|
+
const lockKey = `${LOCK_PREFIX}${key}`;
|
|
1296
|
+
const exists = await client.exists(lockKey);
|
|
1297
|
+
return exists === 1;
|
|
1298
|
+
});
|
|
1299
|
+
},
|
|
1300
|
+
async extend(key, ttl, owner) {
|
|
1301
|
+
return wrapOperation(async () => {
|
|
1302
|
+
const lockKey = `${LOCK_PREFIX}${key}`;
|
|
1303
|
+
if (owner !== void 0) {
|
|
1304
|
+
const result2 = await client.eval(EXTEND_LOCK_SCRIPT, 1, lockKey, owner, ttl);
|
|
1305
|
+
return result2 === 1;
|
|
1306
|
+
}
|
|
1307
|
+
const result = await client.expire(lockKey, ttl);
|
|
1308
|
+
return result === 1;
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
return {
|
|
1313
|
+
name: "redis",
|
|
1314
|
+
/**
|
|
1315
|
+
* 连接 Redis 服务
|
|
1316
|
+
*
|
|
1317
|
+
* 根据配置自动选择连接模式:
|
|
1318
|
+
* 1. url → 单机 URL 连接
|
|
1319
|
+
* 2. cluster → 集群模式
|
|
1320
|
+
* 3. sentinel → 哨兵模式
|
|
1321
|
+
* 4. host/port → 单机连接(默认)
|
|
1322
|
+
*
|
|
1323
|
+
* @param config - 已校验的 Redis 配置
|
|
1324
|
+
* @returns 成功 ok(undefined),失败 CONNECTION_FAILED
|
|
1325
|
+
*/
|
|
1326
|
+
async connect(config) {
|
|
1327
|
+
if (config.type !== "redis") {
|
|
1328
|
+
return err(
|
|
1329
|
+
HaiCacheError.UNSUPPORTED_TYPE,
|
|
1330
|
+
cacheM("cache_unsupportedType", { params: { type: config.type } })
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
try {
|
|
1334
|
+
const redisOptions = {
|
|
1335
|
+
connectTimeout: config.connectTimeout,
|
|
1336
|
+
commandTimeout: config.commandTimeout,
|
|
1337
|
+
keyPrefix: config.keyPrefix,
|
|
1338
|
+
maxRetriesPerRequest: config.maxRetries,
|
|
1339
|
+
retryStrategy: (times) => {
|
|
1340
|
+
if (times > config.maxRetries)
|
|
1341
|
+
return null;
|
|
1342
|
+
return config.retryDelay * times;
|
|
1343
|
+
},
|
|
1344
|
+
lazyConnect: true
|
|
1345
|
+
};
|
|
1346
|
+
if (config.tls)
|
|
1347
|
+
redisOptions.tls = {};
|
|
1348
|
+
if (config.readOnly)
|
|
1349
|
+
redisOptions.readOnly = true;
|
|
1350
|
+
if (config.url) {
|
|
1351
|
+
client = new Redis(config.url, redisOptions);
|
|
1352
|
+
} else if (config.cluster && config.cluster.length > 0) {
|
|
1353
|
+
const clusterOptions = {
|
|
1354
|
+
redisOptions,
|
|
1355
|
+
clusterRetryStrategy: (times) => {
|
|
1356
|
+
if (times > config.maxRetries)
|
|
1357
|
+
return null;
|
|
1358
|
+
return config.retryDelay * times;
|
|
1359
|
+
}
|
|
1360
|
+
};
|
|
1361
|
+
client = new Redis.Cluster(config.cluster, clusterOptions);
|
|
1362
|
+
} else if (config.sentinel) {
|
|
1363
|
+
client = new Redis({
|
|
1364
|
+
...redisOptions,
|
|
1365
|
+
sentinels: config.sentinel.sentinels,
|
|
1366
|
+
name: config.sentinel.name,
|
|
1367
|
+
password: config.password,
|
|
1368
|
+
db: config.db
|
|
1369
|
+
});
|
|
1370
|
+
} else {
|
|
1371
|
+
client = new Redis({
|
|
1372
|
+
...redisOptions,
|
|
1373
|
+
host: config.host,
|
|
1374
|
+
port: config.port,
|
|
1375
|
+
password: config.password,
|
|
1376
|
+
db: config.db
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
await client.connect();
|
|
1380
|
+
await client.ping();
|
|
1381
|
+
const address = config.url ? sanitizeRedisUrl(config.url) : `${config.host}:${config.port}`;
|
|
1382
|
+
logger.info("Redis connected", { address });
|
|
1383
|
+
return ok(void 0);
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
return err(
|
|
1386
|
+
HaiCacheError.CONNECTION_FAILED,
|
|
1387
|
+
cacheM("cache_redisConnectionFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
1388
|
+
error
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1392
|
+
async close() {
|
|
1393
|
+
if (client) {
|
|
1394
|
+
logger.info("Disconnecting Redis");
|
|
1395
|
+
await client.quit();
|
|
1396
|
+
client = null;
|
|
1397
|
+
logger.info("Redis disconnected");
|
|
1398
|
+
}
|
|
1399
|
+
},
|
|
1400
|
+
isConnected: () => client !== null,
|
|
1401
|
+
kv,
|
|
1402
|
+
hash,
|
|
1403
|
+
list,
|
|
1404
|
+
set_,
|
|
1405
|
+
zset,
|
|
1406
|
+
lock,
|
|
1407
|
+
async ping() {
|
|
1408
|
+
return wrapOperation(() => client.ping());
|
|
1409
|
+
}
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// src/cache-main.ts
|
|
1414
|
+
var logger2 = core.logger.child({ module: "cache", scope: "main" });
|
|
1415
|
+
var currentProvider = null;
|
|
1416
|
+
var currentConfig = null;
|
|
1417
|
+
var initInProgress = false;
|
|
1418
|
+
function createProvider(config) {
|
|
1419
|
+
switch (config.type) {
|
|
1420
|
+
case "memory":
|
|
1421
|
+
return createMemoryProvider();
|
|
1422
|
+
case "redis":
|
|
1423
|
+
return createRedisProvider();
|
|
1424
|
+
default:
|
|
1425
|
+
throw new Error(cacheM("cache_unsupportedType", { params: { type: config.type } }));
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
var notInitialized = core.module.createNotInitializedKit(
|
|
1429
|
+
HaiCacheError.NOT_INITIALIZED,
|
|
1430
|
+
() => cacheM("cache_notInitialized")
|
|
1431
|
+
);
|
|
1432
|
+
var notInitializedKv = notInitialized.proxy();
|
|
1433
|
+
var notInitializedHash = notInitialized.proxy();
|
|
1434
|
+
var notInitializedList = notInitialized.proxy();
|
|
1435
|
+
var notInitializedSet = notInitialized.proxy();
|
|
1436
|
+
var notInitializedZSet = notInitialized.proxy();
|
|
1437
|
+
var notInitializedLock = notInitialized.proxy();
|
|
1438
|
+
var cache = {
|
|
1439
|
+
/**
|
|
1440
|
+
* 初始化缓存连接。
|
|
1441
|
+
*
|
|
1442
|
+
* 会先关闭已有连接,再用新配置重新初始化。
|
|
1443
|
+
* 配置经 Zod Schema parse 后传给 Provider。
|
|
1444
|
+
*
|
|
1445
|
+
* @param config 缓存配置(支持 memory / redis)。
|
|
1446
|
+
* @returns 成功时返回 ok(undefined);失败时返回 Provider 错误(如 CONNECTION_FAILED)。
|
|
1447
|
+
*
|
|
1448
|
+
* @example
|
|
1449
|
+
* ```ts
|
|
1450
|
+
* const result = await cache.init({ type: 'redis', host: 'localhost' })
|
|
1451
|
+
* if (!result.success) {
|
|
1452
|
+
* // 生产代码中请使用项目 logger 输出错误
|
|
1453
|
+
* }
|
|
1454
|
+
* ```
|
|
1455
|
+
*/
|
|
1456
|
+
async init(config) {
|
|
1457
|
+
if (initInProgress) {
|
|
1458
|
+
logger2.warn("Cache init already in progress, skipping concurrent call");
|
|
1459
|
+
return err(
|
|
1460
|
+
HaiCacheError.OPERATION_FAILED,
|
|
1461
|
+
cacheM("cache_operationFailed", { params: { error: "Concurrent initialization detected" } })
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
initInProgress = true;
|
|
1465
|
+
try {
|
|
1466
|
+
if (currentProvider) {
|
|
1467
|
+
logger2.warn("Cache module is already initialized, reinitializing");
|
|
1468
|
+
await cache.close();
|
|
1469
|
+
}
|
|
1470
|
+
logger2.info("Initializing cache module");
|
|
1471
|
+
const parseResult = CacheConfigSchema.safeParse(config);
|
|
1472
|
+
if (!parseResult.success) {
|
|
1473
|
+
logger2.error("Cache config validation failed", { error: parseResult.error.message });
|
|
1474
|
+
return err(
|
|
1475
|
+
HaiCacheError.CONFIG_ERROR,
|
|
1476
|
+
cacheM("cache_configError", { params: { error: parseResult.error.message } }),
|
|
1477
|
+
parseResult.error
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
const parsed = parseResult.data;
|
|
1481
|
+
try {
|
|
1482
|
+
const provider = createProvider(parsed);
|
|
1483
|
+
const connectResult = await provider.connect(parsed);
|
|
1484
|
+
if (!connectResult.success) {
|
|
1485
|
+
logger2.error("Cache module initialization failed", {
|
|
1486
|
+
code: connectResult.error.code,
|
|
1487
|
+
message: connectResult.error.message
|
|
1488
|
+
});
|
|
1489
|
+
return connectResult;
|
|
1490
|
+
}
|
|
1491
|
+
currentProvider = provider;
|
|
1492
|
+
currentConfig = parsed;
|
|
1493
|
+
logger2.info("Cache module initialized", { type: parsed.type });
|
|
1494
|
+
return ok(void 0);
|
|
1495
|
+
} catch (error) {
|
|
1496
|
+
logger2.error("Cache module initialization failed", { error });
|
|
1497
|
+
return err(
|
|
1498
|
+
HaiCacheError.CONNECTION_FAILED,
|
|
1499
|
+
cacheM("cache_initFailed", {
|
|
1500
|
+
params: { error: error instanceof Error ? error.message : String(error) }
|
|
1501
|
+
}),
|
|
1502
|
+
error
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
} finally {
|
|
1506
|
+
initInProgress = false;
|
|
1507
|
+
}
|
|
1508
|
+
},
|
|
1509
|
+
/** KV 操作子接口;未 init 时所有方法返回 NOT_INITIALIZED */
|
|
1510
|
+
get kv() {
|
|
1511
|
+
return currentProvider?.kv ?? notInitializedKv;
|
|
1512
|
+
},
|
|
1513
|
+
/** Hash 操作子接口 */
|
|
1514
|
+
get hash() {
|
|
1515
|
+
return currentProvider?.hash ?? notInitializedHash;
|
|
1516
|
+
},
|
|
1517
|
+
/** List 操作子接口 */
|
|
1518
|
+
get list() {
|
|
1519
|
+
return currentProvider?.list ?? notInitializedList;
|
|
1520
|
+
},
|
|
1521
|
+
/** Set 操作子接口(名称加 _ 以避免与 JS 关键字冲突) */
|
|
1522
|
+
get set_() {
|
|
1523
|
+
return currentProvider?.set_ ?? notInitializedSet;
|
|
1524
|
+
},
|
|
1525
|
+
/** ZSet(有序集合)操作子接口 */
|
|
1526
|
+
get zset() {
|
|
1527
|
+
return currentProvider?.zset ?? notInitializedZSet;
|
|
1528
|
+
},
|
|
1529
|
+
/** 分布式锁操作子接口 */
|
|
1530
|
+
get lock() {
|
|
1531
|
+
return currentProvider?.lock ?? notInitializedLock;
|
|
1532
|
+
},
|
|
1533
|
+
/** 当前配置(parse 后);未初始化时返回 null */
|
|
1534
|
+
get config() {
|
|
1535
|
+
return currentConfig;
|
|
1536
|
+
},
|
|
1537
|
+
/** 是否已初始化并连接;Provider 不存在或未连接时返回 false */
|
|
1538
|
+
get isInitialized() {
|
|
1539
|
+
return currentProvider !== null && currentProvider.isConnected();
|
|
1540
|
+
},
|
|
1541
|
+
/**
|
|
1542
|
+
* 测试缓存连接状态
|
|
1543
|
+
*
|
|
1544
|
+
* @returns 成功时返回 'PONG';未初始化时返回 NOT_INITIALIZED 错误
|
|
1545
|
+
*/
|
|
1546
|
+
ping() {
|
|
1547
|
+
return currentProvider?.ping() ?? Promise.resolve(notInitialized.result());
|
|
1548
|
+
},
|
|
1549
|
+
/** 关闭缓存连接并释放资源;未初始化时调用安全无副作用 */
|
|
1550
|
+
async close() {
|
|
1551
|
+
if (!currentProvider) {
|
|
1552
|
+
currentConfig = null;
|
|
1553
|
+
logger2.info("Cache module already closed, skipping");
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
logger2.info("Closing cache module");
|
|
1557
|
+
try {
|
|
1558
|
+
await currentProvider.close();
|
|
1559
|
+
logger2.info("Cache module closed");
|
|
1560
|
+
} catch (error) {
|
|
1561
|
+
logger2.error("Cache module close failed", { error });
|
|
1562
|
+
} finally {
|
|
1563
|
+
currentProvider = null;
|
|
1564
|
+
currentConfig = null;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
export { CacheConfigSchema, HaiCacheError, MemoryConfigSchema, RedisClusterNodeSchema, RedisConfigSchema, RedisSentinelConfigSchema, cache };
|
|
1570
|
+
//# sourceMappingURL=index.js.map
|
|
1571
|
+
//# sourceMappingURL=index.js.map
|