@maiyunnet/kebab 9.18.0 → 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.0";
8
+ export declare const VER = "9.18.2";
9
9
  /** --- 框架根目录,以 / 结尾 --- */
10
10
  export declare const ROOT_PATH: string;
11
11
  /** --- 框架的 LIB,以 / 结尾 --- */
@@ -159,6 +159,8 @@ export interface IConfigConst {
159
159
  'host': string;
160
160
  'hostname': string;
161
161
  'hostport': number;
162
+ /** --- 协议与 host,如 https://example.com:8443 --- */
163
+ 'urlOrigin': string;
162
164
  'uri': IUrlParse;
163
165
  'rootPath': string;
164
166
  'modPath': string;
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * --- 本文件用来定义每个目录实体地址的常量 ---
7
7
  */
8
8
  /** --- 当前系统版本号 --- */
9
- export const VER = '9.18.0';
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.0",
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
@@ -145,6 +145,7 @@ export async function run(data) {
145
145
  'host': data.uri.host ?? '',
146
146
  'hostname': data.uri.hostname ?? '',
147
147
  'hostport': data.uri.port ? parseInt(data.uri.port) : (data.uri.protocol === 'https:' ? 443 : 80),
148
+ 'urlOrigin': (data.uri.protocol ?? '') + '//' + (data.uri.host ?? ''),
148
149
  'uri': data.uri,
149
150
  // --- 服务端用的路径 ---
150
151
  'rootPath': data.rootPath,
@@ -452,6 +453,9 @@ export async function run(data) {
452
453
  }
453
454
  if (reqStartRtn === 1) {
454
455
  const rawPost = await getPost(data.req);
456
+ if (!rawPost) {
457
+ return true;
458
+ }
455
459
  // --- 原始 POST ---
456
460
  middle.setPrototype('_rawPost', rawPost.raw);
457
461
  // --- 原始 input ---
@@ -843,6 +847,7 @@ export async function waitCtr(cctr) {
843
847
  /**
844
848
  * --- 获取 post 对象(通常已自动获取),如果是文件上传(formdata)的情况则不获取 ---
845
849
  * @param req 请求对象
850
+ * @returns 解析后的 POST 数据,请求传输中断时返回 false
846
851
  */
847
852
  export function getPost(req) {
848
853
  return new Promise(function (resolve) {
@@ -860,7 +865,28 @@ export function getPost(req) {
860
865
  const maxPostSize = 50 * 1024 * 1024;
861
866
  let buffer = Buffer.from('');
862
867
  let overflow = false;
863
- 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) => {
864
890
  if (overflow) {
865
891
  return;
866
892
  }
@@ -870,10 +896,10 @@ export function getPost(req) {
870
896
  return;
871
897
  }
872
898
  buffer = Buffer.concat([buffer, chunk], buffer.length + chunk.length);
873
- });
874
- req.on('end', function () {
899
+ };
900
+ onEnd = () => {
875
901
  if (overflow) {
876
- resolve({
902
+ finish({
877
903
  'input': '',
878
904
  'raw': {},
879
905
  'post': {},
@@ -882,7 +908,7 @@ export function getPost(req) {
882
908
  }
883
909
  const s = buffer.toString();
884
910
  if (!s) {
885
- resolve({
911
+ finish({
886
912
  'input': '',
887
913
  'raw': {},
888
914
  'post': {},
@@ -893,14 +919,14 @@ export function getPost(req) {
893
919
  if (ct.includes('json')) {
894
920
  try {
895
921
  const raw = lText.parseJson(s);
896
- resolve({
922
+ finish({
897
923
  'input': s,
898
924
  'raw': raw,
899
925
  'post': lText.trimJson(raw),
900
926
  });
901
927
  }
902
928
  catch {
903
- resolve({
929
+ finish({
904
930
  'input': '',
905
931
  'raw': {},
906
932
  'post': {},
@@ -909,12 +935,26 @@ export function getPost(req) {
909
935
  return;
910
936
  }
911
937
  const raw = lText.queryParse(s);
912
- resolve({
938
+ finish({
913
939
  'input': s,
914
940
  'raw': raw,
915
941
  'post': lText.trimJson(raw),
916
942
  });
917
- });
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);
918
958
  });
919
959
  }
920
960
  /**