chanjs 2.7.3 → 2.7.5

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.
Files changed (93) hide show
  1. package/USAGE.md +533 -0
  2. package/config/index.js +37 -6
  3. package/core/App.js +166 -0
  4. package/core/Container.js +77 -0
  5. package/core/Controller.js +29 -0
  6. package/core/Database.js +93 -0
  7. package/core/Repository.js +327 -0
  8. package/core/Service.js +11 -0
  9. package/core/bootstrap/error-handler.js +104 -0
  10. package/core/bootstrap/hook-runner.js +64 -0
  11. package/core/bootstrap/middleware.js +35 -0
  12. package/core/bootstrap/router-loader.js +53 -0
  13. package/core/errors.js +224 -0
  14. package/core/loader.js +89 -0
  15. package/core/registry.js +17 -0
  16. package/doc/Cache.md +279 -106
  17. package/doc/Common.md +590 -134
  18. package/doc/Controller.md +166 -95
  19. package/doc/Help.md +299 -698
  20. package/doc/QuickStart.md +116 -0
  21. package/doc/Repository.md +560 -0
  22. package/doc/Service.md +201 -527
  23. package/index.js +75 -37
  24. package/middleware/body.js +17 -0
  25. package/middleware/cookie.js +7 -15
  26. package/middleware/cors.js +9 -27
  27. package/middleware/favicon.js +7 -17
  28. package/middleware/header.js +15 -16
  29. package/middleware/index.js +11 -11
  30. package/middleware/log.js +26 -56
  31. package/middleware/static.js +15 -28
  32. package/middleware/template.js +75 -115
  33. package/middleware/validate.js +79 -0
  34. package/middleware/waf.js +174 -197
  35. package/package.json +11 -3
  36. package/response/code.js +73 -0
  37. package/response/index.js +9 -6
  38. package/response/response.js +82 -236
  39. package/security/checker.js +26 -74
  40. package/security/index.js +4 -9
  41. package/security/jwt.js +69 -142
  42. package/security/keywords.js +32 -136
  43. package/security/rate-limit.js +38 -80
  44. package/security/sign.js +83 -176
  45. package/security/xss-filter.js +21 -53
  46. package/storage/cache.js +57 -196
  47. package/storage/index.js +3 -6
  48. package/storage/redis.js +123 -181
  49. package/storage/store.js +163 -188
  50. package/utils/data-parse.js +42 -186
  51. package/utils/file.js +73 -244
  52. package/utils/filter.js +22 -25
  53. package/utils/html.js +49 -33
  54. package/utils/index.js +21 -7
  55. package/utils/ip.js +31 -71
  56. package/utils/logger.js +117 -0
  57. package/utils/pages.js +55 -0
  58. package/utils/paths.js +18 -0
  59. package/utils/request.js +94 -136
  60. package/utils/signal.js +87 -0
  61. package/utils/time.js +33 -75
  62. package/utils/tree.js +112 -104
  63. package/App.js +0 -533
  64. package/base/Aop.js +0 -195
  65. package/base/Container.js +0 -161
  66. package/base/Controller.js +0 -65
  67. package/base/Database.js +0 -133
  68. package/base/Event.js +0 -61
  69. package/base/Repository.js +0 -644
  70. package/common/api.js +0 -35
  71. package/common/code.js +0 -52
  72. package/common/email.js +0 -191
  73. package/common/index.js +0 -5
  74. package/common/pages.js +0 -120
  75. package/common/utils.js +0 -73
  76. package/config/code.js +0 -166
  77. package/config/paths.js +0 -60
  78. package/doc/Aop.md +0 -269
  79. package/doc/Email.md +0 -114
  80. package/doc/Event.md +0 -232
  81. package/global/env.js +0 -11
  82. package/global/import.js +0 -39
  83. package/global/index.js +0 -8
  84. package/helper/index.js +0 -79
  85. package/loader/index.js +0 -6
  86. package/loader/loader.js +0 -138
  87. package/middleware/compress.js +0 -185
  88. package/middleware/setBody.js +0 -32
  89. package/realtime/index.js +0 -7
  90. package/realtime/sse.js +0 -424
  91. package/realtime/websocket.js +0 -540
  92. package/schedule/index.js +0 -6
  93. package/schedule/schedule.js +0 -491
package/storage/redis.js CHANGED
@@ -1,258 +1,200 @@
1
- /**
2
- * 纯 Redis 后端(ioredis 封装)
3
- *
4
- * ============================================================
5
- * 使用方法
6
- * ============================================================
7
- *
8
- * 本模块为纯 Redis 后端,仅负责与 Redis 通信,不包含内存降级逻辑。
9
- * 适合明确知道自己需要 Redis 的场景独立使用。
10
- *
11
- * 1. 独立使用
12
- * import RedisBackend from 'chanjs/helper/redis.js';
13
- * const redis = new RedisBackend({ host: '127.0.0.1', port: 6379 });
14
- * await redis.set('key', 'value', 60000);
15
- * const val = await redis.get('key');
16
- *
17
- * 2. 需要自动适配(Redis + 内存降级)
18
- * 请使用 helper/store.js(适配层)。
19
- *
20
- * 3. 只需要内存缓存(同步 API)
21
- * 请使用 helper/cache.js。
22
- *
23
- * 4. API(全部返回 Promise)
24
- * - get(key) 读取
25
- * - set(key, value, ttlMs=60000) 写入(毫秒级 TTL)
26
- * - del(key) 删除
27
- * - incr(key) 自增(新建 key 自动附加默认 TTL)
28
- * - incrAndExpire(key, ttlMs) 自增 + 首次设置过期
29
- * - exists(key) 存在性检查
30
- * - expire(key, ttlMs) 刷新过期时间
31
- * - close() 关闭连接
32
- *
33
- * 5. 常量
34
- * - DEFAULT_INCR_TTL = 60_000ms incr 新建 key 的默认 TTL
35
- *
36
- * ============================================================
37
- *
38
- * 设计要点:
39
- * 1. 仅负责 Redis 通信,不处理降级(降级逻辑在 store.js 适配层)
40
- * 2. 客户端懒加载,第一次调用时建立连接
41
- * 3. incr/incrAndExpire 用 SET NX PX 保证原子性(兼容 Redis 2.6.12+)
42
- * 4. 连接事件监听,便于排查网络问题
43
- */
1
+ import logger from "../utils/logger.js";
44
2
 
45
- /**
46
- * incr 默认 TTL(毫秒)
47
- * 用于 incr 新建 key 时自动附加过期,避免永久占用内存
48
- */
49
- const DEFAULT_INCR_TTL = 60 * 1000;
3
+ // Redis固定常量统一管理,冻结防止修改
4
+ const REDIS_CONST = Object.freeze({
5
+ DEFAULT_INCR_TTL: 60 * 1000, // incr新建key默认过期时间 60s
6
+ DEFAULT_CONNECT_TIMEOUT: 5000, // 连接超时5秒
7
+ DEFAULT_CMD_TIMEOUT: 1000, // 单条命令执行超时1秒
8
+ MAX_RETRY_TIMES: 3, // 重连最大尝试次数
9
+ });
10
+ export const DEFAULT_INCR_TTL = REDIS_CONST.DEFAULT_INCR_TTL;
50
11
 
51
12
  /**
52
- * Redis 后端
53
- * @class RedisBackend
13
+ * Redis纯存储后端封装(基于ioredis)
14
+ * 1. 懒加载连接:首次调用才创建客户端,启动不阻塞进程
15
+ * 2. incr:先INCR,返回1(新建/过期重建)则补PEXPIRE,递增不刷新TTL,无竞态
16
+ * 3. 自动序列化/反序列化JSON,普通字符串直接透传
17
+ * 4. 关闭离线队列、单次请求仅重试1次,故障快速抛出供上层降级
54
18
  */
55
19
  class RedisBackend {
56
20
  /**
57
- * @param {Object} config - Redis 连接配置
58
- * @param {string} [config.host='127.0.0.1'] - Redis 主机
59
- * @param {number} [config.port=6379] - Redis 端口
60
- * @param {string} [config.password] - Redis 密码
61
- * @param {number} [config.db=0] - Redis 数据库
62
- * @param {number} [config.connectTimeout=5000] - 连接超时(毫秒)
63
- * @param {number} [config.commandTimeout=1000] - 命令超时(毫秒)
21
+ * @param {Object} cfg Redis连接配置 host/port/password/db/超时/重连策略
64
22
  */
65
- constructor(config = {}) {
66
- this.config = config;
67
- this.client = null;
68
- this._initPromise = null;
23
+ constructor(cfg = {}) {
24
+ this.cfg = cfg; // 连接配置缓存
25
+ this.client = null; // ioredis实例
26
+ this.initTask = null; // 连接初始化Promise,防止并发重复建连
69
27
  }
70
28
 
71
29
  /**
72
- * 懒加载 Redis 客户端
73
- * @private
74
- * @returns {Promise<Object>} ioredis 客户端实例
75
- * @throws {Error} 连接失败时抛出
30
+ * 内部工具:懒加载获取Redis客户端,连接失败抛出异常
31
+ * 外部store适配层可调用,去掉#私有标识解决跨文件访问报错
76
32
  */
77
- async _ensureClient() {
33
+ async _getClient() {
34
+ // 已有客户端直接返回
78
35
  if (this.client) return this.client;
79
- if (this._initPromise) return this._initPromise;
80
-
81
- this._initPromise = (async () => {
82
- const { default: Redis } = await import('ioredis');
83
- this.client = new Redis({
84
- host: this.config.host || '127.0.0.1',
85
- port: this.config.port || 6379,
86
- password: this.config.password || undefined,
87
- db: this.config.db || 0,
88
- connectTimeout: this.config.connectTimeout || 5000,
89
- commandTimeout: this.config.commandTimeout || 1000,
90
- retryStrategy: this.config.retryStrategy || ((times) => {
91
- if (times > 3) return null;
36
+ // 正在初始化,等待初始化Promise
37
+ if (this.initTask) return this.initTask;
38
+
39
+ // 开始异步初始化连接
40
+ this.initTask = (async () => {
41
+ // 动态导入ioredis,避免未安装Redis依赖时报启动错误
42
+ const { default: Redis } = await import("ioredis");
43
+ const client = new Redis({
44
+ host: this.cfg.host ?? "127.0.0.1",
45
+ port: this.cfg.port ?? 6379,
46
+ password: this.cfg.password || undefined,
47
+ db: this.cfg.db ?? 0,
48
+ connectTimeout: this.cfg.connectTimeout ?? REDIS_CONST.DEFAULT_CONNECT_TIMEOUT,
49
+ commandTimeout: this.cfg.commandTimeout ?? REDIS_CONST.DEFAULT_CMD_TIMEOUT,
50
+ lazyConnect: true, // 实例化不立即连接,手动connect
51
+ maxRetriesPerRequest: 1, // 单条命令失败仅重试1次
52
+ enableOfflineQueue: false, // 断连后不堆积请求,直接报错
53
+ retryStrategy: this.cfg.retryStrategy ?? ((times) => {
54
+ // 超过最大重试次数返回null,停止重连
55
+ if (times > REDIS_CONST.MAX_RETRY_TIMES) return null;
56
+ // 重试间隔阶梯增长,最大1秒
92
57
  return Math.min(times * 200, 1000);
93
58
  }),
94
- lazyConnect: true,
95
- maxRetriesPerRequest: 1,
96
- enableOfflineQueue: false,
97
- });
98
-
99
- this.client.on('error', (err) => {
100
- console.error('[Redis] 连接错误:', err.message);
101
59
  });
102
60
 
103
- this.client.on('connect', () => {
104
- console.log(`[Redis] 已连接 ${this.config.host}:${this.config.port}`);
105
- });
106
-
107
- this.client.on('reconnecting', () => {
108
- console.log('[Redis] 正在重连...');
109
- });
61
+ // 连接事件监听,打印日志
62
+ client
63
+ .on("error", err => logger.error("[Redis] 连接异常", err.message))
64
+ .on("connect", () => logger.info(`[Redis] 已连接 ${this.cfg.host}:${this.cfg.port}`))
65
+ .on("reconnecting", () => logger.info("[Redis] 正在重连"));
110
66
 
111
- await this.client.connect();
112
- return this.client;
67
+ // 手动发起连接
68
+ await client.connect();
69
+ this.client = client;
70
+ return client;
113
71
  })();
114
72
 
115
73
  try {
116
- return await this._initPromise;
74
+ return await this.initTask;
117
75
  } catch (err) {
76
+ // 连接失败清空缓存,下次调用重新初始化
118
77
  this.client = null;
119
- this._initPromise = null;
120
- throw new Error(`Redis 连接失败: ${err.message}`);
78
+ this.initTask = null;
79
+ throw new Error(`Redis连接失败: ${err.message}`);
121
80
  }
122
81
  }
123
82
 
124
83
  /**
125
- * 读取
126
- * @param {string} key - 键
127
- * @returns {Promise<*|null>} 值(自动 JSON 解析),不存在返回 null
84
+ * 读取缓存
85
+ * @param {string} key 缓存键名
86
+ * @returns {any|null} 解析后数据,不存在返回null
128
87
  */
129
88
  async get(key) {
130
- const client = await this._ensureClient();
131
- const value = await client.get(key);
132
- if (value === null) return null;
89
+ const cli = await this._getClient();
90
+ const raw = await cli.get(key);
91
+ if (raw === null) return null;
133
92
  try {
134
- return JSON.parse(value);
93
+ // JSON字符串自动解析
94
+ return JSON.parse(raw);
135
95
  } catch {
136
- return value;
96
+ // 普通字符串直接返回
97
+ return raw;
137
98
  }
138
99
  }
139
100
 
140
101
  /**
141
- * 写入
142
- * @param {string} key - 键
143
- * @param {*} value - 值(自动 JSON 序列化)
144
- * @param {number} [ttlMs=60000] - TTL 毫秒,<=0 表示永久
145
- * @returns {Promise<boolean>} 始终返回 true
102
+ * 写入缓存
103
+ * @param {string} key 键名
104
+ * @param {any} val 存储值
105
+ * @param {number} ttlMs 过期毫秒,<=0永久存储
106
+ * @returns {boolean} 固定返回true
146
107
  */
147
- async set(key, value, ttlMs = 60000) {
148
- const client = await this._ensureClient();
149
- const serialized = typeof value === 'string' ? value : JSON.stringify(value);
108
+ async set(key, val, ttlMs = 60000) {
109
+ const cli = await this._getClient();
110
+ const data = typeof val === "string" ? val : JSON.stringify(val);
150
111
  if (ttlMs > 0) {
151
- // PX 毫秒级过期
152
- await client.set(key, serialized, 'PX', ttlMs);
112
+ await cli.set(key, data, "PX", ttlMs);
153
113
  } else {
154
- await client.set(key, serialized);
114
+ await cli.set(key, data);
155
115
  }
156
116
  return true;
157
117
  }
158
118
 
159
119
  /**
160
- * 删除
161
- * @param {string} key - 键
162
- * @returns {Promise<boolean>} 是否删除了至少 1 条
120
+ * 删除key
121
+ * @param {string} key 键名
122
+ * @returns {boolean} 是否删除成功
163
123
  */
164
124
  async del(key) {
165
- const client = await this._ensureClient();
166
- const result = await client.del(key);
167
- return result > 0;
125
+ const cli = await this._getClient();
126
+ const cnt = await cli.del(key);
127
+ return cnt > 0;
168
128
  }
169
129
 
170
130
  /**
171
- * 自增,新建 key 自动附加默认 TTL
172
- *
173
- * 实现方式:SET key 1 PX ttl NX + 分支判断
174
- * - NX 选项:只有 key 不存在时才设置(原子操作)
175
- * - 设置成功返回 "OK" → 新建 key,返回 1
176
- * - 设置失败返回 null → key 已存在,走 INCR 递增(不刷新 TTL)
177
- *
178
- * 兼容性:SET ... NX PX 是 Redis 2.6.12+ 标准用法,覆盖所有生产环境
179
- *
180
- * @param {string} key - 键
181
- * @returns {Promise<number>} 自增后的值
131
+ * 自增计数,新建key使用全局默认TTL
132
+ * 纯ioredis方案:先INCR,返回1(key新建/过期重建)则补PEXPIRE,递增不刷新TTL
133
+ * 解决原 SET NX + INCR 之间 key 过期导致的无 TTL 内存泄漏竞态
134
+ * @param {string} key 计数key
135
+ * @returns {number} 当前计数值
182
136
  */
183
137
  async incr(key) {
184
- const client = await this._ensureClient();
185
- // 尝试新建 key 并设置默认 TTL(原子操作)
186
- const setResult = await client.set(key, 1, 'PX', DEFAULT_INCR_TTL, 'NX');
187
- if (setResult === 'OK') {
188
- return 1; // 新建成功
189
- }
190
- // key 已存在,INCR 递增(不刷新 TTL)
191
- return client.incr(key);
138
+ const cli = await this._getClient();
139
+ const count = await cli.incr(key);
140
+ // count===1 说明 key 是新建的(首次或过期重建),补设 TTL;递增(count>1)不刷新 TTL
141
+ if (count === 1) await cli.pexpire(key, REDIS_CONST.DEFAULT_INCR_TTL);
142
+ return count;
192
143
  }
193
144
 
194
145
  /**
195
- * 自增并首次设置过期时间
196
- * 用于 Rate Limit 场景:第一次访问设置 TTL,后续访问只递增不续期
197
- *
198
- * @param {string} key - 键
199
- * @param {number} ttlMs - TTL 毫秒
200
- * @returns {Promise<number>} 自增后的值
146
+ * 限流专用自增,自定义新建key过期时间
147
+ * @param {string} key 计数key
148
+ * @param {number} ttlMs 窗口过期时间
149
+ * @returns {number} 当前计数值
201
150
  */
202
151
  async incrAndExpire(key, ttlMs) {
203
- const client = await this._ensureClient();
204
- // 尝试新建 key 并设置指定 TTL(原子操作)
205
- const setResult = await client.set(key, 1, 'PX', ttlMs, 'NX');
206
- if (setResult === 'OK') {
207
- return 1; // 新建成功,TTL 已设置
208
- }
209
- // key 已存在,INCR 递增(不刷新 TTL)
210
- return client.incr(key);
152
+ const cli = await this._getClient();
153
+ const count = await cli.incr(key);
154
+ if (count === 1) await cli.pexpire(key, ttlMs);
155
+ return count;
211
156
  }
212
157
 
213
158
  /**
214
- * 存在性检查
215
- * @param {string} key - 键
216
- * @returns {Promise<boolean>}
159
+ * 判断key是否存在
160
+ * @param {string} key 键名
161
+ * @returns {boolean} true存在 / false不存在
217
162
  */
218
163
  async exists(key) {
219
- const client = await this._ensureClient();
220
- const result = await client.exists(key);
221
- return result === 1;
164
+ const cli = await this._getClient();
165
+ const res = await cli.exists(key);
166
+ return res === 1;
222
167
  }
223
168
 
224
169
  /**
225
- * 刷新过期时间
226
- * @param {string} key - 键
227
- * @param {number} ttlMs - TTL 毫秒
228
- * @returns {Promise<boolean>} 是否设置成功
170
+ * 主动刷新key过期时间
171
+ * @param {string} key 键名
172
+ * @param {number} ttlMs 新过期毫秒
173
+ * @returns {number} redis原生返回值
229
174
  */
230
175
  async expire(key, ttlMs) {
231
- const client = await this._ensureClient();
232
- return client.pexpire(key, ttlMs);
176
+ const cli = await this._getClient();
177
+ return cli.pexpire(key, ttlMs);
233
178
  }
234
179
 
235
180
  /**
236
- * 关闭连接
237
- * @returns {Promise<void>}
181
+ * 关闭Redis连接,清空实例缓存
238
182
  */
239
183
  async close() {
240
- if (this.client) {
241
- await this.client.quit();
242
- this.client = null;
243
- this._initPromise = null;
244
- }
184
+ if (!this.client) return;
185
+ await this.client.quit();
186
+ this.client = null;
187
+ this.initTask = null;
245
188
  }
246
189
 
247
190
  /**
248
- * 获取客户端状态(诊断用)
249
- * @returns {string} 'idle' | 'ready' | 'connecting' | 'closed'
191
+ * 获取客户端连接状态,用于上层诊断监控
192
+ * @returns {'idle'|'ready'|'connecting'|'closed'}
250
193
  */
251
194
  getStatus() {
252
- if (!this.client) return 'idle';
253
- return this.client.status || 'unknown';
195
+ if (!this.client) return "idle";
196
+ return this.client.status ?? "unknown";
254
197
  }
255
198
  }
256
199
 
257
- export { DEFAULT_INCR_TTL };
258
- export default RedisBackend;
200
+ export default RedisBackend;