@zenweb/cache 4.5.0 → 4.7.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/dist/cache.d.ts +14 -3
- package/dist/cache.js +129 -79
- package/dist/index.js +1 -0
- package/dist/middleware.d.ts +3 -10
- package/dist/middleware.js +5 -19
- package/dist/options.js +2 -0
- package/dist/types.d.ts +13 -6
- package/package.json +3 -1
package/dist/cache.d.ts
CHANGED
|
@@ -17,9 +17,13 @@ export declare class CacheResult {
|
|
|
17
17
|
* 对象缓存系统
|
|
18
18
|
*/
|
|
19
19
|
export declare class Cache {
|
|
20
|
-
|
|
20
|
+
redis: Redis;
|
|
21
21
|
private option;
|
|
22
22
|
constructor(redis: Redis, option: Required<SetupOption>);
|
|
23
|
+
/**
|
|
24
|
+
* 取得缓存剩余有效期
|
|
25
|
+
*/
|
|
26
|
+
ttl(key: string): Promise<number>;
|
|
23
27
|
/**
|
|
24
28
|
* 删除缓存
|
|
25
29
|
*/
|
|
@@ -59,10 +63,17 @@ export declare class Cache {
|
|
|
59
63
|
* @param key 缓存KEY
|
|
60
64
|
* @param fetch 缓存设置方法回调
|
|
61
65
|
*/
|
|
62
|
-
lockGet<T = unknown>(key: string, fetch: () => Promise<T> | T,
|
|
66
|
+
lockGet<T = unknown>(key: string, fetch: () => Promise<T> | T, opt: {
|
|
67
|
+
parse: false;
|
|
68
|
+
noWait: true;
|
|
69
|
+
} & LockGetOption): Promise<CacheResult | undefined>;
|
|
70
|
+
lockGet<T = unknown>(key: string, fetch: () => Promise<T> | T, opt: {
|
|
71
|
+
noWait: true;
|
|
72
|
+
} & LockGetOption): Promise<T | undefined>;
|
|
73
|
+
lockGet<T = unknown>(key: string, fetch: () => Promise<T> | T, opt: {
|
|
63
74
|
parse: false;
|
|
64
75
|
} & LockGetOption): Promise<CacheResult>;
|
|
65
|
-
lockGet<T = unknown>(key: string, fetch: () => Promise<T> | T,
|
|
76
|
+
lockGet<T = unknown>(key: string, fetch: () => Promise<T> | T, opt?: {
|
|
66
77
|
parse?: true;
|
|
67
78
|
} & LockGetOption): Promise<T>;
|
|
68
79
|
/**
|
package/dist/cache.js
CHANGED
|
@@ -33,6 +33,12 @@ class Cache {
|
|
|
33
33
|
this.redis = redis;
|
|
34
34
|
this.option = option;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* 取得缓存剩余有效期
|
|
38
|
+
*/
|
|
39
|
+
ttl(key) {
|
|
40
|
+
return this.redis.ttl(key);
|
|
41
|
+
}
|
|
36
42
|
/**
|
|
37
43
|
* 删除缓存
|
|
38
44
|
*/
|
|
@@ -106,110 +112,154 @@ class Cache {
|
|
|
106
112
|
return new CacheResult(compressed, data);
|
|
107
113
|
}
|
|
108
114
|
}
|
|
109
|
-
|
|
110
|
-
const
|
|
115
|
+
lockGet(key, fetch, opt) {
|
|
116
|
+
const _opt = Object.assign({}, this.option.set, this.option.lockGet, opt);
|
|
117
|
+
return new LockGet(this, key, fetch, _opt).get();
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 单例执行,在并发情况下保证同一个 key 只会单独执行,防止同时处理
|
|
121
|
+
* @param key 锁key
|
|
122
|
+
* @param run 获得锁时调用
|
|
123
|
+
*/
|
|
124
|
+
async singleRunner(key, run, _opt) {
|
|
125
|
+
const opt = Object.assign({}, this.option.lockGet, _opt);
|
|
126
|
+
const locker = new locker_1.Locker(this.redis, key, opt.lockTimeout);
|
|
127
|
+
const retryTimeout = typeof opt.retryTimeout === 'number' ? opt.retryTimeout : 0;
|
|
128
|
+
const retryDelay = (opt.retryDelay === undefined || opt.retryDelay < 0) ? 500 : opt.retryDelay;
|
|
129
|
+
let retryTime = 0;
|
|
130
|
+
while (true) {
|
|
131
|
+
// 取得锁并执行
|
|
132
|
+
if (await locker.acquire()) {
|
|
133
|
+
try {
|
|
134
|
+
return await run();
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
await locker.release();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 无法取得锁是否重复尝试
|
|
141
|
+
if (retryTime >= retryTimeout) {
|
|
142
|
+
throw new Error('Unable to acquire lock');
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
retryTime += retryDelay;
|
|
146
|
+
await (0, utils_1.sleep)(retryDelay);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
exports.Cache = Cache;
|
|
152
|
+
class LockGet {
|
|
153
|
+
constructor(cache, key, fetch, opt) {
|
|
154
|
+
this.cache = cache;
|
|
155
|
+
this.key = key;
|
|
156
|
+
this.fetch = fetch;
|
|
157
|
+
this.opt = opt;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 获取数据并设置到缓存中
|
|
161
|
+
*/
|
|
162
|
+
async fetchAndSet() {
|
|
163
|
+
const obj = await this.fetch();
|
|
111
164
|
// 本地存储
|
|
112
|
-
if (
|
|
113
|
-
|
|
165
|
+
if (this.opt.localStore) {
|
|
166
|
+
this.opt.localStore[this.key] = obj;
|
|
114
167
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
168
|
+
const result = await this.cache.set(this.key, obj, this.opt);
|
|
169
|
+
if (this.opt.parse !== false) {
|
|
170
|
+
return obj;
|
|
171
|
+
}
|
|
172
|
+
return new CacheResult(!this.opt.decompress && Boolean(result.compressed), !this.opt.decompress && result.compressed || Buffer.from(result.data));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 预刷新处理
|
|
176
|
+
*/
|
|
177
|
+
async preRefresh(preRefresh) {
|
|
178
|
+
const ttl = await this.cache.ttl(this.key);
|
|
179
|
+
_lockDebug('[%s] preRefresh remain: %d', this.key, ttl);
|
|
180
|
+
if (ttl < preRefresh) {
|
|
181
|
+
_lockDebug('[%s] preRefresh -> fetchAndSet', this.key);
|
|
182
|
+
await this.fetchAndSet();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 从缓存中取得数据
|
|
187
|
+
*/
|
|
188
|
+
async getCache() {
|
|
189
|
+
// 本地存储
|
|
190
|
+
if (this.opt.localStore && this.key in this.opt.localStore) {
|
|
191
|
+
return this.opt.localStore[this.key];
|
|
192
|
+
}
|
|
193
|
+
// 尝试从 redis 中获取
|
|
194
|
+
const data = await this.cache.get(this.key, this.opt);
|
|
195
|
+
if (data !== undefined) {
|
|
196
|
+
if (!this.locker && this.opt.preRefresh && this.opt.preRefresh > 0) {
|
|
197
|
+
this.preRefresh(this.opt.preRefresh);
|
|
125
198
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const preRefresh = async () => {
|
|
130
|
-
if (opt.preRefresh && opt.preRefresh > 0) {
|
|
131
|
-
const ttl = await this.redis.ttl(key);
|
|
132
|
-
_lockDebug('[%s] preRefresh remain: %d', key, ttl);
|
|
133
|
-
if (ttl < opt.preRefresh) {
|
|
134
|
-
_lockDebug('[%s] preRefresh -> fetchAndSet', key);
|
|
135
|
-
await fetchAndSet();
|
|
136
|
-
}
|
|
199
|
+
// 本地存储
|
|
200
|
+
if (this.opt.localStore && typeof this.opt.localStore === 'object') {
|
|
201
|
+
this.opt.localStore[this.key] = data;
|
|
137
202
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
203
|
+
return data;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async tryFetch() {
|
|
207
|
+
const retryTimeout = typeof this.opt.retryTimeout === 'number' ? this.opt.retryTimeout : 5000;
|
|
208
|
+
const retryDelay = (this.opt.retryDelay === undefined || this.opt.retryDelay < 0) ? 500 : this.opt.retryDelay;
|
|
209
|
+
let retryTime = 0;
|
|
143
210
|
while (true) {
|
|
144
|
-
if (!
|
|
211
|
+
if (!this.locker) {
|
|
212
|
+
this.locker = new locker_1.Locker(this.cache.redis, `${this.key}.LOCK`, this.opt.lockTimeout);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
145
215
|
// 尝试从 redis 中获取
|
|
146
|
-
const data = await this.
|
|
216
|
+
const data = await this.getCache();
|
|
147
217
|
if (data !== undefined) {
|
|
148
|
-
if (!locker) {
|
|
149
|
-
preRefresh();
|
|
150
|
-
}
|
|
151
|
-
// 本地存储
|
|
152
|
-
if (opt.localStore) {
|
|
153
|
-
opt.localStore[key] = data;
|
|
154
|
-
}
|
|
155
218
|
return data;
|
|
156
219
|
}
|
|
157
220
|
}
|
|
158
|
-
// 已经跳过 redis 获取可以结束强制刷新,进入锁获取,锁获取不成功则代表其他刷新任务已经进行,只要等待获取即可
|
|
159
|
-
refresh = false;
|
|
160
221
|
// 获取锁
|
|
161
|
-
if (
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
if (await locker.acquire()) {
|
|
165
|
-
_lockDebug('[%s] acquire success', key);
|
|
222
|
+
if (await this.locker.acquire()) {
|
|
223
|
+
_lockDebug('[%s] acquire success', this.key);
|
|
166
224
|
try {
|
|
167
|
-
return await fetchAndSet();
|
|
225
|
+
return await this.fetchAndSet();
|
|
168
226
|
}
|
|
169
227
|
finally {
|
|
170
|
-
await locker.release();
|
|
228
|
+
await this.locker.release();
|
|
171
229
|
}
|
|
172
230
|
}
|
|
173
231
|
else {
|
|
232
|
+
// 不等待为 true 不需要重试,说明有其他请求更新数据
|
|
233
|
+
if (this.opt.noWait === true) {
|
|
234
|
+
_lockDebug('[%s] no wait', this.key);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
174
237
|
// 获取锁失败,有其他请求更新数据,等待后重新获取获取数据
|
|
175
|
-
if (
|
|
176
|
-
throw new Error('Unable to acquire lock: ' + key);
|
|
238
|
+
if (retryTime >= retryTimeout) {
|
|
239
|
+
throw new Error('Unable to acquire lock: ' + this.key);
|
|
177
240
|
}
|
|
178
241
|
else {
|
|
179
|
-
|
|
180
|
-
await (0, utils_1.sleep)(
|
|
242
|
+
retryTime += retryDelay;
|
|
243
|
+
await (0, utils_1.sleep)(retryDelay);
|
|
181
244
|
}
|
|
182
245
|
}
|
|
183
246
|
}
|
|
184
247
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
let retryCount = opt.retryCount || 0;
|
|
194
|
-
while (true) {
|
|
195
|
-
// 取得锁并执行
|
|
196
|
-
if (await locker.acquire()) {
|
|
197
|
-
try {
|
|
198
|
-
return await run();
|
|
199
|
-
}
|
|
200
|
-
finally {
|
|
201
|
-
await locker.release();
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
// 无法取得锁是否重复尝试
|
|
205
|
-
if (retryCount <= 0) {
|
|
206
|
-
throw new Error('Unable to acquire lock');
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
retryCount--;
|
|
210
|
-
await (0, utils_1.sleep)(opt.retryDelay || 300);
|
|
248
|
+
async get() {
|
|
249
|
+
// 如果强制刷新则不需要尝试从缓存获取,直接进入锁获取并取得数据
|
|
250
|
+
const refresh = this.opt.refresh || false;
|
|
251
|
+
if (!refresh) {
|
|
252
|
+
// 先尝试从缓存获取
|
|
253
|
+
const data = await this.getCache();
|
|
254
|
+
if (data !== undefined) {
|
|
255
|
+
return data;
|
|
211
256
|
}
|
|
212
257
|
}
|
|
258
|
+
// 不等待为 true 直接返回 undefined
|
|
259
|
+
if (this.opt.noWait === true) {
|
|
260
|
+
this.tryFetch();
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
return this.tryFetch();
|
|
213
264
|
}
|
|
214
265
|
}
|
|
215
|
-
exports.Cache = Cache;
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ Object.defineProperty(exports, "cached", { enumerable: true, get: function () {
|
|
|
34
34
|
*/
|
|
35
35
|
function setup(option) {
|
|
36
36
|
return async function cache(setup) {
|
|
37
|
+
setup.assertModuleExists('result', 'need setup @zenweb/result');
|
|
37
38
|
const opt = Object.assign({}, options_1.defaultSetupOption, option);
|
|
38
39
|
setup.debug('option: %o', opt);
|
|
39
40
|
const redis = opt.redis instanceof ioredis_1.Redis ? opt.redis : new ioredis_1.Redis(opt.redis);
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import { Context, Middleware } from "@zenweb/core";
|
|
2
2
|
import { LockGetOption } from "./types";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 输出内容类型
|
|
6
|
-
* - 默认 json
|
|
7
|
-
* - 需要指定输出类型,因为缓存的是 body 主体,并不包含类型
|
|
8
|
-
*/
|
|
9
|
-
type?: string;
|
|
10
|
-
}
|
|
3
|
+
type CachedMiddlewareOption = Omit<LockGetOption, 'parse' | 'decompress'>;
|
|
11
4
|
/**
|
|
12
5
|
* 缓存中间件
|
|
13
|
-
* -
|
|
14
|
-
* -
|
|
6
|
+
* - 缓存 ctx.success() 结果
|
|
7
|
+
* - fail() 时不缓存
|
|
15
8
|
* @param key 不指定 `key` 则默认使用 `ctx.path`,`key: null` 不适用使用缓存
|
|
16
9
|
*/
|
|
17
10
|
export declare function cached(key?: string | null | ((ctx: Context) => string | null | Promise<string | null>), opt?: CachedMiddlewareOption): Middleware;
|
package/dist/middleware.js
CHANGED
|
@@ -3,8 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.cached = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* 缓存中间件
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
6
|
+
* - 缓存 ctx.success() 结果
|
|
7
|
+
* - fail() 时不缓存
|
|
8
8
|
* @param key 不指定 `key` 则默认使用 `ctx.path`,`key: null` 不适用使用缓存
|
|
9
9
|
*/
|
|
10
10
|
function cached(key, opt) {
|
|
@@ -16,25 +16,11 @@ function cached(key, opt) {
|
|
|
16
16
|
if (!_key) {
|
|
17
17
|
return next();
|
|
18
18
|
}
|
|
19
|
-
const decompress = ctx.acceptsEncodings('gzip') === false;
|
|
20
|
-
const _opt = Object.assign({
|
|
21
|
-
type: 'json'
|
|
22
|
-
}, opt);
|
|
23
19
|
const result = await ctx.core.cache2.lockGet(_key, async function () {
|
|
24
20
|
await next();
|
|
25
|
-
return
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
parse: false,
|
|
29
|
-
decompress,
|
|
30
|
-
});
|
|
31
|
-
if (result.compressed) {
|
|
32
|
-
ctx.set('Content-Encoding', 'gzip');
|
|
33
|
-
}
|
|
34
|
-
if (_opt.type) {
|
|
35
|
-
ctx.type = _opt.type;
|
|
36
|
-
}
|
|
37
|
-
ctx.body = result.data;
|
|
21
|
+
return ctx.successData;
|
|
22
|
+
}, opt);
|
|
23
|
+
ctx.success(result);
|
|
38
24
|
};
|
|
39
25
|
}
|
|
40
26
|
exports.cached = cached;
|
package/dist/options.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export interface SetupOption {
|
|
|
45
45
|
/**
|
|
46
46
|
* Redis 实例或 Redis 选项
|
|
47
47
|
*/
|
|
48
|
-
redis
|
|
48
|
+
redis?: Redis | RedisOptions;
|
|
49
49
|
/**
|
|
50
50
|
* 默认缓存设置选项
|
|
51
51
|
*/
|
|
@@ -100,15 +100,15 @@ export interface LockOption {
|
|
|
100
100
|
*/
|
|
101
101
|
lockTimeout?: number;
|
|
102
102
|
/**
|
|
103
|
-
*
|
|
103
|
+
* 数据获取重试超时 (毫秒)
|
|
104
104
|
* - 0 不进行重试
|
|
105
|
-
* - lockGet 默认为
|
|
105
|
+
* - lockGet 默认为 5000
|
|
106
106
|
* - singleRunner 默认为 0
|
|
107
107
|
*/
|
|
108
|
-
|
|
108
|
+
retryTimeout?: number;
|
|
109
109
|
/**
|
|
110
|
-
*
|
|
111
|
-
* @default
|
|
110
|
+
* 数据获取重试间隔 (毫秒)
|
|
111
|
+
* @default 500
|
|
112
112
|
*/
|
|
113
113
|
retryDelay?: number;
|
|
114
114
|
}
|
|
@@ -138,4 +138,11 @@ export interface LockGetOption extends GetOption, SetOption, LockOption {
|
|
|
138
138
|
localStore?: {
|
|
139
139
|
[key: string]: any;
|
|
140
140
|
};
|
|
141
|
+
/**
|
|
142
|
+
* 不等待
|
|
143
|
+
* - 当缓存不存在时直接返回 `undefined`,之后使用 `fetch` 取得数据并设置缓存
|
|
144
|
+
* - 应用场景: 某些数据并不参与业务逻辑又比较耗时,当 `fetch` 处理时间过长的情况下可能会造成请求超时,使用 `wait: false` 可以跳过等待
|
|
145
|
+
* @default false
|
|
146
|
+
*/
|
|
147
|
+
noWait?: boolean;
|
|
141
148
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenweb/cache",
|
|
3
3
|
"packageManager": "yarn@4.0.2",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.7.0",
|
|
5
5
|
"description": "Zenweb Cache module",
|
|
6
6
|
"exports": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"@types/node": "^20.10.6",
|
|
37
37
|
"cross-env": "^7.0.3",
|
|
38
38
|
"mocha": "^10.2.0",
|
|
39
|
+
"msgpackr": "^1.11.0",
|
|
39
40
|
"rimraf": "^4.3.1",
|
|
40
41
|
"ts-mocha": "^10.0.0",
|
|
41
42
|
"ts-node": "^10.9.1",
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"@zenweb/core": "^4.2.3",
|
|
48
|
+
"@zenweb/result": "^4.1.2",
|
|
47
49
|
"ioredis": "^5.3.2"
|
|
48
50
|
}
|
|
49
51
|
}
|