@zenweb/cache 5.2.0 → 5.3.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/README.md +43 -0
- package/dist/cache.js +5 -10
- package/dist/compressor.d.ts +9 -0
- package/dist/compressor.js +20 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/options.d.ts +5 -2
- package/dist/options.js +7 -11
- package/dist/serializer.d.ts +5 -0
- package/dist/serializer.js +8 -0
- package/dist/types.d.ts +38 -10
- package/dist/utils.js +8 -6
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
- JSON 序列直接输出
|
|
9
9
|
- 防止缓存击穿
|
|
10
10
|
- 单例执行
|
|
11
|
+
- 支持自定义序列化方式
|
|
12
|
+
- 支持自定义压缩方式
|
|
11
13
|
|
|
12
14
|
## 安装
|
|
13
15
|
|
|
@@ -83,3 +85,44 @@ export class CacheController {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
```
|
|
88
|
+
|
|
89
|
+
## 进阶优化
|
|
90
|
+
使用 msgpackr 序列化 + snappy 压缩
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { create } from 'zenweb';
|
|
94
|
+
import modCache from '@zenweb/cache';
|
|
95
|
+
import { Packr } from 'msgpackr';
|
|
96
|
+
import * as snappy from 'snappy';
|
|
97
|
+
|
|
98
|
+
const SNAPPY_MAGIC_HEADER = Buffer.from('SNAPPY');
|
|
99
|
+
const packer = new Packr();
|
|
100
|
+
|
|
101
|
+
const app = create();
|
|
102
|
+
|
|
103
|
+
app.setup(cache({
|
|
104
|
+
serializer: {
|
|
105
|
+
deserialize(data) {
|
|
106
|
+
return packer.unpack(data)
|
|
107
|
+
},
|
|
108
|
+
serialize(data) {
|
|
109
|
+
return packer.pack(data)
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
compressor: {
|
|
113
|
+
isCompressed(data) {
|
|
114
|
+
return SNAPPY_MAGIC_HEADER.equals(data.subarray(0, SNAPPY_MAGIC_HEADER.length));
|
|
115
|
+
},
|
|
116
|
+
async compress(data) {
|
|
117
|
+
const compressed = await snappy.compress(data);
|
|
118
|
+
return Buffer.concat([SNAPPY_MAGIC_HEADER, compressed]);
|
|
119
|
+
},
|
|
120
|
+
async decompress(data) {
|
|
121
|
+
const result = await snappy.uncompress(data.subarray(SNAPPY_MAGIC_HEADER.length));
|
|
122
|
+
return typeof result === 'string' ? Buffer.from(result) : result;
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
app.start();
|
|
128
|
+
```
|
package/dist/cache.js
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import { gzip, unzip } from 'node:zlib';
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
1
|
import { Locker } from './locker.js';
|
|
4
2
|
import { debug, sleep } from './utils.js';
|
|
5
|
-
const compress = promisify(gzip);
|
|
6
|
-
const decompress = promisify(unzip);
|
|
7
|
-
const GZ_HEADER = Buffer.from([0x1F, 0x8B, 0x08]);
|
|
8
3
|
const _getDebug = debug.extend('get');
|
|
9
4
|
const _setDebug = debug.extend('set');
|
|
10
5
|
const _lockDebug = debug.extend('lock');
|
|
@@ -65,14 +60,14 @@ export class Cache {
|
|
|
65
60
|
*/
|
|
66
61
|
async set(key, value, ttlopt) {
|
|
67
62
|
let compressed;
|
|
68
|
-
let data = Buffer.isBuffer(value) ? value : this.option.serializer.serialize(value);
|
|
63
|
+
let data = Buffer.isBuffer(value) ? value : await this.option.serializer.serialize(value);
|
|
69
64
|
const opt = Object.assign({}, this.option.set, typeof ttlopt === 'object' ? ttlopt : undefined);
|
|
70
65
|
if (typeof ttlopt === 'number') {
|
|
71
66
|
opt.ttl = ttlopt;
|
|
72
67
|
}
|
|
73
68
|
_setDebug('[%s] data length:', key, data.length);
|
|
74
69
|
if (opt.compressMinLength && data.length >= opt.compressMinLength) {
|
|
75
|
-
let _compressed = await compress(data
|
|
70
|
+
let _compressed = await this.option.compressor.compress(data);
|
|
76
71
|
_setDebug('[%s] compressed length: %d', key, _compressed.length);
|
|
77
72
|
if (_compressed.length < (data.length * (opt.compressStoreRatio || 0.95))) {
|
|
78
73
|
compressed = _compressed;
|
|
@@ -95,19 +90,19 @@ export class Cache {
|
|
|
95
90
|
let _opt = Object.assign({ parse: true, decompress: true }, opt);
|
|
96
91
|
if (data) {
|
|
97
92
|
// 解压处理
|
|
98
|
-
let compressed =
|
|
93
|
+
let compressed = await this.option.compressor.isCompressed(data);
|
|
99
94
|
if (compressed) {
|
|
100
95
|
_getDebug('[%s] is compressed', key);
|
|
101
96
|
if (_opt.parse || _opt.decompress) {
|
|
102
97
|
_getDebug('[%s] decompress', key);
|
|
103
|
-
data = await decompress(data);
|
|
98
|
+
data = await this.option.compressor.decompress(data);
|
|
104
99
|
compressed = false;
|
|
105
100
|
}
|
|
106
101
|
}
|
|
107
102
|
// 解析处理
|
|
108
103
|
if (_opt.parse) {
|
|
109
104
|
_getDebug('[%s] parse', key);
|
|
110
|
-
return this.option.serializer.deserialize(data);
|
|
105
|
+
return await this.option.serializer.deserialize(data);
|
|
111
106
|
}
|
|
112
107
|
return new CacheResult(compressed, data);
|
|
113
108
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ZlibOptions } from 'node:zlib';
|
|
2
|
+
import { DataCompressor } from "./types.js";
|
|
3
|
+
export declare class GzCompressor implements DataCompressor {
|
|
4
|
+
private opt?;
|
|
5
|
+
constructor(opt?: ZlibOptions | undefined);
|
|
6
|
+
isCompressed(data: Buffer): boolean;
|
|
7
|
+
compress(data: Buffer): Promise<Buffer<ArrayBufferLike>>;
|
|
8
|
+
decompress(data: Buffer): Promise<Buffer<ArrayBufferLike>>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { gzip, unzip } from 'node:zlib';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const compress = promisify(gzip);
|
|
4
|
+
const decompress = promisify(unzip);
|
|
5
|
+
const GZ_HEADER = Buffer.from([0x1F, 0x8B, 0x08]);
|
|
6
|
+
export class GzCompressor {
|
|
7
|
+
opt;
|
|
8
|
+
constructor(opt) {
|
|
9
|
+
this.opt = opt;
|
|
10
|
+
}
|
|
11
|
+
isCompressed(data) {
|
|
12
|
+
return GZ_HEADER.equals(data.subarray(0, GZ_HEADER.length));
|
|
13
|
+
}
|
|
14
|
+
compress(data) {
|
|
15
|
+
return compress(data, this.opt);
|
|
16
|
+
}
|
|
17
|
+
decompress(data) {
|
|
18
|
+
return decompress(data, this.opt);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { Redis } from 'ioredis';
|
|
|
5
5
|
export * from './global.js';
|
|
6
6
|
export * from './types.js';
|
|
7
7
|
export * from './utils.js';
|
|
8
|
+
export { GzCompressor } from './compressor.js';
|
|
9
|
+
export { JSONSerializer } from './serializer.js';
|
|
8
10
|
export { Locker } from './locker.js';
|
|
9
11
|
export { cached } from './middleware.js';
|
|
10
12
|
export { CacheHelper } from './helper.js';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import { defaultSetupOption } from './options.js';
|
|
|
4
4
|
export * from './global.js';
|
|
5
5
|
export * from './types.js';
|
|
6
6
|
export * from './utils.js';
|
|
7
|
+
export { GzCompressor } from './compressor.js';
|
|
8
|
+
export { JSONSerializer } from './serializer.js';
|
|
7
9
|
export { Locker } from './locker.js';
|
|
8
10
|
export { cached } from './middleware.js';
|
|
9
11
|
export { CacheHelper } from './helper.js';
|
package/dist/options.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { RedisOptions } from "ioredis";
|
|
2
|
-
import type { LockGetOption,
|
|
2
|
+
import type { LockGetOption, SetOption, SetupOption } from "./types.js";
|
|
3
|
+
import { JSONSerializer } from "./serializer.js";
|
|
4
|
+
import { GzCompressor } from "./compressor.js";
|
|
3
5
|
export declare const defaultRedisOption: RedisOptions;
|
|
4
6
|
export declare const defaultSetOption: SetOption;
|
|
5
7
|
export declare const defaultLockGetOption: LockGetOption;
|
|
6
|
-
export declare const
|
|
8
|
+
export declare const jsonSerializer: JSONSerializer;
|
|
9
|
+
export declare const gzCompressor: GzCompressor;
|
|
7
10
|
export declare const defaultSetupOption: Required<SetupOption>;
|
package/dist/options.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { JSONSerializer } from "./serializer.js";
|
|
2
|
+
import { GzCompressor } from "./compressor.js";
|
|
1
3
|
export const defaultRedisOption = {
|
|
2
4
|
host: process.env.REDIS_HOST || '127.0.0.1',
|
|
3
5
|
port: parseInt(process.env.REDIS_PORT || '') || 6379,
|
|
@@ -7,8 +9,7 @@ export const defaultRedisOption = {
|
|
|
7
9
|
export const defaultSetOption = {
|
|
8
10
|
ttl: 60,
|
|
9
11
|
compressMinLength: 1024,
|
|
10
|
-
compressStoreRatio: 0.
|
|
11
|
-
compressLevel: 1,
|
|
12
|
+
compressStoreRatio: 0.8,
|
|
12
13
|
};
|
|
13
14
|
export const defaultLockGetOption = {
|
|
14
15
|
retryTimeout: 5000,
|
|
@@ -17,17 +18,12 @@ export const defaultLockGetOption = {
|
|
|
17
18
|
refresh: false,
|
|
18
19
|
localStore: undefined,
|
|
19
20
|
};
|
|
20
|
-
export const
|
|
21
|
-
|
|
22
|
-
return Buffer.from(JSON.stringify(data));
|
|
23
|
-
},
|
|
24
|
-
deserialize: (data) => {
|
|
25
|
-
return JSON.parse(data.toString());
|
|
26
|
-
},
|
|
27
|
-
};
|
|
21
|
+
export const jsonSerializer = new JSONSerializer();
|
|
22
|
+
export const gzCompressor = new GzCompressor({ level: 1 });
|
|
28
23
|
export const defaultSetupOption = {
|
|
29
24
|
redis: defaultRedisOption,
|
|
30
25
|
set: defaultSetOption,
|
|
31
26
|
lockGet: defaultLockGetOption,
|
|
32
|
-
serializer:
|
|
27
|
+
serializer: jsonSerializer,
|
|
28
|
+
compressor: gzCompressor,
|
|
33
29
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -23,22 +23,45 @@ export interface SetOption {
|
|
|
23
23
|
* - 压缩完成后的长度对比压缩之前的长度所缩减的比率
|
|
24
24
|
* - 压缩率不满足要求则不存储压缩数据
|
|
25
25
|
* - 相对于低比率的压缩数据再解压节省的空间十分有限又浪费计算资源
|
|
26
|
-
* - 默认 0.
|
|
27
|
-
* @default 0.
|
|
26
|
+
* - 默认 0.8, 至少压缩率达到 95% 才有价值
|
|
27
|
+
* @default 0.8
|
|
28
28
|
*/
|
|
29
29
|
compressStoreRatio?: number;
|
|
30
|
-
/**
|
|
31
|
-
* 压缩级别
|
|
32
|
-
* @default 1
|
|
33
|
-
*/
|
|
34
|
-
compressLevel?: number;
|
|
35
30
|
}
|
|
36
31
|
/**
|
|
37
32
|
* 对象序列化
|
|
38
33
|
*/
|
|
39
34
|
export interface ObjectSerializer {
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
/**
|
|
36
|
+
* 序列化
|
|
37
|
+
* @param data 需要被序列化的对象
|
|
38
|
+
*/
|
|
39
|
+
serialize(data: object): Buffer | Promise<Buffer>;
|
|
40
|
+
/**
|
|
41
|
+
* 发序列化
|
|
42
|
+
* @param data 序列化数据
|
|
43
|
+
*/
|
|
44
|
+
deserialize(data: Buffer): object | Promise<object>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* 数据压缩器
|
|
48
|
+
*/
|
|
49
|
+
export interface DataCompressor {
|
|
50
|
+
/**
|
|
51
|
+
* 是否为压缩数据
|
|
52
|
+
* @param data 压缩数据
|
|
53
|
+
*/
|
|
54
|
+
isCompressed(data: Buffer): boolean | Promise<boolean>;
|
|
55
|
+
/**
|
|
56
|
+
* 压缩数据
|
|
57
|
+
* @param data 原始数据
|
|
58
|
+
*/
|
|
59
|
+
compress(data: Buffer): Buffer | Promise<Buffer>;
|
|
60
|
+
/**
|
|
61
|
+
* 解压缩数据
|
|
62
|
+
* @param data 压缩数据
|
|
63
|
+
*/
|
|
64
|
+
decompress(data: Buffer): Buffer | Promise<Buffer>;
|
|
42
65
|
}
|
|
43
66
|
export interface SetupOption {
|
|
44
67
|
/**
|
|
@@ -58,6 +81,11 @@ export interface SetupOption {
|
|
|
58
81
|
* - 默认为 JSON 序列化
|
|
59
82
|
*/
|
|
60
83
|
serializer?: ObjectSerializer;
|
|
84
|
+
/**
|
|
85
|
+
* 数据压缩器
|
|
86
|
+
* - 默认为 Gz 压缩器
|
|
87
|
+
*/
|
|
88
|
+
compressor?: DataCompressor;
|
|
61
89
|
}
|
|
62
90
|
/**
|
|
63
91
|
* 取得缓存选项
|
|
@@ -145,6 +173,6 @@ export interface LockGetOption extends GetOption, SetOption, LockOption {
|
|
|
145
173
|
*/
|
|
146
174
|
noWait?: boolean;
|
|
147
175
|
}
|
|
148
|
-
export type CacheKeyType = string | number | object;
|
|
176
|
+
export type CacheKeyType = string | number | object | undefined | null | boolean;
|
|
149
177
|
export type CacheHelperOption = Omit<LockGetOption, 'parse' | 'noWait'>;
|
|
150
178
|
export type CacheHelperKey<P extends CacheKeyType[]> = string | ((...param: P) => string | Promise<string>);
|
package/dist/utils.js
CHANGED
|
@@ -72,12 +72,14 @@ function plainifyOrHash(obj) {
|
|
|
72
72
|
*/
|
|
73
73
|
export function cacheKey(...key) {
|
|
74
74
|
return key.map(i => {
|
|
75
|
-
if (
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
return '';
|
|
75
|
+
if (i === null)
|
|
76
|
+
return 'N';
|
|
77
|
+
switch (typeof i) {
|
|
78
|
+
case 'string': return i;
|
|
79
|
+
case 'number': return String(i);
|
|
80
|
+
case 'boolean': return i ? 'T' : 'F';
|
|
81
|
+
case 'undefined': return '';
|
|
82
|
+
}
|
|
81
83
|
if (i instanceof Date)
|
|
82
84
|
return i.toJSON();
|
|
83
85
|
return plainifyOrHash(i);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenweb/cache",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.3.0",
|
|
5
5
|
"description": "Zenweb Cache module",
|
|
6
6
|
"exports": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"mocha": "^10.2.0",
|
|
38
38
|
"msgpackr": "^1.11.0",
|
|
39
39
|
"rimraf": "^4.3.1",
|
|
40
|
+
"snappy": "^7.3.3",
|
|
40
41
|
"ts-node": "^10.9.1",
|
|
41
42
|
"typescript": "^5.6.3",
|
|
42
43
|
"zenweb": "^5.1.0"
|