@less-is-more/less-js 1.5.3-0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@less-is-more/less-js",
3
- "version": "1.5.3-0",
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 = false;
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
- if (savedData == null) {
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
- console.log("Do not cache fail result");
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 (Cache._needZip(saveContent, zip)) {
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
- // 本地缓存1分钟
110
- Cache._setLocal(fullKey, saveContent, 60000);
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" + (hasLocal ? " local" : ""), fullKey);
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
- return JSON.parse(savedData);
153
+ result = JSON.parse(savedData);
123
154
  } catch (e) {
124
- return savedData;
155
+ result = savedData;
125
156
  }
126
157
  } else {
127
- return savedData;
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
- static _zip(text) {
133
- const zipData = zlib.gzipSync(Buffer.from(text));
134
- return zipData.toString("base64");
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
- // 判断是否gzip
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
- text = zlib.gunzipSync(buffer).toString();
144
- } else {
145
- text = content;
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
- return text;
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
- let cacheDir = null;
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
- let content = fs.readFileSync(filePath, "utf8");
94
- const lines = content.split("\n");
95
+ const content = fs.readFileSync(filePath, "utf8");
95
96
 
96
- if (lines.length >= 2) {
97
- const expireTimeString = lines[0];
98
- const value = lines.slice(1).join("\n"); // 处理值中可能包含换行符的情况
97
+ // 找到第一个换行符,前面为过期时间戳,后面为 base64 编码的值
98
+ const nlIdx = content.indexOf("\n");
99
+ if (nlIdx < 0) return null;
99
100
 
100
- const expireTime = parseInt(expireTimeString);
101
+ const expireTime = parseInt(content.substring(0, nlIdx));
102
+ const value = content.substring(nlIdx + 1); // 处理值中可能包含换行符的情况
101
103
 
102
- if (!isNaN(expireTime)) {
103
- if (expireTime === -1 || Date.now() < expireTime) {
104
- // 文件未过期,返回值
105
- let valueStr = Buffer.from(value, "base64").toString("utf8");
106
- try {
107
- return JSON.parse(valueStr);
108
- } catch (e) {
109
- // 如果解析JSON失败,则返回原始字符串
110
- return valueStr;
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
- if (fs.existsSync(filePath)) {
203
- const content = fs.readFileSync(filePath, "utf8");
204
- const lines = content.split("\n");
205
-
206
- if (lines.length >= 1) {
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
- if (
211
- !isNaN(expireTime) &&
212
- expireTime !== -1 &&
213
- // 仅删除过期超过一分钟的文件
214
- Date.now() > expireTime + 60 * 1000
215
- ) {
216
- console.log("删除过期缓存文件:", filePath);
217
- fs.unlinkSync(filePath);
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 lines = content.split("\n");
273
-
274
- if (lines.length >= 1) {
275
- const expireTimeString = lines[0];
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
- if (!isNaN(expireTime)) {
279
- if (Date.now() < expireTime) {
280
- // 文件未过期,计算剩余时间
281
- const remainingTime = Math.floor((expireTime - Date.now()) / 1000);
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, isTest = true, isStage = false) {
9
+ static init(domain) {
12
10
  this.#domain = domain;
13
- this.#isTest = isTest;
14
- this.#stage = isStage;
15
- this.#urlPrefix =
16
- "http://SERVICE." +
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.#urlPrefix.replace("SERVICE", serviceName) + path;
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 = this.setDelay(async, asyncDelaySecond);
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.#urlPrefix.replace("SERVICE", serviceName) + path;
59
+ let url = this._getUrlPrefix().replace("SERVICE", serviceName) + path;
65
60
  try {
66
- let headers = this.setDelay(async, asyncDelaySecond);
61
+ let headers = Service.setDelay(async, asyncDelaySecond);
67
62
  if (!formFormat) {
68
63
  headers["Content-Type"] = "application/json";
69
64
  }
@@ -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 === "3");
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
- "test",
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 === "3");
125
+ assert(result == "3");
122
126
  });
123
127
 
124
128
  it("auto long text base64", async () => {
125
129
  let result = await Cache.auto(
126
- "test",
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 === "3");
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("test3", 20, arguments, () => {
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
- const result = await test(1, 2);
166
- console.log("cache", Cache.localCache);
167
- console.log("result", JSON.stringify(result));
168
- const compare = { a: 1, b: 2 };
169
- assert(result.a === compare.a);
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
  });
@@ -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
  });
@@ -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 == 1004);
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 == 1004);
33
+ assert(result.code == 1000);
34
34
  });
35
35
 
36
36
  it("async", async () => {