@maiyunnet/kebab 9.17.1 → 9.17.3

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/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * --- 本文件用来定义每个目录实体地址的常量 ---
6
6
  */
7
7
  /** --- 当前系统版本号 --- */
8
- export declare const VER = "9.17.1";
8
+ export declare const VER = "9.17.3";
9
9
  /** --- 框架根目录,以 / 结尾 --- */
10
10
  export declare const ROOT_PATH: string;
11
11
  /** --- 框架的 LIB,以 / 结尾 --- */
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * --- 本文件用来定义每个目录实体地址的常量 ---
7
7
  */
8
8
  /** --- 当前系统版本号 --- */
9
- export const VER = '9.17.1';
9
+ export const VER = '9.17.3';
10
10
  // --- 服务端用的路径 ---
11
11
  const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
12
12
  /** --- /xxx/xxx --- */
package/lib/core.d.ts CHANGED
@@ -206,6 +206,15 @@ export declare function sendRestart(hosts?: string[] | 'config'): Promise<Record
206
206
  'result': boolean;
207
207
  'return': string;
208
208
  }>>;
209
+ /**
210
+ * --- 向主进程(或局域网同代码机子)发送 stop 操作,停止接收新连接并在现有连接全部结束后退出 ---
211
+ * @param hosts 局域网主机列表,config 表示使用全局配置;不传时通知当前 master
212
+ * @returns 各主机发送结果
213
+ */
214
+ export declare function sendStop(hosts?: string[] | 'config'): Promise<Record<string, {
215
+ 'result': boolean;
216
+ 'return': string;
217
+ }>>;
209
218
  /** --- PM2 操作类型 --- */
210
219
  export type TPm2Action = 'start' | 'stop' | 'restart';
211
220
  /**
@@ -213,6 +222,7 @@ export type TPm2Action = 'start' | 'stop' | 'restart';
213
222
  * @param name PM2 进程名称
214
223
  * @param action PM2 操作类型
215
224
  * @param hosts 局域网列表
225
+ * @returns 各主机是否已接收并排队,不代表 PM2 操作最终执行成功
216
226
  */
217
227
  export declare function sendPm2(name: string, action?: TPm2Action, hosts?: string[] | 'config'): Promise<Record<string, {
218
228
  'result': boolean;
package/lib/core.js CHANGED
@@ -713,11 +713,62 @@ export async function sendRestart(hosts) {
713
713
  }
714
714
  return rtn;
715
715
  }
716
+ /**
717
+ * --- 向主进程(或局域网同代码机子)发送 stop 操作,停止接收新连接并在现有连接全部结束后退出 ---
718
+ * @param hosts 局域网主机列表,config 表示使用全局配置;不传时通知当前 master
719
+ * @returns 各主机发送结果
720
+ */
721
+ export async function sendStop(hosts) {
722
+ if (!hosts) {
723
+ // --- 本地模式 ---
724
+ // eslint-disable-next-line no-console
725
+ console.log('[ Child] Sending stop request...');
726
+ process.send({
727
+ 'action': 'stop'
728
+ });
729
+ return {
730
+ '127.0.0.1': { 'result': true, 'return': 'Done' }
731
+ };
732
+ }
733
+ if (hosts === 'config') {
734
+ hosts = globalConfig.hosts;
735
+ }
736
+ // --- 未传或 config 展开后为空数组,均回退到本机 ---
737
+ if (!hosts?.length) {
738
+ hosts = ['127.0.0.1'];
739
+ }
740
+ // --- 局域网模式 ---
741
+ const time = lTime.stamp();
742
+ /** --- 返回成功的 host --- */
743
+ const rtn = {};
744
+ for (const host of hosts) {
745
+ const res = await lUndici.get('http://' + host + ':' + globalConfig.rpcPort.toString() + '/' + lCrypto.aesEncrypt(lText.stringifyJson({
746
+ 'action': 'stop',
747
+ 'time': time
748
+ }), globalConfig.rpcSecret), {
749
+ 'timeout': 5
750
+ });
751
+ const content = await res.getContent();
752
+ if (!content) {
753
+ rtn[host] = { 'result': false, 'return': 'Timeout' };
754
+ continue;
755
+ }
756
+ const str = content.toString();
757
+ if (str === 'Done') {
758
+ rtn[host] = { 'result': true, 'return': 'Done' };
759
+ }
760
+ else {
761
+ rtn[host] = { 'result': false, 'return': str };
762
+ }
763
+ }
764
+ return rtn;
765
+ }
716
766
  /**
717
767
  * --- 向本机或局域网 RPC 发送 PM2 操作 ---
718
768
  * @param name PM2 进程名称
719
769
  * @param action PM2 操作类型
720
770
  * @param hosts 局域网列表
771
+ * @returns 各主机是否已接收并排队,不代表 PM2 操作最终执行成功
721
772
  */
722
773
  export async function sendPm2(name, action = 'restart', hosts) {
723
774
  if (hosts === 'config') {
package/lib/socket.js CHANGED
@@ -17,31 +17,60 @@ export function rwebsocket(port, url, opt = {}) {
17
17
  /** --- 请求端产生的双向 socket --- */
18
18
  const server = net.createServer(socket => {
19
19
  socket.setKeepAlive(true);
20
+ /** --- 远程端的双向 WebSocket --- */
21
+ let rws = null;
22
+ /** --- 心跳定时器 --- */
23
+ let timer = null;
24
+ /** --- 是否已开始清理 --- */
25
+ let closed = false;
26
+ /** --- 同时清理 TCP、WebSocket 与心跳 --- */
27
+ const close = () => {
28
+ if (closed) {
29
+ return;
30
+ }
31
+ closed = true;
32
+ if (timer) {
33
+ clearInterval(timer);
34
+ timer = null;
35
+ }
36
+ socket.destroy();
37
+ rws?.destroy();
38
+ };
39
+ // --- 必须在异步连接前监听,避免本地先断开后留下远端连接 ---
40
+ socket.on('close', close).on('error', err => {
41
+ lCore.display('[' + lTime.format(null, 'Y-m-d H:i:s') + '] Client error: ' + socket.remoteAddress + ':' + socket.remotePort + ', ' + err.message);
42
+ close();
43
+ });
20
44
  (async () => {
21
45
  // --- 每次进一个新连接都反代到一个新 WebSocket ---
22
46
  lCore.display('[' + lTime.format(null, 'Y-m-d H:i:s') + '] New client: ' + socket.remoteAddress + ':' + socket.remotePort);
23
- /** --- 远程端的双向 websocket --- */
24
- const rws = await lWs.connect(url, opt);
47
+ rws = await lWs.connect(url, opt);
25
48
  if (!rws) {
26
- socket.end();
49
+ close();
50
+ return false;
51
+ }
52
+ if (closed) {
53
+ rws.destroy();
27
54
  return false;
28
55
  }
29
- const timer = setInterval(() => {
30
- rws.ping();
56
+ timer = setInterval(() => {
57
+ rws?.ping();
31
58
  }, 10_000);
32
59
  rws.on('message', msg => {
33
60
  switch (msg.opcode) {
34
61
  case lWs.EOpcode.TEXT:
35
62
  case lWs.EOpcode.BINARY: {
36
- socket.write(msg.data);
63
+ if (!socket.write(msg.data)) {
64
+ rws?.pause();
65
+ }
37
66
  break;
38
67
  }
39
68
  case lWs.EOpcode.CLOSE: {
40
- socket.end();
69
+ close();
41
70
  break;
42
71
  }
43
72
  case lWs.EOpcode.PING: {
44
- rws.pong();
73
+ rws?.pong();
45
74
  break;
46
75
  }
47
76
  case lWs.EOpcode.PONG: {
@@ -51,20 +80,18 @@ export function rwebsocket(port, url, opt = {}) {
51
80
  // --- EOpcode.CONTINUATION ---
52
81
  }
53
82
  }
54
- }).on('close', () => {
55
- clearInterval(timer);
56
- socket.end();
57
- });
83
+ }).on('drain', () => {
84
+ socket.resume();
85
+ }).on('close', close);
58
86
  socket.on('data', data => {
59
- rws.writeBinary(data);
60
- }).on('close', () => {
61
- clearInterval(timer);
62
- rws.end();
87
+ if (rws && !rws.writeBinary(data)) {
88
+ socket.pause();
89
+ }
90
+ }).on('drain', () => {
91
+ rws?.resume();
63
92
  }).on('end', () => {
64
- rws.end();
65
93
  lCore.display('[' + lTime.format(null, 'Y-m-d H:i:s') + '] Client disconnected: ' + socket.remoteAddress + ':' + socket.remotePort);
66
- }).on('error', err => {
67
- lCore.display('[' + lTime.format(null, 'Y-m-d H:i:s') + '] Client error: ' + socket.remoteAddress + ':' + socket.remotePort + ', ' + err.message);
94
+ close();
68
95
  });
69
96
  })().catch(() => { });
70
97
  }).listen(port, () => {
@@ -31,7 +31,7 @@ export declare class Response {
31
31
  /**
32
32
  * --- 获取响应读取流对象 ---
33
33
  */
34
- getStream(): zlib.BrotliDecompress | zlib.Gunzip | zlib.Inflate | (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | null;
34
+ getStream(): (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | zlib.Gunzip | zlib.Inflate | zlib.BrotliDecompress | null;
35
35
  /**
36
36
  * --- 获取原生响应读取流对象 ---
37
37
  */
package/lib/undici.d.ts CHANGED
@@ -133,6 +133,8 @@ export interface IRequestOptions {
133
133
  'retry'?: number;
134
134
  /** --- JSON 解析失败后的重试次数,默认 0;仅适用于 ResponseJson 快捷方法,非幂等请求需由调用方保证安全 --- */
135
135
  'retryJson'?: number;
136
+ /** --- JSON 重试验证方法;返回 false 时按 retryJson 的设置重试,仅适用于 ResponseJson 快捷方法 --- */
137
+ 'retryJsonHandler'?: (json: kebab.Json) => boolean;
136
138
  /** --- 追踪 location 次数,0 为不追踪,默认为 0 --- */
137
139
  'follow'?: number;
138
140
  /** --- 自定义 host 映射,如 {'www.maiyun.net': '127.0.0.1'},或全部映射到一个 host --- */
package/lib/undici.js CHANGED
@@ -152,7 +152,7 @@ async function requestResponseJson(u, data, opt, action) {
152
152
  }
153
153
  const rtnStr = rtn.toString();
154
154
  const json = lText.parseJson(rtnStr);
155
- if (json) {
155
+ if (json && (opt.retryJsonHandler?.(json) ?? true)) {
156
156
  return json;
157
157
  }
158
158
  if (i < retryJson) {
@@ -162,7 +162,8 @@ async function requestResponseJson(u, data, opt, action) {
162
162
  }
163
163
  if (opt.log === undefined || opt.log) {
164
164
  const requestData = data === undefined ? '' : `, data: ${lText.stringifyJson(data)}`;
165
- lCore.log({}, `[UNDICI][${action}] parse json failed, url: ${u}${requestData}, content: ${rtnStr}`, '-neterror');
165
+ const reason = json ? 'retry json handler returned false' : 'parse json failed';
166
+ lCore.log({}, `[UNDICI][${action}] ${reason}, url: ${u}${requestData}, content: ${rtnStr}`, '-neterror');
166
167
  }
167
168
  return false;
168
169
  }
@@ -635,10 +636,14 @@ export async function mproxy(ctr, auth, opt = {}) {
635
636
  });
636
637
  // --- 同时监听 close 确保 promise 一定会 resolve(客户端断连等场景) ---
637
638
  stream.pipe(res).on('close', () => {
639
+ stream.destroy();
638
640
  resolve();
639
641
  });
640
642
  });
641
643
  }
644
+ else {
645
+ stream.destroy();
646
+ }
642
647
  return 1;
643
648
  }
644
649
  /**
@@ -738,16 +743,22 @@ export async function rproxy(ctr, route, opt = {}) {
738
743
  // --- 同时监听 close 确保 promise 一定会 resolve(客户端断连等场景) ---
739
744
  if (compress) {
740
745
  stream.pipe(compress.compress).pipe(res).on('close', () => {
746
+ stream.destroy();
747
+ compress.compress.destroy();
741
748
  resolve();
742
749
  });
743
750
  }
744
751
  else {
745
752
  stream.pipe(res).on('close', () => {
753
+ stream.destroy();
746
754
  resolve();
747
755
  });
748
756
  }
749
757
  });
750
758
  }
759
+ else {
760
+ stream.destroy();
761
+ }
751
762
  return true;
752
763
  }
753
764
  return false;
package/lib/ws.d.ts CHANGED
@@ -96,12 +96,23 @@ export declare class Socket {
96
96
  private _bindEvent;
97
97
  /** --- 还未开启监听时来的数据将存在这里 --- */
98
98
  private readonly _waitMsg;
99
+ /** --- 尚未交给消息监听器的数据量 --- */
100
+ private _waitMsgBytes;
101
+ /** --- 是否暂停向上层派发消息 --- */
102
+ private _paused;
99
103
  /** --- 还未开启 error 监听时产生的 error 错误对象 --- */
100
104
  private _error;
101
105
  /** --- 还未开启 close 监听时是不是就已经 close --- */
102
106
  private _close;
103
107
  /** --- 绑定的自定义监听事件(未绑定则默认在 _bindEvent 执行) --- */
104
108
  private _on;
109
+ /**
110
+ * --- 暂存尚不能交给上层处理的消息 ---
111
+ * @param msg 消息
112
+ */
113
+ private _queueMessage;
114
+ /** --- 依次派发缓存消息,若再次发生背压则停止 --- */
115
+ private _flushMessages;
105
116
  /** --- 绑定监听 --- */
106
117
  on(event: 'message', cb: (msg: {
107
118
  'opcode': EOpcode;
@@ -113,6 +124,10 @@ export declare class Socket {
113
124
  off(event: 'message' | 'drain' | 'error' | 'close' | 'end' | 'timeout'): this;
114
125
  end(): void;
115
126
  destroy(): void;
127
+ /** --- 暂停向消息监听器派发数据,底层仍只保留有界缓存 --- */
128
+ pause(): void;
129
+ /** --- 恢复向消息监听器派发数据 --- */
130
+ resume(): void;
116
131
  /** --- 发送文本 --- */
117
132
  writeText(data: Buffer | string | Array<Buffer | string>): boolean;
118
133
  /** --- 发送结果对象字符串 --- */
package/lib/ws.js CHANGED
@@ -24,6 +24,10 @@ export var EOpcode;
24
24
  const liwsServer = liws.createServer({
25
25
  'frameReceiveMode': EFrameReceiveMode.SIMPLE,
26
26
  });
27
+ /** --- 未能及时转发的 WebSocket 消息最大缓存量,超过后关闭连接保护进程内存 --- */
28
+ const MAX_PENDING_MESSAGE_BYTES = 8 * 1024 * 1024;
29
+ /** --- 未能及时转发的 WebSocket 消息最大条数,防止大量小帧占满内存 --- */
30
+ const MAX_PENDING_MESSAGES = 4_096;
27
31
  export class Socket {
28
32
  /** --- 当前的 ws 对象 --- */
29
33
  _ws;
@@ -132,19 +136,15 @@ export class Socket {
132
136
  if (!('data' in msg)) {
133
137
  return;
134
138
  }
135
- const buf = Buffer.concat(msg.data);
136
- if (this._on.message) {
137
- this._on.message({
138
- 'opcode': msg.opcode,
139
- 'data': buf,
140
- });
141
- }
142
- else {
143
- this._waitMsg.push({
144
- 'opcode': msg.opcode,
145
- 'data': buf,
146
- });
139
+ const item = {
140
+ 'opcode': msg.opcode,
141
+ 'data': Buffer.concat(msg.data),
142
+ };
143
+ if (this._paused || !this._on.message) {
144
+ this._queueMessage(item);
145
+ return;
147
146
  }
147
+ this._on.message(item);
148
148
  }).on('drain', () => {
149
149
  this._on.drain?.();
150
150
  }).on('error', (e) => {
@@ -157,6 +157,8 @@ export class Socket {
157
157
  }).on('end', () => {
158
158
  this._on.end?.();
159
159
  }).on('close', () => {
160
+ this._waitMsg.length = 0;
161
+ this._waitMsgBytes = 0;
160
162
  if (this._on.close) {
161
163
  this._on.close();
162
164
  }
@@ -169,6 +171,10 @@ export class Socket {
169
171
  }
170
172
  /** --- 还未开启监听时来的数据将存在这里 --- */
171
173
  _waitMsg = [];
174
+ /** --- 尚未交给消息监听器的数据量 --- */
175
+ _waitMsgBytes = 0;
176
+ /** --- 是否暂停向上层派发消息 --- */
177
+ _paused = false;
172
178
  /** --- 还未开启 error 监听时产生的 error 错误对象 --- */
173
179
  _error = null;
174
180
  /** --- 还未开启 close 监听时是不是就已经 close --- */
@@ -183,13 +189,44 @@ export class Socket {
183
189
  end: undefined,
184
190
  timeout: undefined,
185
191
  };
192
+ /**
193
+ * --- 暂存尚不能交给上层处理的消息 ---
194
+ * @param msg 消息
195
+ */
196
+ _queueMessage(msg) {
197
+ this._waitMsgBytes += msg.data.length;
198
+ if ((this._waitMsgBytes > MAX_PENDING_MESSAGE_BYTES) ||
199
+ (this._waitMsg.length >= MAX_PENDING_MESSAGES)) {
200
+ this._waitMsg.length = 0;
201
+ this._waitMsgBytes = 0;
202
+ this.destroy();
203
+ return;
204
+ }
205
+ this._waitMsg.push(msg);
206
+ }
207
+ /** --- 依次派发缓存消息,若再次发生背压则停止 --- */
208
+ _flushMessages() {
209
+ if (this._paused || !this._on.message || !this._waitMsg.length) {
210
+ return;
211
+ }
212
+ const messages = this._waitMsg.splice(0);
213
+ this._waitMsgBytes = 0;
214
+ for (let i = 0; i < messages.length; ++i) {
215
+ if (this._paused) {
216
+ for (; i < messages.length; ++i) {
217
+ this._waitMsg.push(messages[i]);
218
+ this._waitMsgBytes += messages[i].data.length;
219
+ }
220
+ return;
221
+ }
222
+ this._on.message(messages[i]);
223
+ }
224
+ }
186
225
  on(event, cb) {
187
226
  this._on[event] = cb;
188
227
  switch (event) {
189
228
  case 'message': {
190
- for (const item of this._waitMsg) {
191
- cb(item);
192
- }
229
+ this._flushMessages();
193
230
  break;
194
231
  }
195
232
  case 'error': {
@@ -227,6 +264,15 @@ export class Socket {
227
264
  destroy() {
228
265
  this._ws.destroy();
229
266
  }
267
+ /** --- 暂停向消息监听器派发数据,底层仍只保留有界缓存 --- */
268
+ pause() {
269
+ this._paused = true;
270
+ }
271
+ /** --- 恢复向消息监听器派发数据 --- */
272
+ resume() {
273
+ this._paused = false;
274
+ this._flushMessages();
275
+ }
230
276
  /** --- 发送文本 --- */
231
277
  writeText(data) {
232
278
  if (!this._ws.writable) {
@@ -311,69 +357,93 @@ export function createServer(request, socket, head, options = {}) {
311
357
  */
312
358
  function bindPipe(s1, s2) {
313
359
  return new Promise(resolve => {
360
+ /** --- 是否已经完成关闭,防止双向 close 重复处理 --- */
361
+ let closed = false;
362
+ /** --- 两端一起销毁,避免半开连接继续占用资源 --- */
363
+ const close = () => {
364
+ if (closed) {
365
+ return;
366
+ }
367
+ closed = true;
368
+ s1.destroy();
369
+ s2.destroy();
370
+ resolve();
371
+ };
314
372
  // --- 监听发送端的 ---
315
373
  s1.on('message', (msg) => {
316
374
  switch (msg.opcode) {
317
375
  case EOpcode.TEXT: {
318
- s2.writeText(msg.data.toString());
376
+ if (!s2.writeText(msg.data.toString())) {
377
+ s1.pause();
378
+ }
319
379
  break;
320
380
  }
321
381
  case EOpcode.BINARY: {
322
- s2.writeBinary(msg.data);
382
+ if (!s2.writeBinary(msg.data)) {
383
+ s1.pause();
384
+ }
323
385
  break;
324
386
  }
325
387
  case EOpcode.CLOSE: {
326
- s2.end();
327
- resolve();
388
+ close();
328
389
  break;
329
390
  }
330
391
  case EOpcode.PING: {
331
- s2.ping(msg.data);
392
+ if (!s2.ping(msg.data)) {
393
+ s1.pause();
394
+ }
332
395
  break;
333
396
  }
334
397
  case EOpcode.PONG: {
335
- s2.pong(msg.data);
398
+ if (!s2.pong(msg.data)) {
399
+ s1.pause();
400
+ }
336
401
  break;
337
402
  }
338
403
  default: {
339
404
  // --- EOpcode.CONTINUATION ---
340
405
  }
341
406
  }
342
- }).on('close', () => {
343
- s2.end();
344
- resolve();
407
+ }).on('close', close).on('drain', () => {
408
+ s2.resume();
345
409
  });
346
410
  // --- 监听远程端的 ---
347
411
  s2.on('message', (msg) => {
348
412
  switch (msg.opcode) {
349
413
  case EOpcode.TEXT: {
350
- s1.writeText(msg.data.toString());
414
+ if (!s1.writeText(msg.data.toString())) {
415
+ s2.pause();
416
+ }
351
417
  break;
352
418
  }
353
419
  case EOpcode.BINARY: {
354
- s1.writeBinary(msg.data);
420
+ if (!s1.writeBinary(msg.data)) {
421
+ s2.pause();
422
+ }
355
423
  break;
356
424
  }
357
425
  case EOpcode.CLOSE: {
358
- s1.end();
359
- resolve();
426
+ close();
360
427
  break;
361
428
  }
362
429
  case EOpcode.PING: {
363
- s1.ping(msg.data);
430
+ if (!s1.ping(msg.data)) {
431
+ s2.pause();
432
+ }
364
433
  break;
365
434
  }
366
435
  case EOpcode.PONG: {
367
- s1.pong(msg.data);
436
+ if (!s1.pong(msg.data)) {
437
+ s2.pause();
438
+ }
368
439
  break;
369
440
  }
370
441
  default: {
371
442
  // --- EOpcode.CONTINUATION ---
372
443
  }
373
444
  }
374
- }).on('close', () => {
375
- s1.end();
376
- resolve();
445
+ }).on('close', close).on('drain', () => {
446
+ s1.resume();
377
447
  });
378
448
  });
379
449
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.17.1",
3
+ "version": "9.17.3",
4
4
  "description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -35,7 +35,7 @@
35
35
  "@tailwindcss/cli": "^4.3.3",
36
36
  "@types/ssh2": "^1.15.5",
37
37
  "ajv": "^8.20.0",
38
- "ajv-formats": "^3.0.1",
38
+ "ajv-formats": "^3.0.1",
39
39
  "class-variance-authority": "^0.7.1",
40
40
  "clsx": "^2.1.1",
41
41
  "ejs": "^6.0.1",