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.
- package/USAGE.md +533 -0
- package/config/index.js +37 -6
- package/core/App.js +166 -0
- package/core/Container.js +77 -0
- package/core/Controller.js +29 -0
- package/core/Database.js +93 -0
- package/core/Repository.js +327 -0
- package/core/Service.js +11 -0
- package/core/bootstrap/error-handler.js +104 -0
- package/core/bootstrap/hook-runner.js +64 -0
- package/core/bootstrap/middleware.js +35 -0
- package/core/bootstrap/router-loader.js +53 -0
- package/core/errors.js +224 -0
- package/core/loader.js +89 -0
- package/core/registry.js +17 -0
- package/doc/Cache.md +279 -106
- package/doc/Common.md +590 -134
- package/doc/Controller.md +166 -95
- package/doc/Help.md +299 -698
- package/doc/QuickStart.md +116 -0
- package/doc/Repository.md +560 -0
- package/doc/Service.md +201 -527
- package/index.js +75 -37
- package/middleware/body.js +17 -0
- package/middleware/cookie.js +7 -15
- package/middleware/cors.js +9 -27
- package/middleware/favicon.js +7 -17
- package/middleware/header.js +15 -16
- package/middleware/index.js +11 -11
- package/middleware/log.js +26 -56
- package/middleware/static.js +15 -28
- package/middleware/template.js +75 -115
- package/middleware/validate.js +79 -0
- package/middleware/waf.js +174 -197
- package/package.json +11 -3
- package/response/code.js +73 -0
- package/response/index.js +9 -6
- package/response/response.js +82 -236
- package/security/checker.js +26 -74
- package/security/index.js +4 -9
- package/security/jwt.js +69 -142
- package/security/keywords.js +32 -136
- package/security/rate-limit.js +38 -80
- package/security/sign.js +83 -176
- package/security/xss-filter.js +21 -53
- package/storage/cache.js +57 -196
- package/storage/index.js +3 -6
- package/storage/redis.js +123 -181
- package/storage/store.js +163 -188
- package/utils/data-parse.js +42 -186
- package/utils/file.js +73 -244
- package/utils/filter.js +22 -25
- package/utils/html.js +49 -33
- package/utils/index.js +21 -7
- package/utils/ip.js +31 -71
- package/utils/logger.js +117 -0
- package/utils/pages.js +55 -0
- package/utils/paths.js +18 -0
- package/utils/request.js +94 -136
- package/utils/signal.js +87 -0
- package/utils/time.js +33 -75
- package/utils/tree.js +112 -104
- package/App.js +0 -533
- package/base/Aop.js +0 -195
- package/base/Container.js +0 -161
- package/base/Controller.js +0 -65
- package/base/Database.js +0 -133
- package/base/Event.js +0 -61
- package/base/Repository.js +0 -644
- package/common/api.js +0 -35
- package/common/code.js +0 -52
- package/common/email.js +0 -191
- package/common/index.js +0 -5
- package/common/pages.js +0 -120
- package/common/utils.js +0 -73
- package/config/code.js +0 -166
- package/config/paths.js +0 -60
- package/doc/Aop.md +0 -269
- package/doc/Email.md +0 -114
- package/doc/Event.md +0 -232
- package/global/env.js +0 -11
- package/global/import.js +0 -39
- package/global/index.js +0 -8
- package/helper/index.js +0 -79
- package/loader/index.js +0 -6
- package/loader/loader.js +0 -138
- package/middleware/compress.js +0 -185
- package/middleware/setBody.js +0 -32
- package/realtime/index.js +0 -7
- package/realtime/sse.js +0 -424
- package/realtime/websocket.js +0 -540
- package/schedule/index.js +0 -6
- 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
|
-
|
|
47
|
-
*
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
*
|
|
53
|
-
*
|
|
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}
|
|
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(
|
|
66
|
-
this.
|
|
67
|
-
this.client = null;
|
|
68
|
-
this.
|
|
23
|
+
constructor(cfg = {}) {
|
|
24
|
+
this.cfg = cfg; // 连接配置缓存
|
|
25
|
+
this.client = null; // ioredis实例
|
|
26
|
+
this.initTask = null; // 连接初始化Promise,防止并发重复建连
|
|
69
27
|
}
|
|
70
28
|
|
|
71
29
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* @returns {Promise<Object>} ioredis 客户端实例
|
|
75
|
-
* @throws {Error} 连接失败时抛出
|
|
30
|
+
* 内部工具:懒加载获取Redis客户端,连接失败抛出异常
|
|
31
|
+
* 外部store适配层可调用,去掉#私有标识解决跨文件访问报错
|
|
76
32
|
*/
|
|
77
|
-
async
|
|
33
|
+
async _getClient() {
|
|
34
|
+
// 已有客户端直接返回
|
|
78
35
|
if (this.client) return this.client;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
67
|
+
// 手动发起连接
|
|
68
|
+
await client.connect();
|
|
69
|
+
this.client = client;
|
|
70
|
+
return client;
|
|
113
71
|
})();
|
|
114
72
|
|
|
115
73
|
try {
|
|
116
|
-
return await this.
|
|
74
|
+
return await this.initTask;
|
|
117
75
|
} catch (err) {
|
|
76
|
+
// 连接失败清空缓存,下次调用重新初始化
|
|
118
77
|
this.client = null;
|
|
119
|
-
this.
|
|
120
|
-
throw new Error(`Redis
|
|
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 {
|
|
84
|
+
* 读取缓存
|
|
85
|
+
* @param {string} key 缓存键名
|
|
86
|
+
* @returns {any|null} 解析后数据,不存在返回null
|
|
128
87
|
*/
|
|
129
88
|
async get(key) {
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
89
|
+
const cli = await this._getClient();
|
|
90
|
+
const raw = await cli.get(key);
|
|
91
|
+
if (raw === null) return null;
|
|
133
92
|
try {
|
|
134
|
-
|
|
93
|
+
// JSON字符串自动解析
|
|
94
|
+
return JSON.parse(raw);
|
|
135
95
|
} catch {
|
|
136
|
-
|
|
96
|
+
// 普通字符串直接返回
|
|
97
|
+
return raw;
|
|
137
98
|
}
|
|
138
99
|
}
|
|
139
100
|
|
|
140
101
|
/**
|
|
141
|
-
*
|
|
142
|
-
* @param {string} key
|
|
143
|
-
* @param {
|
|
144
|
-
* @param {number}
|
|
145
|
-
* @returns {
|
|
102
|
+
* 写入缓存
|
|
103
|
+
* @param {string} key 键名
|
|
104
|
+
* @param {any} val 存储值
|
|
105
|
+
* @param {number} ttlMs 过期毫秒,<=0永久存储
|
|
106
|
+
* @returns {boolean} 固定返回true
|
|
146
107
|
*/
|
|
147
|
-
async set(key,
|
|
148
|
-
const
|
|
149
|
-
const
|
|
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
|
-
|
|
152
|
-
await client.set(key, serialized, 'PX', ttlMs);
|
|
112
|
+
await cli.set(key, data, "PX", ttlMs);
|
|
153
113
|
} else {
|
|
154
|
-
await
|
|
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 {
|
|
120
|
+
* 删除key
|
|
121
|
+
* @param {string} key 键名
|
|
122
|
+
* @returns {boolean} 是否删除成功
|
|
163
123
|
*/
|
|
164
124
|
async del(key) {
|
|
165
|
-
const
|
|
166
|
-
const
|
|
167
|
-
return
|
|
125
|
+
const cli = await this._getClient();
|
|
126
|
+
const cnt = await cli.del(key);
|
|
127
|
+
return cnt > 0;
|
|
168
128
|
}
|
|
169
129
|
|
|
170
130
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
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
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
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
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* @
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 {
|
|
159
|
+
* 判断key是否存在
|
|
160
|
+
* @param {string} key 键名
|
|
161
|
+
* @returns {boolean} true存在 / false不存在
|
|
217
162
|
*/
|
|
218
163
|
async exists(key) {
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
return
|
|
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
|
|
228
|
-
* @returns {
|
|
170
|
+
* 主动刷新key过期时间
|
|
171
|
+
* @param {string} key 键名
|
|
172
|
+
* @param {number} ttlMs 新过期毫秒
|
|
173
|
+
* @returns {number} redis原生返回值
|
|
229
174
|
*/
|
|
230
175
|
async expire(key, ttlMs) {
|
|
231
|
-
const
|
|
232
|
-
return
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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 {
|
|
191
|
+
* 获取客户端连接状态,用于上层诊断监控
|
|
192
|
+
* @returns {'idle'|'ready'|'connecting'|'closed'}
|
|
250
193
|
*/
|
|
251
194
|
getStatus() {
|
|
252
|
-
if (!this.client) return
|
|
253
|
-
return this.client.status
|
|
195
|
+
if (!this.client) return "idle";
|
|
196
|
+
return this.client.status ?? "unknown";
|
|
254
197
|
}
|
|
255
198
|
}
|
|
256
199
|
|
|
257
|
-
export
|
|
258
|
-
export default RedisBackend;
|
|
200
|
+
export default RedisBackend;
|