@anjianshi/utils 2.3.9 → 2.3.10
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/env-node/redis-cache.d.ts +39 -0
- package/env-node/redis-cache.js +117 -0
- package/package.json +8 -4
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type RedisClientType } from 'redis';
|
|
2
|
+
import { type Logger } from '../logging/index.js';
|
|
3
|
+
export declare function initRedisLogging(redis: RedisClientType, logger?: Logger): void;
|
|
4
|
+
export interface CacheOptions {
|
|
5
|
+
logger: Logger;
|
|
6
|
+
/** 数据有效期,单位秒。默认为 10 分钟。小于等于 0 代表不设有效期 */
|
|
7
|
+
expires: number;
|
|
8
|
+
/** 读取时是否自动刷新有效期,仅设置了 expire 时有效,默认为 true */
|
|
9
|
+
refreshOnRead: boolean;
|
|
10
|
+
/** 若为 true,读取数据后会立即将其删除,默认为 false */
|
|
11
|
+
oneTime: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 维护缓存数据
|
|
15
|
+
* 1. 每个 Cache 实例只维护一个主题的数据,且需明确定义数据类型,这样设计可明确对每一项缓存的使用、避免混乱。
|
|
16
|
+
* 2. 值在存储时会 JSON 化,读取时再进行 JSON 解析(支持 JSON 化 Date 对象)。
|
|
17
|
+
*/
|
|
18
|
+
export declare class Cache<T> {
|
|
19
|
+
readonly redis: RedisClientType;
|
|
20
|
+
readonly topic: string;
|
|
21
|
+
readonly options: CacheOptions;
|
|
22
|
+
constructor(redis: RedisClientType, topic: string, options?: Partial<CacheOptions>);
|
|
23
|
+
get logger(): Logger;
|
|
24
|
+
protected jsonStringify(value: T): string;
|
|
25
|
+
protected jsonParse(redisValue: string): T;
|
|
26
|
+
protected getRedisKey(identity: string): string;
|
|
27
|
+
/** 读取一项内容 */
|
|
28
|
+
get(identity: string, defaults: T): Promise<T>;
|
|
29
|
+
get(identity?: string): Promise<T | undefined>;
|
|
30
|
+
/** 写入/更新一项内容 */
|
|
31
|
+
set(value: T): Promise<void>;
|
|
32
|
+
set(identity: string, value: T): Promise<void>;
|
|
33
|
+
/** 移除一项内容 */
|
|
34
|
+
delete(identity?: string | string[]): Promise<number>;
|
|
35
|
+
/** 刷新一项内容的过期时间 */
|
|
36
|
+
refresh(identity?: string): Promise<boolean>;
|
|
37
|
+
/** 确认一项内容是否存在 */
|
|
38
|
+
exists(identity?: string): Promise<boolean>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { logger as rootLogger } from '../logging/index.js';
|
|
2
|
+
export function initRedisLogging(redis, logger) {
|
|
3
|
+
if (!logger)
|
|
4
|
+
logger = rootLogger.getChild('redis');
|
|
5
|
+
redis.on('connect', () => logger.info('connecting'));
|
|
6
|
+
redis.on('ready', () => logger.info('connected'));
|
|
7
|
+
redis.on('end', () => logger.info('connection closed'));
|
|
8
|
+
redis.on('reconnecting', () => logger.info('reconnecting'));
|
|
9
|
+
redis.on('error', error => logger.error(error));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 维护缓存数据
|
|
13
|
+
* 1. 每个 Cache 实例只维护一个主题的数据,且需明确定义数据类型,这样设计可明确对每一项缓存的使用、避免混乱。
|
|
14
|
+
* 2. 值在存储时会 JSON 化,读取时再进行 JSON 解析(支持 JSON 化 Date 对象)。
|
|
15
|
+
*/
|
|
16
|
+
export class Cache {
|
|
17
|
+
redis;
|
|
18
|
+
topic;
|
|
19
|
+
options;
|
|
20
|
+
constructor(redis, topic, options) {
|
|
21
|
+
this.redis = redis;
|
|
22
|
+
this.topic = topic;
|
|
23
|
+
this.options = {
|
|
24
|
+
logger: rootLogger.getChild('cache'),
|
|
25
|
+
expires: 600,
|
|
26
|
+
refreshOnRead: true,
|
|
27
|
+
oneTime: false,
|
|
28
|
+
...(options ?? {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
get logger() {
|
|
32
|
+
return this.options.logger;
|
|
33
|
+
}
|
|
34
|
+
// 经过定制的 JSON 序列化和解析方法
|
|
35
|
+
jsonStringify(value) {
|
|
36
|
+
// 参考:https://stackoverflow.com/a/54037861/2815178
|
|
37
|
+
function replacer(key, value) {
|
|
38
|
+
// value 是经过预处理过的值,对于 Date 对象,此时已经是 string,需要通过 this[key] 才能拿到 Date 值。
|
|
39
|
+
const rawValue = this[key];
|
|
40
|
+
if (rawValue instanceof Date)
|
|
41
|
+
return { __json_type: 'date', value: rawValue.toISOString() };
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
return JSON.stringify(value, replacer);
|
|
45
|
+
}
|
|
46
|
+
jsonParse(redisValue) {
|
|
47
|
+
function reviver(key, value) {
|
|
48
|
+
if (typeof value === 'object' && value !== null) {
|
|
49
|
+
const obj = value;
|
|
50
|
+
if (obj.__json_type === 'date')
|
|
51
|
+
return new Date(obj.value);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
return JSON.parse(redisValue, reviver);
|
|
56
|
+
}
|
|
57
|
+
getRedisKey(identity) {
|
|
58
|
+
return `${this.topic}${identity ? ':' + identity : ''}`;
|
|
59
|
+
}
|
|
60
|
+
async get(identity = '', defaults) {
|
|
61
|
+
const redisKey = this.getRedisKey(identity);
|
|
62
|
+
const redisValue = await this.redis.get(redisKey);
|
|
63
|
+
this.logger.debug('get', redisKey, redisValue);
|
|
64
|
+
if (redisValue === null)
|
|
65
|
+
return defaults;
|
|
66
|
+
if (this.options.refreshOnRead)
|
|
67
|
+
void this.refresh(identity);
|
|
68
|
+
try {
|
|
69
|
+
const value = this.jsonParse(redisValue);
|
|
70
|
+
if (this.options.oneTime)
|
|
71
|
+
await this.delete(identity);
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.logger.error(`解析 cache 数据失败,key=${redisKey},value=${redisValue}`, error);
|
|
76
|
+
void this.delete(identity);
|
|
77
|
+
return defaults;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async set(identity, value) {
|
|
81
|
+
if (value === undefined) {
|
|
82
|
+
value = identity;
|
|
83
|
+
identity = '';
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
identity = identity;
|
|
87
|
+
}
|
|
88
|
+
const redisKey = this.getRedisKey(identity);
|
|
89
|
+
let redisValue;
|
|
90
|
+
try {
|
|
91
|
+
redisValue = this.jsonStringify(value);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.logger.error(`格式化 cache 数据失败,key=${redisKey}`, value, error);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
this.logger.debug('set', redisKey, redisValue);
|
|
98
|
+
await this.redis.set(redisKey, redisValue, { EX: this.options.expires });
|
|
99
|
+
}
|
|
100
|
+
/** 移除一项内容 */
|
|
101
|
+
async delete(identity = '') {
|
|
102
|
+
const identities = Array.isArray(identity) ? identity : [identity];
|
|
103
|
+
this.logger.debug('delete', identities);
|
|
104
|
+
return this.redis.del(identities.map(identity => this.getRedisKey(identity)));
|
|
105
|
+
}
|
|
106
|
+
/** 刷新一项内容的过期时间 */
|
|
107
|
+
async refresh(identity = '') {
|
|
108
|
+
if (this.options.expires >= 0)
|
|
109
|
+
return this.redis.expire(this.getRedisKey(identity), this.options.expires);
|
|
110
|
+
else
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
/** 确认一项内容是否存在 */
|
|
114
|
+
async exists(identity = '') {
|
|
115
|
+
return (await this.redis.exists(this.getRedisKey(identity))) === 1;
|
|
116
|
+
}
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anjianshi/utils",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.10",
|
|
4
4
|
"description": "Common JavaScript Utils",
|
|
5
5
|
"homepage": "https://github.com/anjianshi/js-packages/utils",
|
|
6
6
|
"bugs": {
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"dotenv": "^16.4.5",
|
|
27
27
|
"typescript": "^5.5.4",
|
|
28
28
|
"vconsole": "^3.15.1",
|
|
29
|
-
"@anjianshi/presets-eslint-typescript": "5.0.5",
|
|
30
29
|
"@anjianshi/presets-eslint-node": "4.0.8",
|
|
31
|
-
"@anjianshi/presets-
|
|
30
|
+
"@anjianshi/presets-eslint-typescript": "5.0.5",
|
|
32
31
|
"@anjianshi/presets-typescript": "3.2.2",
|
|
32
|
+
"@anjianshi/presets-prettier": "3.0.1",
|
|
33
33
|
"@anjianshi/presets-eslint-react": "4.0.7"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"@prisma/client": "^5.20.0",
|
|
40
40
|
"chalk": "^5.3.0",
|
|
41
41
|
"dayjs": "^1.11.10",
|
|
42
|
-
"react": "^18.3.1"
|
|
42
|
+
"react": "^18.3.1",
|
|
43
|
+
"redis": "^4.7.0"
|
|
43
44
|
},
|
|
44
45
|
"peerDependenciesMeta": {
|
|
45
46
|
"@emotion/react": {
|
|
@@ -62,6 +63,9 @@
|
|
|
62
63
|
},
|
|
63
64
|
"react": {
|
|
64
65
|
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"redis": {
|
|
68
|
+
"optional": true
|
|
65
69
|
}
|
|
66
70
|
},
|
|
67
71
|
"prettier": "@anjianshi/presets-prettier/prettierrc"
|