@less-is-more/less-js 1.5.3 → 2.0.0-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/package.json +3 -2
- package/src/cache.js +147 -34
- package/src/nas.js +46 -56
- package/src/redis.js +7 -0
- package/src/router.js +21 -0
- package/src/service.js +10 -15
- package/test/test-cache.js +125 -31
- package/test/test-router.js +122 -2
- package/test/test-service.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@less-is-more/less-js",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-0",
|
|
4
4
|
"description": "Fast develop kit for nodejs",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"redis": "^4.0.1",
|
|
33
33
|
"sequelize": "^6.13.0",
|
|
34
34
|
"uuid": "^9.0.0",
|
|
35
|
-
"validator": "^13.7.0"
|
|
35
|
+
"validator": "^13.7.0",
|
|
36
|
+
"zstd-codec": "^0.1.5"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"mocha": "^9.1.3",
|
package/src/cache.js
CHANGED
|
@@ -2,17 +2,35 @@ const Redis = require("./redis");
|
|
|
2
2
|
const Param = require("./param");
|
|
3
3
|
const zlib = require("zlib");
|
|
4
4
|
const Nas = require("./nas");
|
|
5
|
+
const { ZstdCodec } = require("zstd-codec");
|
|
5
6
|
|
|
6
7
|
module.exports = class Cache {
|
|
7
8
|
static printLog = true;
|
|
9
|
+
static _zstd = null;
|
|
10
|
+
static _zstdInitPromise = null;
|
|
8
11
|
|
|
9
12
|
/*
|
|
10
13
|
* 设置是否打印日志
|
|
11
14
|
* @param {boolean} printLog
|
|
12
|
-
|
|
15
|
+
*/
|
|
13
16
|
static setPrintLog(printLog) {
|
|
14
17
|
Cache.printLog = printLog;
|
|
15
18
|
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 初始化 zstd 压缩(首次压缩时自动调用,也可提前调用)
|
|
22
|
+
*/
|
|
23
|
+
static async initZstd() {
|
|
24
|
+
if (Cache._zstd) return;
|
|
25
|
+
if (Cache._zstdInitPromise) return Cache._zstdInitPromise;
|
|
26
|
+
Cache._zstdInitPromise = new Promise((resolve) => {
|
|
27
|
+
ZstdCodec.run((zstd) => {
|
|
28
|
+
Cache._zstd = new zstd.Simple();
|
|
29
|
+
resolve();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
return Cache._zstdInitPromise;
|
|
33
|
+
}
|
|
16
34
|
/**
|
|
17
35
|
* 缓存并获取结果(调用方)
|
|
18
36
|
* @param {*} key 缓存关键词,拼接参数
|
|
@@ -66,30 +84,32 @@ module.exports = class Cache {
|
|
|
66
84
|
for (let i = 0; i < args.length; i++) {
|
|
67
85
|
fullKey += ":" + args[i].toString();
|
|
68
86
|
}
|
|
69
|
-
//
|
|
87
|
+
// 多参数的不做本地缓存,避免爆内存
|
|
88
|
+
const canLocalCache = args.length === 0;
|
|
89
|
+
|
|
90
|
+
// 先从本地缓存取(本地缓存存的是反序列化后的对象,命中时跳过JSON.parse)
|
|
70
91
|
let savedData = Cache._getLocal(fullKey);
|
|
71
|
-
let hasLocal =
|
|
92
|
+
let hasLocal = savedData !== null;
|
|
93
|
+
|
|
72
94
|
if (Param.isBlank(savedData)) {
|
|
73
95
|
if (useNas) {
|
|
74
96
|
savedData = Nas.get(fullKey);
|
|
75
97
|
} else {
|
|
76
98
|
savedData = await Redis.exec("get", fullKey);
|
|
77
99
|
}
|
|
78
|
-
if (!Param.isBlank(savedData)) {
|
|
79
|
-
// 本地缓存1分钟
|
|
80
|
-
Cache._setLocal(fullKey, savedData, 60000);
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
hasLocal = true;
|
|
84
100
|
}
|
|
85
|
-
|
|
101
|
+
|
|
102
|
+
if (Param.isBlank(savedData)) {
|
|
103
|
+
// 无缓存,执行函数获取结果
|
|
86
104
|
let result = await fn();
|
|
87
105
|
if (!Param.isBlank(result)) {
|
|
88
106
|
let saveContent = "";
|
|
89
107
|
// 处理JSON
|
|
90
108
|
if (result instanceof Object) {
|
|
91
109
|
if (result.success === false) {
|
|
92
|
-
|
|
110
|
+
if (Cache.printLog) {
|
|
111
|
+
console.log("Do not cache fail result");
|
|
112
|
+
}
|
|
93
113
|
} else {
|
|
94
114
|
saveContent = JSON.stringify(result);
|
|
95
115
|
}
|
|
@@ -97,8 +117,8 @@ module.exports = class Cache {
|
|
|
97
117
|
saveContent = result.toString();
|
|
98
118
|
}
|
|
99
119
|
if (saveContent != "") {
|
|
100
|
-
if (
|
|
101
|
-
saveContent = Cache._zip(saveContent);
|
|
120
|
+
if (zip) {
|
|
121
|
+
saveContent = await Cache._zip(saveContent);
|
|
102
122
|
}
|
|
103
123
|
if (useNas) {
|
|
104
124
|
Nas.set(fullKey, saveContent, timeSecond);
|
|
@@ -106,54 +126,147 @@ module.exports = class Cache {
|
|
|
106
126
|
await Redis.exec("setex", fullKey, timeSecond + "", saveContent);
|
|
107
127
|
}
|
|
108
128
|
|
|
109
|
-
//
|
|
110
|
-
|
|
129
|
+
// 本地缓存直接存结果对象,下次命中跳过反序列化
|
|
130
|
+
if (canLocalCache) {
|
|
131
|
+
Cache._setLocal(fullKey, result, 60000);
|
|
132
|
+
}
|
|
111
133
|
}
|
|
112
134
|
return result;
|
|
113
135
|
}
|
|
114
136
|
} else {
|
|
137
|
+
// 有缓存(本地缓存或Nas/Redis)
|
|
138
|
+
if (hasLocal) {
|
|
139
|
+
// 本地缓存命中,直接返回已解析的对象,跳过反序列化
|
|
140
|
+
if (Cache.printLog) {
|
|
141
|
+
console.log("Found cache local", fullKey);
|
|
142
|
+
}
|
|
143
|
+
return savedData;
|
|
144
|
+
}
|
|
115
145
|
if (Cache.printLog) {
|
|
116
|
-
console.log("Found cache"
|
|
146
|
+
console.log("Found cache", fullKey);
|
|
117
147
|
}
|
|
118
|
-
savedData = Cache._unzip(savedData);
|
|
148
|
+
savedData = await Cache._unzip(savedData);
|
|
149
|
+
let result;
|
|
119
150
|
if (typeof savedData === "string") {
|
|
120
151
|
// 优先转成json
|
|
121
152
|
try {
|
|
122
|
-
|
|
153
|
+
result = JSON.parse(savedData);
|
|
123
154
|
} catch (e) {
|
|
124
|
-
|
|
155
|
+
result = savedData;
|
|
125
156
|
}
|
|
126
157
|
} else {
|
|
127
|
-
|
|
158
|
+
result = savedData;
|
|
159
|
+
}
|
|
160
|
+
// 本地缓存存解析后的对象,下次命中跳过反序列化
|
|
161
|
+
if (canLocalCache) {
|
|
162
|
+
Cache._setLocal(fullKey, result, 60000);
|
|
128
163
|
}
|
|
164
|
+
return result;
|
|
129
165
|
}
|
|
130
166
|
}
|
|
131
167
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
168
|
+
/**
|
|
169
|
+
* 直接执行并写入缓存(不读取缓存)
|
|
170
|
+
* @param {string} keyPrefix 缓存前缀
|
|
171
|
+
* @param {number} timeSecond 缓存时间
|
|
172
|
+
* @param {*} args 直接传arguments,用于拼接缓存key
|
|
173
|
+
* @param {function} fn 实现函数
|
|
174
|
+
* @param {boolean} zip 是否压缩
|
|
175
|
+
* @param {boolean} useNas 是否使用Nas缓存
|
|
176
|
+
* @returns 优先返回对象
|
|
177
|
+
*/
|
|
178
|
+
static async write(
|
|
179
|
+
keyPrefix,
|
|
180
|
+
timeSecond,
|
|
181
|
+
args,
|
|
182
|
+
fn,
|
|
183
|
+
zip = false,
|
|
184
|
+
useNas = false,
|
|
185
|
+
) {
|
|
186
|
+
let fullKey = keyPrefix;
|
|
187
|
+
for (let i = 0; i < args.length; i++) {
|
|
188
|
+
fullKey += ":" + args[i].toString();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 直接执行函数获取结果
|
|
192
|
+
let result = await fn();
|
|
193
|
+
if (!Param.isBlank(result)) {
|
|
194
|
+
let saveContent = "";
|
|
195
|
+
// 处理JSON
|
|
196
|
+
if (result instanceof Object) {
|
|
197
|
+
if (result.success === false) {
|
|
198
|
+
if (Cache.printLog) {
|
|
199
|
+
console.log("Do not cache fail result");
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
saveContent = JSON.stringify(result);
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
saveContent = result.toString();
|
|
206
|
+
}
|
|
207
|
+
if (saveContent != "") {
|
|
208
|
+
if (zip) {
|
|
209
|
+
saveContent = await Cache._zip(saveContent);
|
|
210
|
+
}
|
|
211
|
+
if (useNas) {
|
|
212
|
+
Nas.set(fullKey, saveContent, timeSecond);
|
|
213
|
+
} else {
|
|
214
|
+
await Redis.exec("setex", fullKey, timeSecond + "", saveContent);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 本地缓存直接存结果对象
|
|
218
|
+
Cache._setLocal(fullKey, result, 60000);
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
static async _zip(text) {
|
|
225
|
+
await Cache.initZstd();
|
|
226
|
+
const compressed = Cache._zstd.compress(Buffer.from(text));
|
|
227
|
+
return Buffer.from(compressed).toString("base64");
|
|
135
228
|
}
|
|
136
229
|
|
|
137
|
-
static _unzip(content) {
|
|
230
|
+
static async _unzip(content) {
|
|
138
231
|
if (typeof content === "string") {
|
|
139
|
-
let text = "";
|
|
140
232
|
const buffer = Buffer.from(content, "base64");
|
|
141
|
-
//
|
|
233
|
+
// zstd格式检测(魔术字节 0x28 0xB5 0x2F 0xFD)
|
|
234
|
+
if (
|
|
235
|
+
buffer.length > 4 &&
|
|
236
|
+
buffer[0] === 0x28 &&
|
|
237
|
+
buffer[1] === 0xb5 &&
|
|
238
|
+
buffer[2] === 0x2f &&
|
|
239
|
+
buffer[3] === 0xfd
|
|
240
|
+
) {
|
|
241
|
+
await Cache.initZstd();
|
|
242
|
+
return Buffer.from(Cache._zstd.decompress(buffer)).toString();
|
|
243
|
+
}
|
|
244
|
+
// gzip格式检测(魔术字节 0x1F 0x8B)
|
|
142
245
|
if (buffer.length > 2 && buffer[0] === 0x1f && buffer[1] === 0x8b) {
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
246
|
+
return zlib.gunzipSync(buffer).toString();
|
|
247
|
+
}
|
|
248
|
+
// 非压缩数据,直接返回原始字符串
|
|
249
|
+
return content;
|
|
250
|
+
} else if (Buffer.isBuffer(content)) {
|
|
251
|
+
if (
|
|
252
|
+
content.length > 4 &&
|
|
253
|
+
content[0] === 0x28 &&
|
|
254
|
+
content[1] === 0xb5 &&
|
|
255
|
+
content[2] === 0x2f &&
|
|
256
|
+
content[3] === 0xfd
|
|
257
|
+
) {
|
|
258
|
+
await Cache.initZstd();
|
|
259
|
+
return Buffer.from(Cache._zstd.decompress(content)).toString();
|
|
146
260
|
}
|
|
147
|
-
|
|
261
|
+
if (content.length > 2 && content[0] === 0x1f && content[1] === 0x8b) {
|
|
262
|
+
return zlib.gunzipSync(content).toString();
|
|
263
|
+
}
|
|
264
|
+
return content.toString();
|
|
148
265
|
} else {
|
|
149
266
|
return content;
|
|
150
267
|
}
|
|
151
268
|
}
|
|
152
269
|
|
|
153
|
-
static _needZip(text, zip) {
|
|
154
|
-
return zip || Buffer.byteLength(text) / 1024 > 1;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
270
|
static setWebCache(res, expireSecond) {
|
|
158
271
|
res.setHeader("Cache-Control", "max-age=" + expireSecond);
|
|
159
272
|
}
|
package/src/nas.js
CHANGED
|
@@ -8,13 +8,14 @@ const os = require("os");
|
|
|
8
8
|
* 适用于多服务器共享NAS存储
|
|
9
9
|
*/
|
|
10
10
|
class Nas {
|
|
11
|
+
static _cacheDir = null;
|
|
12
|
+
|
|
11
13
|
static _getCacheDir() {
|
|
12
|
-
|
|
14
|
+
if (Nas._cacheDir) return Nas._cacheDir;
|
|
13
15
|
|
|
14
16
|
// 如果提供了缓存目录参数,则使用该目录,否则使用默认目录
|
|
15
17
|
const defaultCacheDir = path.join(os.tmpdir(), "cache");
|
|
16
|
-
cacheDir =
|
|
17
|
-
cacheDir || (fs.existsSync("/cache") ? "/cache" : defaultCacheDir);
|
|
18
|
+
const cacheDir = fs.existsSync("/cache") ? "/cache" : defaultCacheDir;
|
|
18
19
|
|
|
19
20
|
// 确保缓存目录存在
|
|
20
21
|
try {
|
|
@@ -24,6 +25,7 @@ class Nas {
|
|
|
24
25
|
} catch (e) {
|
|
25
26
|
console.error("创建缓存目录失败:", cacheDir, e);
|
|
26
27
|
}
|
|
28
|
+
Nas._cacheDir = cacheDir;
|
|
27
29
|
return cacheDir;
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -90,27 +92,24 @@ class Nas {
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
// 读取缓存文件
|
|
93
|
-
|
|
94
|
-
const lines = content.split("\n");
|
|
95
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
95
96
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// 找到第一个换行符,前面为过期时间戳,后面为 base64 编码的值
|
|
98
|
+
const nlIdx = content.indexOf("\n");
|
|
99
|
+
if (nlIdx < 0) return null;
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
const expireTime = parseInt(content.substring(0, nlIdx));
|
|
102
|
+
const value = content.substring(nlIdx + 1); // 处理值中可能包含换行符的情况
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
return null;
|
|
104
|
+
if (!isNaN(expireTime)) {
|
|
105
|
+
if (expireTime === -1 || Date.now() < expireTime) {
|
|
106
|
+
// 文件未过期,返回值
|
|
107
|
+
let valueStr = Buffer.from(value, "base64").toString("utf8");
|
|
108
|
+
try {
|
|
109
|
+
return JSON.parse(valueStr);
|
|
110
|
+
} catch (e) {
|
|
111
|
+
// 如果解析JSON失败,则返回原始字符串
|
|
112
|
+
return valueStr;
|
|
114
113
|
}
|
|
115
114
|
}
|
|
116
115
|
}
|
|
@@ -137,11 +136,12 @@ class Nas {
|
|
|
137
136
|
const content = Buffer.from(valueStr, "utf8").toString("base64");
|
|
138
137
|
const filePath = Nas._getFilePath(key);
|
|
139
138
|
|
|
139
|
+
// 创建临时文件名,避免写入过程中的并发问题
|
|
140
|
+
const timestamp = Date.now();
|
|
141
|
+
const randomSuffix = Math.random().toFixed(4) * 10000;
|
|
142
|
+
const tempFilePath = `${filePath}.tmp.${timestamp}.${randomSuffix}`;
|
|
143
|
+
|
|
140
144
|
try {
|
|
141
|
-
// 创建临时文件名,避免写入过程中的并发问题
|
|
142
|
-
const timestamp = Date.now();
|
|
143
|
-
const randomSuffix = Math.random().toFixed(4) * 10000;
|
|
144
|
-
const tempFilePath = `${filePath}.tmp.${timestamp}.${randomSuffix}`;
|
|
145
145
|
// 写入过期时间戳和值,用换行符分隔
|
|
146
146
|
const fileContent = `${expireTime}\n${content}`;
|
|
147
147
|
fs.writeFileSync(tempFilePath, fileContent, "utf8");
|
|
@@ -199,24 +199,20 @@ class Nas {
|
|
|
199
199
|
const filePath = path.join(cacheDir, file);
|
|
200
200
|
// 检查文件是否过期超过一分钟,如果超过则删除
|
|
201
201
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const expireTimeString = lines[0];
|
|
208
|
-
const expireTime = parseInt(expireTimeString);
|
|
202
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
203
|
+
const nlIdx = content.indexOf("\n");
|
|
204
|
+
const expireTimeString =
|
|
205
|
+
nlIdx >= 0 ? content.substring(0, nlIdx) : content;
|
|
206
|
+
const expireTime = parseInt(expireTimeString);
|
|
209
207
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
}
|
|
208
|
+
if (
|
|
209
|
+
!isNaN(expireTime) &&
|
|
210
|
+
expireTime !== -1 &&
|
|
211
|
+
// 仅删除过期超过一分钟的文件
|
|
212
|
+
Date.now() > expireTime + 60 * 1000
|
|
213
|
+
) {
|
|
214
|
+
console.log("删除过期缓存文件:", filePath);
|
|
215
|
+
fs.unlinkSync(filePath);
|
|
220
216
|
}
|
|
221
217
|
} catch (e) {
|
|
222
218
|
console.warn("检查缓存文件过期时间失败:", filePath, e);
|
|
@@ -269,21 +265,15 @@ class Nas {
|
|
|
269
265
|
|
|
270
266
|
// 读取缓存文件
|
|
271
267
|
const content = fs.readFileSync(filePath, "utf8");
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const expireTime = parseInt(expireTimeString);
|
|
268
|
+
const nlIdx = content.indexOf("\n");
|
|
269
|
+
const expireTimeString =
|
|
270
|
+
nlIdx >= 0 ? content.substring(0, nlIdx) : content;
|
|
271
|
+
const expireTime = parseInt(expireTimeString);
|
|
277
272
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
return Math.max(0, remainingTime); // 确保返回非负值
|
|
283
|
-
} else {
|
|
284
|
-
return -1;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
273
|
+
if (!isNaN(expireTime) && expireTime !== -1 && Date.now() < expireTime) {
|
|
274
|
+
// 文件未过期,计算剩余时间
|
|
275
|
+
const remainingTime = Math.floor((expireTime - Date.now()) / 1000);
|
|
276
|
+
return Math.max(0, remainingTime);
|
|
287
277
|
}
|
|
288
278
|
} catch (e) {
|
|
289
279
|
console.warn("读取缓存文件失败:", filePath, e);
|
package/src/redis.js
CHANGED
|
@@ -42,6 +42,13 @@ module.exports = class Redis {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
static async close() {
|
|
46
|
+
if (this.#client) {
|
|
47
|
+
await this.#client.quit();
|
|
48
|
+
this.#client = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
45
52
|
static async exec(name, ...args) {
|
|
46
53
|
let client = await Redis.getInstance();
|
|
47
54
|
args.unshift(name);
|
package/src/router.js
CHANGED
|
@@ -8,6 +8,7 @@ const zlib = require("zlib");
|
|
|
8
8
|
|
|
9
9
|
module.exports = class Router {
|
|
10
10
|
static #defaultFormat = true;
|
|
11
|
+
static #runtimeEnv = "test";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 根据request路由
|
|
@@ -94,6 +95,8 @@ module.exports = class Router {
|
|
|
94
95
|
}
|
|
95
96
|
const method = controller[methodName];
|
|
96
97
|
if (method) {
|
|
98
|
+
this._setRuntimeEnv(req.headers);
|
|
99
|
+
|
|
97
100
|
try {
|
|
98
101
|
// before
|
|
99
102
|
if (targets.before) {
|
|
@@ -328,4 +331,22 @@ module.exports = class Router {
|
|
|
328
331
|
|
|
329
332
|
return ipv4Regex.test(ip.trim()) || ipv6Regex.test(ip.trim());
|
|
330
333
|
}
|
|
334
|
+
|
|
335
|
+
static _setRuntimeEnv(headers) {
|
|
336
|
+
let host = headers["host"] || headers["Host"];
|
|
337
|
+
host = host.split(",")[0];
|
|
338
|
+
if (host.includes("127.0.0.1") || host.includes("localhost")) {
|
|
339
|
+
this.#runtimeEnv = "test";
|
|
340
|
+
} else {
|
|
341
|
+
const match = host.match(/((test|stage)[\d]*)\./);
|
|
342
|
+
if (match) {
|
|
343
|
+
this.#runtimeEnv = match[1];
|
|
344
|
+
} else {
|
|
345
|
+
this.#runtimeEnv = "prod";
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
static getRuntimeEnv() {
|
|
350
|
+
return this.#runtimeEnv;
|
|
351
|
+
}
|
|
331
352
|
};
|
package/src/service.js
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
const { default: axios } = require("axios");
|
|
2
2
|
const qs = require("qs");
|
|
3
3
|
const Ret = require("./ret");
|
|
4
|
+
const Router = require("./router");
|
|
4
5
|
|
|
5
6
|
module.exports = class Service {
|
|
6
7
|
static #domain;
|
|
7
|
-
static #isTest;
|
|
8
|
-
static #stage;
|
|
9
|
-
static #urlPrefix;
|
|
10
8
|
|
|
11
|
-
static init(domain
|
|
9
|
+
static init(domain) {
|
|
12
10
|
this.#domain = domain;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
(isStage ? "stage" : isTest ? "test" : "prod") +
|
|
18
|
-
"." +
|
|
19
|
-
domain;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static _getUrlPrefix() {
|
|
14
|
+
return "http://SERVICE." + Router.getRuntimeEnv() + "." + this.#domain;
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
static setDelay(async = false, asyncDelaySecond = 0) {
|
|
@@ -38,12 +33,12 @@ module.exports = class Service {
|
|
|
38
33
|
async = false,
|
|
39
34
|
asyncDelaySecond = 0,
|
|
40
35
|
) {
|
|
41
|
-
let url = this
|
|
36
|
+
let url = this._getUrlPrefix().replace("SERVICE", serviceName) + path;
|
|
42
37
|
try {
|
|
43
38
|
if (params) {
|
|
44
39
|
url += "?" + qs.stringify(params);
|
|
45
40
|
}
|
|
46
|
-
let headers =
|
|
41
|
+
let headers = Service.setDelay(async, asyncDelaySecond);
|
|
47
42
|
headers["accept-encoding"] = "gzip";
|
|
48
43
|
const result = await axios.get(url, { headers });
|
|
49
44
|
return result.data;
|
|
@@ -61,9 +56,9 @@ module.exports = class Service {
|
|
|
61
56
|
asyncDelaySecond = 0,
|
|
62
57
|
formFormat = true,
|
|
63
58
|
) {
|
|
64
|
-
let url = this
|
|
59
|
+
let url = this._getUrlPrefix().replace("SERVICE", serviceName) + path;
|
|
65
60
|
try {
|
|
66
|
-
let headers =
|
|
61
|
+
let headers = Service.setDelay(async, asyncDelaySecond);
|
|
67
62
|
if (!formFormat) {
|
|
68
63
|
headers["Content-Type"] = "application/json";
|
|
69
64
|
}
|
package/test/test-cache.js
CHANGED
|
@@ -9,6 +9,10 @@ describe("cache.js", () => {
|
|
|
9
9
|
Redis.init("127.0.0.1", "default", "KS5UggH4LNyyLBdr");
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
+
after(async () => {
|
|
13
|
+
await Redis.close();
|
|
14
|
+
});
|
|
15
|
+
|
|
12
16
|
describe("get()", () => {
|
|
13
17
|
it("return string", async () => {
|
|
14
18
|
let testFunction = (a, b) => {
|
|
@@ -21,7 +25,7 @@ describe("cache.js", () => {
|
|
|
21
25
|
testFunction,
|
|
22
26
|
testFunction,
|
|
23
27
|
1,
|
|
24
|
-
2
|
|
28
|
+
2,
|
|
25
29
|
);
|
|
26
30
|
console.log("result", result);
|
|
27
31
|
assert(result === "3");
|
|
@@ -38,7 +42,7 @@ describe("cache.js", () => {
|
|
|
38
42
|
testFunction,
|
|
39
43
|
testFunction,
|
|
40
44
|
1,
|
|
41
|
-
2
|
|
45
|
+
2,
|
|
42
46
|
);
|
|
43
47
|
} catch (e) {
|
|
44
48
|
assert(e.message === "只支持返回值是文本的方法");
|
|
@@ -56,7 +60,7 @@ describe("cache.js", () => {
|
|
|
56
60
|
}
|
|
57
61
|
const result = await test(1, 2);
|
|
58
62
|
console.log("result", result);
|
|
59
|
-
assert(result
|
|
63
|
+
assert(result == "3");
|
|
60
64
|
});
|
|
61
65
|
it("return int", async () => {
|
|
62
66
|
async function test(a, b) {
|
|
@@ -108,22 +112,22 @@ describe("cache.js", () => {
|
|
|
108
112
|
let a = 1,
|
|
109
113
|
b = 2;
|
|
110
114
|
let result = await Cache.auto(
|
|
111
|
-
"
|
|
115
|
+
"test_base64",
|
|
112
116
|
20,
|
|
113
117
|
[],
|
|
114
118
|
async () => {
|
|
115
119
|
console.log(a + b);
|
|
116
120
|
return a + b + "";
|
|
117
121
|
},
|
|
118
|
-
true
|
|
122
|
+
true,
|
|
119
123
|
);
|
|
120
124
|
console.log("result", result);
|
|
121
|
-
assert(result
|
|
125
|
+
assert(result == "3");
|
|
122
126
|
});
|
|
123
127
|
|
|
124
128
|
it("auto long text base64", async () => {
|
|
125
129
|
let result = await Cache.auto(
|
|
126
|
-
"
|
|
130
|
+
"test_long_base64",
|
|
127
131
|
20,
|
|
128
132
|
[],
|
|
129
133
|
async () => {
|
|
@@ -135,38 +139,128 @@ describe("cache.js", () => {
|
|
|
135
139
|
console.log(text);
|
|
136
140
|
return text;
|
|
137
141
|
},
|
|
138
|
-
true
|
|
142
|
+
true,
|
|
139
143
|
);
|
|
140
144
|
console.log("result", result);
|
|
141
|
-
assert(result ===
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("auto long text default zip", async () => {
|
|
145
|
-
let text = "";
|
|
146
|
-
// 循环1000次
|
|
147
|
-
for (let i = 0; i < 2000; i++) {
|
|
148
|
-
text += "a";
|
|
149
|
-
}
|
|
150
|
-
console.log(text);
|
|
151
|
-
let result = await Cache.auto("test", 20, [], async () => {
|
|
152
|
-
return text;
|
|
153
|
-
});
|
|
154
|
-
console.log("result", result);
|
|
155
|
-
assert(result === text);
|
|
145
|
+
assert(result.length === 1000);
|
|
156
146
|
});
|
|
157
147
|
|
|
158
|
-
it("local cache", async () => {
|
|
148
|
+
it("local cache stores parsed object", async () => {
|
|
159
149
|
async function test(a, b) {
|
|
160
|
-
return await Cache.auto("
|
|
161
|
-
console.log(a, b);
|
|
150
|
+
return await Cache.auto("test_local_obj", 20, arguments, () => {
|
|
162
151
|
return { a, b };
|
|
163
152
|
});
|
|
164
153
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
154
|
+
// 第一次调用,写入缓存
|
|
155
|
+
const result1 = await test(1, 2);
|
|
156
|
+
assert(result1.a === 1);
|
|
157
|
+
// 第二次调用,应该命中本地缓存,直接返回对象
|
|
158
|
+
const result2 = await test(1, 2);
|
|
159
|
+
assert(result2.a === 1);
|
|
160
|
+
// 验证本地缓存存的是对象而不是字符串
|
|
161
|
+
const localKey = "test_local_obj:1:2";
|
|
162
|
+
const localItem = Cache.localCache[localKey];
|
|
163
|
+
// 多参数不做本地缓存
|
|
164
|
+
assert(localItem === undefined);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("local cache only for zero-args", async () => {
|
|
168
|
+
let callCount = 0;
|
|
169
|
+
const result1 = await Cache.auto("test_zero_args", 20, [], () => {
|
|
170
|
+
callCount++;
|
|
171
|
+
return { count: callCount };
|
|
172
|
+
});
|
|
173
|
+
assert(result1.count === 1);
|
|
174
|
+
// 第二次调用应该命中本地缓存,callCount不增加
|
|
175
|
+
const result2 = await Cache.auto("test_zero_args", 20, [], () => {
|
|
176
|
+
callCount++;
|
|
177
|
+
return { count: callCount };
|
|
178
|
+
});
|
|
179
|
+
assert(result2.count === 1);
|
|
180
|
+
// 验证本地缓存存的是对象
|
|
181
|
+
const localItem = Cache.localCache["test_zero_args"];
|
|
182
|
+
assert(localItem !== undefined);
|
|
183
|
+
assert(typeof localItem.value === "object");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("write()", () => {
|
|
188
|
+
it("write and verify cache", async () => {
|
|
189
|
+
let callCount = 0;
|
|
190
|
+
const result = await Cache.write("test_write", 20, [], () => {
|
|
191
|
+
callCount++;
|
|
192
|
+
return { count: callCount };
|
|
193
|
+
});
|
|
194
|
+
assert(result.count === 1);
|
|
195
|
+
// write不读取缓存,直接执行fn
|
|
196
|
+
const result2 = await Cache.write("test_write", 20, [], () => {
|
|
197
|
+
callCount++;
|
|
198
|
+
return { count: callCount };
|
|
199
|
+
});
|
|
200
|
+
assert(result2.count === 2);
|
|
201
|
+
// 验证本地缓存存在
|
|
202
|
+
const localItem = Cache.localCache["test_write"];
|
|
203
|
+
assert(localItem !== undefined);
|
|
204
|
+
assert(localItem.value.count === 2);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("write does not cache fail result", async () => {
|
|
208
|
+
const result = await Cache.write("test_write_fail", 20, [], () => {
|
|
209
|
+
return new Ret(false);
|
|
210
|
+
});
|
|
211
|
+
assert(result.success === false);
|
|
212
|
+
// 失败结果不应该被本地缓存
|
|
213
|
+
const localItem = Cache.localCache["test_write_fail"];
|
|
214
|
+
assert(localItem === undefined);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("zstd", () => {
|
|
219
|
+
it("initZstd", async () => {
|
|
220
|
+
await Cache.initZstd();
|
|
221
|
+
assert(Cache._zstd !== null);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("_zip and _unzip with zstd", async () => {
|
|
225
|
+
const text = '{"success":true,"data":"hello zstd"}';
|
|
226
|
+
const compressed = await Cache._zip(text);
|
|
227
|
+
assert(typeof compressed === "string");
|
|
228
|
+
assert(compressed !== text);
|
|
229
|
+
// 解压
|
|
230
|
+
const decompressed = await Cache._unzip(compressed);
|
|
231
|
+
assert(decompressed === text);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("auto with zstd compression", async () => {
|
|
235
|
+
const result = await Cache.auto(
|
|
236
|
+
"test_zstd_auto",
|
|
237
|
+
20,
|
|
238
|
+
[],
|
|
239
|
+
async () => {
|
|
240
|
+
let text = "";
|
|
241
|
+
for (let i = 0; i < 500; i++) {
|
|
242
|
+
text += "zstd test data ";
|
|
243
|
+
}
|
|
244
|
+
return text;
|
|
245
|
+
},
|
|
246
|
+
true,
|
|
247
|
+
);
|
|
248
|
+
assert(typeof result === "string");
|
|
249
|
+
assert(result.length > 0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("write with zstd compression", async () => {
|
|
253
|
+
const result = await Cache.write(
|
|
254
|
+
"test_zstd_write",
|
|
255
|
+
20,
|
|
256
|
+
[],
|
|
257
|
+
async () => {
|
|
258
|
+
return { message: "zstd write test", count: 42 };
|
|
259
|
+
},
|
|
260
|
+
true,
|
|
261
|
+
);
|
|
262
|
+
assert(result.message === "zstd write test");
|
|
263
|
+
assert(result.count === 42);
|
|
170
264
|
});
|
|
171
265
|
});
|
|
172
266
|
});
|
package/test/test-router.js
CHANGED
|
@@ -136,7 +136,7 @@ describe("router.js", () => {
|
|
|
136
136
|
send: function (data) {
|
|
137
137
|
console.log("send: " + data);
|
|
138
138
|
assert(
|
|
139
|
-
data === '{"code":1000,"success":false,"message":"请输入发货类型"}'
|
|
139
|
+
data === '{"code":1000,"success":false,"message":"请输入发货类型"}',
|
|
140
140
|
);
|
|
141
141
|
},
|
|
142
142
|
};
|
|
@@ -155,7 +155,7 @@ describe("router.js", () => {
|
|
|
155
155
|
send: function (data) {
|
|
156
156
|
console.log("send: " + data);
|
|
157
157
|
assert(
|
|
158
|
-
data === '{"code":1000,"success":false,"message":"请输入发货类型"}'
|
|
158
|
+
data === '{"code":1000,"success":false,"message":"请输入发货类型"}',
|
|
159
159
|
);
|
|
160
160
|
},
|
|
161
161
|
};
|
|
@@ -257,4 +257,124 @@ describe("router.js", () => {
|
|
|
257
257
|
await Router.route(targets, req, res);
|
|
258
258
|
console.log("end");
|
|
259
259
|
});
|
|
260
|
+
|
|
261
|
+
it("runtime env test", async () => {
|
|
262
|
+
let targets = {
|
|
263
|
+
"/": {
|
|
264
|
+
add: function (req, res) {
|
|
265
|
+
res.send("ok");
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
let req = { path: "/add", headers: { host: "aa.test.example.com" } };
|
|
270
|
+
let res = {
|
|
271
|
+
send: function (data) {
|
|
272
|
+
console.log("send: " + data);
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
console.log("start");
|
|
276
|
+
await Router.route(targets, req, res);
|
|
277
|
+
console.log("end");
|
|
278
|
+
assert(Router.getRuntimeEnv() === "test");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("runtime env test2", async () => {
|
|
282
|
+
let targets = {
|
|
283
|
+
"/": {
|
|
284
|
+
add: function (req, res) {
|
|
285
|
+
res.send("ok");
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
let req = { path: "/add", headers: { host: "aa.test2.example.com" } };
|
|
290
|
+
let res = {
|
|
291
|
+
send: function (data) {
|
|
292
|
+
console.log("send: " + data);
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
console.log("start");
|
|
296
|
+
await Router.route(targets, req, res);
|
|
297
|
+
console.log("end");
|
|
298
|
+
assert(Router.getRuntimeEnv() === "test2");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it("runtime env stage", async () => {
|
|
302
|
+
let targets = {
|
|
303
|
+
"/": {
|
|
304
|
+
add: function (req, res) {
|
|
305
|
+
res.send("ok");
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
let req = { path: "/add", headers: { host: "aa.stage.example.com" } };
|
|
310
|
+
let res = {
|
|
311
|
+
send: function (data) {
|
|
312
|
+
console.log("send: " + data);
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
console.log("start");
|
|
316
|
+
await Router.route(targets, req, res);
|
|
317
|
+
console.log("end");
|
|
318
|
+
assert(Router.getRuntimeEnv() === "stage");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("runtime env prod", async () => {
|
|
322
|
+
let targets = {
|
|
323
|
+
"/": {
|
|
324
|
+
add: function (req, res) {
|
|
325
|
+
res.send("ok");
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
let req = { path: "/add", headers: { host: "aa.prod.example.com" } };
|
|
330
|
+
let res = {
|
|
331
|
+
send: function (data) {
|
|
332
|
+
console.log("send: " + data);
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
console.log("start");
|
|
336
|
+
await Router.route(targets, req, res);
|
|
337
|
+
console.log("end");
|
|
338
|
+
assert(Router.getRuntimeEnv() === "prod");
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("runtime env other", async () => {
|
|
342
|
+
let targets = {
|
|
343
|
+
"/": {
|
|
344
|
+
add: function (req, res) {
|
|
345
|
+
res.send("ok");
|
|
346
|
+
},
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
let req = { path: "/add", headers: { host: "aa.example.com" } };
|
|
350
|
+
let res = {
|
|
351
|
+
send: function (data) {
|
|
352
|
+
console.log("send: " + data);
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
console.log("start");
|
|
356
|
+
await Router.route(targets, req, res);
|
|
357
|
+
console.log("end");
|
|
358
|
+
assert(Router.getRuntimeEnv() === "prod");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("runtime env local", async () => {
|
|
362
|
+
let targets = {
|
|
363
|
+
"/": {
|
|
364
|
+
add: function (req, res) {
|
|
365
|
+
res.send("ok");
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
let req = { path: "/add", headers: { host: "127.0.0.1" } };
|
|
370
|
+
let res = {
|
|
371
|
+
send: function (data) {
|
|
372
|
+
console.log("send: " + data);
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
console.log("start");
|
|
376
|
+
await Router.route(targets, req, res);
|
|
377
|
+
console.log("end");
|
|
378
|
+
assert(Router.getRuntimeEnv() === "test");
|
|
379
|
+
});
|
|
260
380
|
});
|
package/test/test-service.js
CHANGED
|
@@ -11,7 +11,7 @@ describe("service.js", () => {
|
|
|
11
11
|
it("ok", async () => {
|
|
12
12
|
const result = await Service.get("supplier", "/");
|
|
13
13
|
console.log(result);
|
|
14
|
-
assert(result.code ==
|
|
14
|
+
assert(result.code == 1000);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it("async", async () => {
|
|
@@ -30,7 +30,7 @@ describe("service.js", () => {
|
|
|
30
30
|
it("ok", async () => {
|
|
31
31
|
const result = await Service.post("supplier", "/");
|
|
32
32
|
console.log(result);
|
|
33
|
-
assert(result.code ==
|
|
33
|
+
assert(result.code == 1000);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
it("async", async () => {
|