chanjs 2.7.2 → 2.7.4

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 (67) hide show
  1. package/App.js +232 -16
  2. package/base/Aop.js +20 -3
  3. package/base/Container.js +80 -3
  4. package/base/Controller.js +38 -9
  5. package/base/Database.js +50 -0
  6. package/base/Event.js +12 -0
  7. package/base/{Service.js → Repository.js} +644 -539
  8. package/common/api.js +18 -8
  9. package/common/code.js +25 -15
  10. package/common/email.js +98 -17
  11. package/common/index.js +1 -1
  12. package/config/code.js +138 -82
  13. package/global/index.js +1 -1
  14. package/helper/index.js +43 -41
  15. package/index.js +19 -6
  16. package/loader/index.js +6 -0
  17. package/{helper → loader}/loader.js +41 -27
  18. package/middleware/compress.js +185 -0
  19. package/middleware/cors.js +36 -24
  20. package/middleware/header.js +5 -10
  21. package/middleware/index.js +1 -0
  22. package/middleware/log.js +27 -3
  23. package/middleware/setBody.js +9 -1
  24. package/middleware/static.js +2 -1
  25. package/middleware/template.js +139 -4
  26. package/middleware/waf.js +136 -76
  27. package/package.json +4 -4
  28. package/realtime/index.js +7 -0
  29. package/realtime/sse.js +424 -0
  30. package/realtime/websocket.js +540 -0
  31. package/response/index.js +12 -0
  32. package/response/response.js +258 -0
  33. package/schedule/index.js +6 -0
  34. package/schedule/schedule.js +491 -0
  35. package/{helper → security}/checker.js +23 -8
  36. package/security/index.js +14 -0
  37. package/{helper → security}/jwt.js +175 -107
  38. package/security/keywords.js +179 -0
  39. package/security/rate-limit.js +105 -0
  40. package/security/sign.js +210 -0
  41. package/security/xss-filter.js +63 -0
  42. package/storage/cache.js +258 -0
  43. package/storage/index.js +9 -0
  44. package/storage/redis.js +258 -0
  45. package/storage/store.js +266 -0
  46. package/{helper → utils}/file.js +106 -15
  47. package/{helper → utils}/filter.js +2 -1
  48. package/{helper → utils}/html.js +19 -1
  49. package/utils/index.js +34 -0
  50. package/{helper → utils}/ip.js +25 -16
  51. package/utils/request.js +172 -0
  52. package/{helper → utils}/time.js +1 -1
  53. package/utils/tree.js +121 -0
  54. package/common/category.js +0 -22
  55. package/common/sms.js +0 -104
  56. package/extend/art-template.js +0 -129
  57. package/extend/index.js +0 -6
  58. package/global/global.js +0 -63
  59. package/helper/cache.js +0 -187
  60. package/helper/keywords.js +0 -132
  61. package/helper/rate-limit.js +0 -116
  62. package/helper/request.js +0 -47
  63. package/helper/response.js +0 -180
  64. package/helper/sign.js +0 -96
  65. package/helper/tree.js +0 -77
  66. package/helper/xss-filter.js +0 -42
  67. /package/{helper → utils}/data-parse.js +0 -0
@@ -0,0 +1,424 @@
1
+ /**
2
+ * SSE (Server-Sent Events) 管理器
3
+ *
4
+ * ============================================================
5
+ * 特性
6
+ * ============================================================
7
+ * 1. 零依赖:基于 Node 原生 http/express 的 res 对象实现
8
+ * 2. 房间(Room)机制:可分组广播,也可对单个连接推送
9
+ * 3. 心跳保活:默认 30s 发送 :heartbeat 注释,防止代理超时断开
10
+ * 4. 鉴权钩子:可选的 authenticate 回调,按业务校验 req
11
+ * 5. 优雅停机:shutdown() 关闭所有连接,清理定时器
12
+ *
13
+ * ============================================================
14
+ * 使用方法
15
+ * ============================================================
16
+ *
17
+ * // 1. 注册 SSE 路由(业务侧只需一行)
18
+ * import { sse } from 'chanjs';
19
+ * app.get('/sse', sse.middleware(), (req, res) => {
20
+ * sse.join(res, req.query.room || 'default');
21
+ * });
22
+ *
23
+ * // 2. 单推
24
+ * sse.send(clientId, { type: 'message', data: 'hello' });
25
+ *
26
+ * // 3. 广播到房间
27
+ * sse.broadcast('room-1', { type: 'notice', data: 'hello all' });
28
+ *
29
+ * // 4. 全局广播
30
+ * sse.broadcastAll({ type: 'ping', data: Date.now() });
31
+ *
32
+ * // 5. 带鉴权
33
+ * app.get('/sse-auth', sse.middleware({
34
+ * authenticate: (req) => req.headers.token === 'xxx',
35
+ * }), (req, res) => sse.join(res, 'user-' + req.user.id));
36
+ *
37
+ * // 6. 状态查询
38
+ * sse.getStatus(); // { connections, rooms, uptime }
39
+ *
40
+ * ============================================================
41
+ * 协议说明
42
+ * ============================================================
43
+ * SSE 响应头:
44
+ * Content-Type: text/event-stream
45
+ * Cache-Control: no-cache
46
+ * Connection: keep-alive
47
+ * 消息格式:
48
+ * data: <json>\n\n
49
+ * event: <event-type>\ndata: <json>\n\n
50
+ * 注释(心跳):
51
+ * : heartbeat\n\n
52
+ */
53
+
54
+ /**
55
+ * SSE 默认心跳间隔(毫秒)
56
+ * 30s 是代理超时的安全值(Nginx proxy_read_timeout 默认 60s)
57
+ */
58
+ const DEFAULT_HEARTBEAT_INTERVAL = 30000;
59
+
60
+ /**
61
+ * SSE 管理器
62
+ * @class SSEManager
63
+ */
64
+ class SSEManager {
65
+ constructor() {
66
+ /** @type {Map<string, {res: Object, rooms: Set<string>, joinedAt: number}>} 连接表 id -> client */
67
+ this._clients = new Map();
68
+ /** @type {Map<string, Set<string>>} 房间表 room -> Set<clientId> */
69
+ this._rooms = new Map();
70
+ /** 心跳定时器句柄 */
71
+ this._heartbeatTimer = null;
72
+ /** 心跳间隔 */
73
+ this._heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL;
74
+ /** 自增连接 id */
75
+ this._idSeq = 0;
76
+ /** 启动时间 */
77
+ this._startedAt = Date.now();
78
+ /** 是否已 shutdown */
79
+ this._shuttingDown = false;
80
+ /** 最大并发连接数上限,超出直接返回 503,防止连接洪泛耗尽内存 */
81
+ this._maxConnections = 10000;
82
+ }
83
+
84
+ /**
85
+ * Express 中间件:建立 SSE 连接
86
+ * @param {Object} [options]
87
+ * @param {Function} [options.authenticate] - 鉴权钩子 (req) => boolean | Promise<boolean>
88
+ * @param {number} [options.heartbeatInterval] - 心跳间隔,默认 30000
89
+ * @returns {Function} Express middleware
90
+ */
91
+ middleware(options = {}) {
92
+ const { authenticate, heartbeatInterval } = options;
93
+
94
+ return async (req, res, next) => {
95
+ if (this._shuttingDown) {
96
+ res.status(503).json({ success: false, code: 5002, msg: '服务不可用' });
97
+ return;
98
+ }
99
+
100
+ // 连接数上限校验,防止连接洪泛导致内存耗尽
101
+ if (this._clients.size >= this._maxConnections) {
102
+ res.status(503).json({ success: false, code: 5002, msg: 'SSE 连接数已达上限,请稍后再试' });
103
+ return;
104
+ }
105
+
106
+ // 鉴权钩子
107
+ if (typeof authenticate === 'function') {
108
+ let ok;
109
+ try {
110
+ ok = await authenticate(req);
111
+ } catch (e) {
112
+ ok = false;
113
+ }
114
+ if (!ok) {
115
+ res.status(401).json({ success: false, code: 1001, msg: '认证失败' });
116
+ return;
117
+ }
118
+ }
119
+
120
+ // 设置 SSE 响应头
121
+ res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
122
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
123
+ res.setHeader('Connection', 'keep-alive');
124
+ // 禁用代理缓冲(Nginx X-Accel-Buffering)
125
+ res.setHeader('X-Accel-Buffering', 'no');
126
+ res.flushHeaders?.();
127
+
128
+ // 生成连接 id 并注册
129
+ const clientId = `sse_${++this._idSeq}_${Date.now()}`;
130
+ this._clients.set(clientId, {
131
+ res,
132
+ rooms: new Set(),
133
+ joinedAt: Date.now(),
134
+ });
135
+
136
+ // 启动心跳(首次连接时启动,全局共享)
137
+ this._ensureHeartbeat(heartbeatInterval);
138
+
139
+ // 注入到 req,便于业务侧拿到 clientId
140
+ req.sseClientId = clientId;
141
+
142
+ // 连接关闭时清理(res 是 Express 响应对象,标准 Node EventEmitter)
143
+ const cleanup = () => {
144
+ this._removeClient(clientId);
145
+ };
146
+ req.on?.('close', cleanup);
147
+ req.on?.('aborted', cleanup);
148
+ res.on?.('close', cleanup);
149
+ res.on?.('error', cleanup);
150
+
151
+ next?.();
152
+ };
153
+ }
154
+
155
+ /**
156
+ * 将连接加入房间
157
+ * @param {Object} res - Express res(建立 SSE 时的那个 res)
158
+ * @param {string} room - 房间名
159
+ * @returns {string|null} clientId(成功时返回,便于业务侧记录)
160
+ */
161
+ join(res, room) {
162
+ const clientId = this._findClientIdByRes(res);
163
+ if (!clientId) return null;
164
+
165
+ const client = this._clients.get(clientId);
166
+ if (!client) return null;
167
+
168
+ client.rooms.add(room);
169
+ if (!this._rooms.has(room)) this._rooms.set(room, new Set());
170
+ this._rooms.get(room).add(clientId);
171
+ return clientId;
172
+ }
173
+
174
+ /**
175
+ * 将连接离开房间
176
+ * @param {Object|string} target - res 或 clientId
177
+ * @param {string} room - 房间名
178
+ */
179
+ leave(target, room) {
180
+ const clientId = typeof target === 'string' ? target : this._findClientIdByRes(target);
181
+ if (!clientId) return false;
182
+ const client = this._clients.get(clientId);
183
+ if (!client) return false;
184
+
185
+ client.rooms.delete(room);
186
+ const members = this._rooms.get(room);
187
+ if (members) {
188
+ members.delete(clientId);
189
+ if (members.size === 0) this._rooms.delete(room);
190
+ }
191
+ return true;
192
+ }
193
+
194
+ /**
195
+ * 向单个连接发送消息
196
+ * @param {string} clientId - 连接 id
197
+ * @param {*} data - 任意可序列化数据
198
+ * @param {Object} [options]
199
+ * @param {string} [options.event] - SSE event 字段(可选)
200
+ * @returns {boolean} 是否发送成功
201
+ */
202
+ send(clientId, data, options = {}) {
203
+ const client = this._clients.get(clientId);
204
+ if (!client) return false;
205
+ return this._write(client.res, data, options.event);
206
+ }
207
+
208
+ /**
209
+ * 广播到指定房间
210
+ * @param {string} room - 房间名
211
+ * @param {*} data - 任意可序列化数据
212
+ * @param {Object} [options]
213
+ * @param {string} [options.event] - SSE event 字段
214
+ * @returns {number} 实际送达的连接数
215
+ */
216
+ broadcast(room, data, options = {}) {
217
+ const members = this._rooms.get(room);
218
+ if (!members || members.size === 0) return 0;
219
+
220
+ let sent = 0;
221
+ for (const clientId of members) {
222
+ if (this.send(clientId, data, options)) sent++;
223
+ }
224
+ return sent;
225
+ }
226
+
227
+ /**
228
+ * 广播到所有连接
229
+ * @param {*} data - 任意可序列化数据
230
+ * @param {Object} [options]
231
+ * @param {string} [options.event] - SSE event 字段
232
+ * @returns {number} 实际送达的连接数
233
+ */
234
+ broadcastAll(data, options = {}) {
235
+ let sent = 0;
236
+ for (const clientId of this._clients.keys()) {
237
+ if (this.send(clientId, data, options)) sent++;
238
+ }
239
+ return sent;
240
+ }
241
+
242
+ /**
243
+ * 获取当前连接数
244
+ * @returns {number}
245
+ */
246
+ size() {
247
+ return this._clients.size;
248
+ }
249
+
250
+ /**
251
+ * 获取指定房间连接数
252
+ * @param {string} room
253
+ * @returns {number}
254
+ */
255
+ roomSize(room) {
256
+ return this._rooms.get(room)?.size || 0;
257
+ }
258
+
259
+ /**
260
+ * 获取运行状态
261
+ * @returns {Object} { connections, rooms, uptime }
262
+ */
263
+ getStatus() {
264
+ return {
265
+ connections: this._clients.size,
266
+ rooms: this._rooms.size,
267
+ roomList: Array.from(this._rooms.keys()),
268
+ uptime: Date.now() - this._startedAt,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * 优雅停机:关闭所有连接,清理心跳
274
+ */
275
+ shutdown() {
276
+ this._shuttingDown = true;
277
+
278
+ // 停止心跳
279
+ if (this._heartbeatTimer) {
280
+ clearInterval(this._heartbeatTimer);
281
+ this._heartbeatTimer = null;
282
+ }
283
+
284
+ // 主动关闭所有连接
285
+ for (const [, client] of this._clients) {
286
+ try {
287
+ client.res.end();
288
+ } catch (e) {
289
+ // 忽略已关闭的连接
290
+ }
291
+ }
292
+ this._clients.clear();
293
+ this._rooms.clear();
294
+ console.log(`[SSE] 已关闭所有 SSE 连接`);
295
+ }
296
+
297
+ /**
298
+ * 写入 SSE 数据(底层方法)
299
+ * @private
300
+ * @param {Object} res - Express res
301
+ * @param {*} data - 数据
302
+ * @param {string} [event] - 事件类型
303
+ * @returns {boolean} 是否写入成功
304
+ */
305
+ _write(res, data, event) {
306
+ if (res.writableEnded || res.destroyed) return false;
307
+
308
+ const payload = typeof data === 'string' ? data : JSON.stringify(data);
309
+ let chunk = '';
310
+ if (event) chunk += `event: ${event}\n`;
311
+ // 多行 data 拼接:每行前缀 data:
312
+ chunk += payload
313
+ .split('\n')
314
+ .map((line) => `data: ${line}`)
315
+ .join('\n');
316
+ chunk += '\n\n';
317
+
318
+ try {
319
+ res.write(chunk);
320
+ return true;
321
+ } catch (e) {
322
+ return false;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * 启动心跳(懒加载,首次连接时启动)
328
+ * @private
329
+ */
330
+ _ensureHeartbeat(interval) {
331
+ if (this._heartbeatTimer) return;
332
+ if (typeof interval === 'number' && interval > 0) {
333
+ this._heartbeatInterval = interval;
334
+ }
335
+
336
+ this._heartbeatTimer = setInterval(() => {
337
+ if (this._clients.size === 0) return;
338
+ for (const [, client] of this._clients) {
339
+ // SSE 注释格式:: heartbeat\n\n(不会触发前端 message 事件)
340
+ this._writeComment(client.res, 'heartbeat');
341
+ }
342
+ }, this._heartbeatInterval);
343
+
344
+ // 不阻止进程退出
345
+ if (this._heartbeatTimer.unref) this._heartbeatTimer.unref();
346
+ }
347
+
348
+ /**
349
+ * 写入 SSE 注释(心跳用,不触发前端 message 事件)
350
+ * @private
351
+ */
352
+ _writeComment(res, text) {
353
+ if (res.writableEnded || res.destroyed) return false;
354
+ try {
355
+ res.write(`: ${text}\n\n`);
356
+ return true;
357
+ } catch (e) {
358
+ return false;
359
+ }
360
+ }
361
+
362
+ /**
363
+ * 移除客户端(清理房间索引)
364
+ * @private
365
+ */
366
+ _removeClient(clientId) {
367
+ const client = this._clients.get(clientId);
368
+ if (!client) return;
369
+
370
+ // 从所有房间移除
371
+ for (const room of client.rooms) {
372
+ const members = this._rooms.get(room);
373
+ if (members) {
374
+ members.delete(clientId);
375
+ if (members.size === 0) this._rooms.delete(room);
376
+ }
377
+ }
378
+
379
+ this._clients.delete(clientId);
380
+ }
381
+
382
+ /**
383
+ * 根据 res 反查 clientId
384
+ * @private
385
+ */
386
+ _findClientIdByRes(res) {
387
+ for (const [id, client] of this._clients) {
388
+ if (client.res === res) return id;
389
+ }
390
+ return null;
391
+ }
392
+ }
393
+
394
+ // 全局单例(懒加载:首次访问属性时才实例化,避免 import 即实例化浪费内存)
395
+ let _sseInstance = null;
396
+ const getSSEInstance = () => {
397
+ if (!_sseInstance) _sseInstance = new SSEManager();
398
+ return _sseInstance;
399
+ };
400
+
401
+ // 用 Proxy 保持 `import { sse } from 'chanjs'` API 兼容,访问任意属性时才实例化
402
+ const sse = new Proxy(
403
+ {},
404
+ {
405
+ get(_target, prop) {
406
+ const inst = getSSEInstance();
407
+ const val = inst[prop];
408
+ // 方法绑定 this,避免解构调用时丢失上下文
409
+ return typeof val === 'function' ? val.bind(inst) : val;
410
+ },
411
+ set(_target, prop, value) {
412
+ getSSEInstance()[prop] = value;
413
+ return true;
414
+ },
415
+ has(_target, prop) {
416
+ return prop in getSSEInstance();
417
+ },
418
+ getPrototypeOf() {
419
+ return SSEManager.prototype;
420
+ },
421
+ }
422
+ );
423
+
424
+ export { sse as default, sse, SSEManager, DEFAULT_HEARTBEAT_INTERVAL };