@maiyunnet/kebab 9.18.1 → 9.18.2

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.18.1";
8
+ export declare const VER = "9.18.2";
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.18.1';
9
+ export const VER = '9.18.2';
10
10
  // --- 服务端用的路径 ---
11
11
  const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
12
12
  /** --- /xxx/xxx --- */
@@ -47,6 +47,16 @@ export declare class Request {
47
47
  * @param timeout 秒
48
48
  */
49
49
  timeout(timeout: number): this;
50
+ /**
51
+ * --- 设置空闲连接允许复用的最长时间 ---
52
+ * @param timeout 秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒
53
+ */
54
+ keepAliveTimeout(timeout: number): this;
55
+ /**
56
+ * --- 设置连接建立后允许承接新请求的最长时间 ---
57
+ * @param timeout 秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制
58
+ */
59
+ reuseTimeout(timeout: number): this;
50
60
  /**
51
61
  * --- 设置网络异常后的重试次数 ---
52
62
  * @param retry 重试次数,默认为 1;非幂等请求需由调用方保证安全
@@ -59,6 +59,22 @@ export class Request {
59
59
  this._opt['timeout'] = timeout;
60
60
  return this;
61
61
  }
62
+ /**
63
+ * --- 设置空闲连接允许复用的最长时间 ---
64
+ * @param timeout 秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒
65
+ */
66
+ keepAliveTimeout(timeout) {
67
+ this._opt['keepAliveTimeout'] = timeout;
68
+ return this;
69
+ }
70
+ /**
71
+ * --- 设置连接建立后允许承接新请求的最长时间 ---
72
+ * @param timeout 秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制
73
+ */
74
+ reuseTimeout(timeout) {
75
+ this._opt['reuseTimeout'] = timeout;
76
+ return this;
77
+ }
62
78
  /**
63
79
  * --- 设置网络异常后的重试次数 ---
64
80
  * @param retry 重试次数,默认为 1;非幂等请求需由调用方保证安全
package/lib/undici.d.ts CHANGED
@@ -74,6 +74,10 @@ export declare function fetch(input: string | URL | Request, init?: RequestInit
74
74
  'hosts'?: Record<string, string> | string;
75
75
  /** --- 网络异常后的重试次数,默认 0;流式请求体不可重试,非幂等请求需由调用方保证安全 --- */
76
76
  'retry'?: number;
77
+ /** --- 空闲连接允许复用的最长秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒 --- */
78
+ 'keepAliveTimeout'?: number;
79
+ /** --- 连接建立后允许承接新请求的最长秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制 --- */
80
+ 'reuseTimeout'?: number;
77
81
  }): Promise<Response>;
78
82
  /**
79
83
  * --- 发起一个请求 ---
@@ -152,6 +156,10 @@ export interface IRequestOptions {
152
156
  };
153
157
  /** --- 连接是否保持长连接(即是否允许复用),默认为 true --- */
154
158
  'keep'?: boolean;
159
+ /** --- 空闲连接允许复用的最长秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒;自定义 Agent 由其自身配置 --- */
160
+ 'keepAliveTimeout'?: number;
161
+ /** --- 连接建立后允许承接新请求的最长秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制;自定义 Agent 由其自身配置 --- */
162
+ 'reuseTimeout'?: number;
155
163
  /** --- 复用池名/Agent,默认为 default --- */
156
164
  'reuse'?: string | undici.ProxyAgent | undici.Agent;
157
165
  /** --- cookie 托管对象 --- */
@@ -170,6 +178,10 @@ export interface IMproxyOptions {
170
178
  'hosts'?: Record<string, string> | string;
171
179
  'local'?: string;
172
180
  'headers'?: THttpHeaders;
181
+ /** --- 空闲连接允许复用的最长秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒 --- */
182
+ 'keepAliveTimeout'?: number;
183
+ /** --- 连接建立后允许承接新请求的最长秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制 --- */
184
+ 'reuseTimeout'?: number;
173
185
  /** --- 过滤 header,返回 true 则留下 --- */
174
186
  filter?: (h: string) => boolean;
175
187
  /** --- 默认为 default --- */
@@ -184,6 +196,10 @@ export interface IRproxyOptions {
184
196
  'hosts'?: Record<string, string> | string;
185
197
  'local'?: string;
186
198
  'headers'?: THttpHeaders;
199
+ /** --- 空闲连接允许复用的最长秒数;非正数或无效值按未设置处理;默认无服务端提示时为 4 秒,有提示时采用提示值减 2 秒且最多 600 秒 --- */
200
+ 'keepAliveTimeout'?: number;
201
+ /** --- 连接建立后允许承接新请求的最长秒数;非正数或无效值按未设置处理;达到后不再复用但不会中断在途请求,默认不限制 --- */
202
+ 'reuseTimeout'?: number;
187
203
  /** --- 过滤 header,返回 true 则留下 --- */
188
204
  filter?: (h: string) => boolean;
189
205
  /** --- 正向 mproxy 代理,url 如 https://xxx/abc --- */
package/lib/undici.js CHANGED
@@ -45,14 +45,31 @@ function normalizeRetry(retry) {
45
45
  return (typeof retry === 'number') && Number.isFinite(retry) ?
46
46
  Math.max(0, Math.floor(retry)) : 0;
47
47
  }
48
+ /**
49
+ * --- 将 Agent 的秒数配置转换为毫秒 ---
50
+ * @param timeout 秒数
51
+ * @returns 未设置或数值无效时返回 undefined,否则返回毫秒数
52
+ */
53
+ function normalizeAgentTimeout(timeout) {
54
+ if (timeout === undefined) {
55
+ return undefined;
56
+ }
57
+ const milliseconds = timeout * 1_000;
58
+ if (!Number.isFinite(milliseconds) || (milliseconds <= 0)) {
59
+ return undefined;
60
+ }
61
+ return milliseconds;
62
+ }
48
63
  /** --- 获取或创建 undici.agent 对象 --- */
49
64
  function getAgent(opt = {}) {
50
65
  let k = opt.reuse ?? 'default';
51
66
  if (typeof k !== 'string') {
52
67
  return k;
53
68
  }
69
+ const keepAliveTimeout = normalizeAgentTimeout(opt.keepAliveTimeout);
70
+ const reuseTimeout = normalizeAgentTimeout(opt.reuseTimeout);
54
71
  if (k === 'default') {
55
- // --- hosts/local/keep 均会影响 agent 行为,需生成独立的 key ---
72
+ // --- hosts/local/keep/连接时长均会影响 agent 行为,需生成独立的 key ---
56
73
  const features = [];
57
74
  if (opt.hosts) {
58
75
  features.push(`h:${lCrypto.hashHmac('md5', lText.stringifyJson(opt.hosts))}`);
@@ -63,6 +80,12 @@ function getAgent(opt = {}) {
63
80
  if (opt.keep === false) {
64
81
  features.push('k:0');
65
82
  }
83
+ if (keepAliveTimeout !== undefined) {
84
+ features.push(`ka:${keepAliveTimeout}`);
85
+ }
86
+ if (reuseTimeout !== undefined) {
87
+ features.push(`rt:${reuseTimeout}`);
88
+ }
66
89
  if (features.length > 0) {
67
90
  k = features.join('|');
68
91
  }
@@ -74,6 +97,13 @@ function getAgent(opt = {}) {
74
97
  lookup: buildDnsLookup(opt.hosts),
75
98
  },
76
99
  'pipelining': opt.keep === false ? 0 : 1,
100
+ ...(keepAliveTimeout === undefined ? {} : {
101
+ 'keepAliveTimeout': keepAliveTimeout,
102
+ 'keepAliveMaxTimeout': keepAliveTimeout,
103
+ }),
104
+ ...(reuseTimeout === undefined ? {} : {
105
+ 'clientTtl': reuseTimeout,
106
+ }),
77
107
  }));
78
108
  }
79
109
  return agents.get(k);
@@ -293,6 +323,8 @@ export async function fetch(input, init = {}) {
293
323
  'hosts': init.hosts,
294
324
  'mproxy': init.mproxy,
295
325
  'retry': init.retry,
326
+ 'keepAliveTimeout': init.keepAliveTimeout,
327
+ 'reuseTimeout': init.reuseTimeout,
296
328
  'signal': init.signal ?? undefined,
297
329
  'follow': init.redirect === 'follow' ? 10 : 0,
298
330
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.18.1",
3
+ "version": "9.18.2",
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": [
package/sys/child.js CHANGED
@@ -203,7 +203,7 @@ async function requestHandler(req, res, https) {
203
203
  'timer': {},
204
204
  'timeout': 30_000,
205
205
  'callback': () => {
206
- if (!req.socket?.writable) {
206
+ if (!sCtr.isHttpRequestAvailable(req, res)) {
207
207
  // --- 用户连接已中断 ---
208
208
  return;
209
209
  }
package/sys/ctr.d.ts CHANGED
@@ -58,6 +58,13 @@ export type TValibotResult<TSchema extends v.GenericSchema, TIssue extends v.Bas
58
58
  readonly issues: [TIssue, ...TIssue[]];
59
59
  readonly response: kebab.Json[];
60
60
  };
61
+ /**
62
+ * --- 判断当前 HTTP 请求是否仍可响应 ---
63
+ * @param req 请求对象
64
+ * @param res 响应对象
65
+ * @returns 客户端对应的请求流仍可响应时返回 true
66
+ */
67
+ export declare function isHttpRequestAvailable(req: http2.Http2ServerRequest | http.IncomingMessage, res: http2.Http2ServerResponse | http.ServerResponse): boolean;
61
68
  export declare class Ctr {
62
69
  /** --- 路由参数序列数组 --- */
63
70
  protected _param: string[];
package/sys/ctr.js CHANGED
@@ -19,6 +19,27 @@ export function clearLocaleData() {
19
19
  localeFiles = [];
20
20
  localeData = {};
21
21
  }
22
+ /**
23
+ * --- 判断当前 HTTP 请求是否仍可响应 ---
24
+ * @param req 请求对象
25
+ * @param res 响应对象
26
+ * @returns 客户端对应的请求流仍可响应时返回 true
27
+ */
28
+ export function isHttpRequestAvailable(req, res) {
29
+ if (req.destroyed && !req.complete) {
30
+ return false;
31
+ }
32
+ if (('stream' in req) && (req.stream.closed || req.stream.destroyed)) {
33
+ return false;
34
+ }
35
+ if (req.socket.destroyed || !req.socket.writable) {
36
+ return false;
37
+ }
38
+ if (res.destroyed || res.closed || res.writableEnded || !res.writable) {
39
+ return false;
40
+ }
41
+ return true;
42
+ }
22
43
  export class Ctr {
23
44
  /** --- 路由参数序列数组 --- */
24
45
  _param = [];
@@ -69,7 +90,13 @@ export class Ctr {
69
90
  }
70
91
  /** --- 当前用户连接是否还在连接中 --- */
71
92
  get _isAvail() {
72
- return this._req.socket.writable;
93
+ if (this._socket) {
94
+ return this._socket.writable;
95
+ }
96
+ if (!this._res) {
97
+ return !this._req.socket.destroyed && this._req.socket.writable;
98
+ }
99
+ return isHttpRequestAvailable(this._req, this._res);
73
100
  }
74
101
  /** --- timeout 的 timer --- */
75
102
  _timer;
package/sys/route.d.ts CHANGED
@@ -51,12 +51,13 @@ export declare function waitCtr(cctr: sCtr.Ctr): Promise<void>;
51
51
  /**
52
52
  * --- 获取 post 对象(通常已自动获取),如果是文件上传(formdata)的情况则不获取 ---
53
53
  * @param req 请求对象
54
+ * @returns 解析后的 POST 数据,请求传输中断时返回 false
54
55
  */
55
56
  export declare function getPost(req: http2.Http2ServerRequest | http.IncomingMessage): Promise<{
56
57
  'input': string;
57
58
  'raw': Record<string, any>;
58
59
  'post': Record<string, any>;
59
- }>;
60
+ } | false>;
60
61
  /**
61
62
  * --- 获取 formdata 的 post ---
62
63
  * @param req 请求头
package/sys/route.js CHANGED
@@ -453,6 +453,9 @@ export async function run(data) {
453
453
  }
454
454
  if (reqStartRtn === 1) {
455
455
  const rawPost = await getPost(data.req);
456
+ if (!rawPost) {
457
+ return true;
458
+ }
456
459
  // --- 原始 POST ---
457
460
  middle.setPrototype('_rawPost', rawPost.raw);
458
461
  // --- 原始 input ---
@@ -844,6 +847,7 @@ export async function waitCtr(cctr) {
844
847
  /**
845
848
  * --- 获取 post 对象(通常已自动获取),如果是文件上传(formdata)的情况则不获取 ---
846
849
  * @param req 请求对象
850
+ * @returns 解析后的 POST 数据,请求传输中断时返回 false
847
851
  */
848
852
  export function getPost(req) {
849
853
  return new Promise(function (resolve) {
@@ -861,7 +865,28 @@ export function getPost(req) {
861
865
  const maxPostSize = 50 * 1024 * 1024;
862
866
  let buffer = Buffer.from('');
863
867
  let overflow = false;
864
- req.on('data', function (chunk) {
868
+ let finished = false;
869
+ let onData;
870
+ let onEnd;
871
+ let onError;
872
+ let onAborted;
873
+ let onClose;
874
+ const cleanup = () => {
875
+ req.off('data', onData);
876
+ req.off('end', onEnd);
877
+ req.off('error', onError);
878
+ req.off('aborted', onAborted);
879
+ req.off('close', onClose);
880
+ };
881
+ const finish = (result) => {
882
+ if (finished) {
883
+ return;
884
+ }
885
+ finished = true;
886
+ cleanup();
887
+ resolve(result);
888
+ };
889
+ onData = (chunk) => {
865
890
  if (overflow) {
866
891
  return;
867
892
  }
@@ -871,10 +896,10 @@ export function getPost(req) {
871
896
  return;
872
897
  }
873
898
  buffer = Buffer.concat([buffer, chunk], buffer.length + chunk.length);
874
- });
875
- req.on('end', function () {
899
+ };
900
+ onEnd = () => {
876
901
  if (overflow) {
877
- resolve({
902
+ finish({
878
903
  'input': '',
879
904
  'raw': {},
880
905
  'post': {},
@@ -883,7 +908,7 @@ export function getPost(req) {
883
908
  }
884
909
  const s = buffer.toString();
885
910
  if (!s) {
886
- resolve({
911
+ finish({
887
912
  'input': '',
888
913
  'raw': {},
889
914
  'post': {},
@@ -894,14 +919,14 @@ export function getPost(req) {
894
919
  if (ct.includes('json')) {
895
920
  try {
896
921
  const raw = lText.parseJson(s);
897
- resolve({
922
+ finish({
898
923
  'input': s,
899
924
  'raw': raw,
900
925
  'post': lText.trimJson(raw),
901
926
  });
902
927
  }
903
928
  catch {
904
- resolve({
929
+ finish({
905
930
  'input': '',
906
931
  'raw': {},
907
932
  'post': {},
@@ -910,12 +935,26 @@ export function getPost(req) {
910
935
  return;
911
936
  }
912
937
  const raw = lText.queryParse(s);
913
- resolve({
938
+ finish({
914
939
  'input': s,
915
940
  'raw': raw,
916
941
  'post': lText.trimJson(raw),
917
942
  });
918
- });
943
+ };
944
+ onError = () => {
945
+ finish(false);
946
+ };
947
+ onAborted = () => {
948
+ finish(false);
949
+ };
950
+ onClose = () => {
951
+ finish(false);
952
+ };
953
+ req.on('data', onData);
954
+ req.on('end', onEnd);
955
+ req.on('error', onError);
956
+ req.on('aborted', onAborted);
957
+ req.on('close', onClose);
919
958
  });
920
959
  }
921
960
  /**