@maiyunnet/kebab 9.15.6 → 9.16.1
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/doc/kebab-rag.md +216 -121
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/core.d.ts +22 -3
- package/lib/core.js +23 -4
- package/lib/db/conn.js +13 -3
- package/lib/db/pool.js +21 -11
- package/lib/text.d.ts +6 -0
- package/lib/text.js +24 -1
- package/lib/undici/request.d.ts +6 -1
- package/lib/undici/request.js +8 -0
- package/lib/undici/response.d.ts +2 -2
- package/lib/undici/response.js +2 -1
- package/lib/undici.d.ts +13 -6
- package/lib/undici.js +83 -35
- package/package.json +1 -1
- package/sys/child.js +15 -12
- package/sys/ctr.js +3 -3
- package/sys/monitor/watchdog.js +9 -5
- package/sys/monitor.d.ts +8 -1
- package/sys/monitor.js +198 -81
- package/sys/route.d.ts +1 -1
- package/sys/route.js +14 -14
- package/www/example/ctr/test.d.ts +2 -0
- package/www/example/ctr/test.js +44 -3
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* --- 本文件用来定义每个目录实体地址的常量 ---
|
|
7
7
|
*/
|
|
8
8
|
/** --- 当前系统版本号 --- */
|
|
9
|
-
export const VER = '9.
|
|
9
|
+
export const VER = '9.16.1';
|
|
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
|
@@ -352,12 +352,31 @@ export declare function debug(message?: any, ...optionalParams: any[]): void;
|
|
|
352
352
|
*/
|
|
353
353
|
export declare function display(message?: any, ...optionalParams: any[]): void;
|
|
354
354
|
/**
|
|
355
|
-
* ---
|
|
355
|
+
* --- 提交 HTTP 响应状态和头部,兼容 HTTP/1.1 与 HTTP/2 ---
|
|
356
|
+
*
|
|
357
|
+
* `setHeader()` 只暂存或修改单个头部,不会发送响应头,也不能提交状态码;在响应头提交前可反复调用。
|
|
358
|
+
* 本方法会立即提交状态码和此前设置的全部头部,必须在所有 `setHeader()` 调用之后、首次
|
|
359
|
+
* `write()`、`end()` 或 `pipe()` 之前调用。提交后再调用 `setHeader()` 会抛出 `ERR_HTTP_HEADERS_SENT`。
|
|
360
|
+
*
|
|
361
|
+
* 普通控制器应设置 `_httpCode`、调用 `_res.setHeader()` 并直接返回内容,由路由层统一提交响应头。
|
|
362
|
+
* 仅框架内部或手动接管响应(错误、重定向、代理、流式输出等)时才应直接调用本方法。
|
|
363
|
+
*
|
|
356
364
|
* @param res 响应对象
|
|
357
|
-
* @param statusCode 状态码
|
|
358
|
-
* @param headers
|
|
365
|
+
* @param statusCode HTTP 状态码
|
|
366
|
+
* @param headers 随本次提交附加的头部;通常优先在提交前使用 `setHeader()`
|
|
367
|
+
* @returns 无返回值
|
|
359
368
|
*/
|
|
360
369
|
export declare function writeHead(res: http2.Http2ServerResponse | http.ServerResponse, statusCode: number, headers?: http.OutgoingHttpHeaders): void;
|
|
370
|
+
/**
|
|
371
|
+
* --- 提交服务器发送事件(SSE)响应头 ---
|
|
372
|
+
*
|
|
373
|
+
* 在发送第一条事件前调用一次,固定提交状态码 200、事件流内容类型和禁止缓存头部。
|
|
374
|
+
* SSE 的内容长度和结束时间未知,因此不设置 `content-length`;调用后响应头已经提交,
|
|
375
|
+
* 不能再调用 `setHeader()`,后续应使用 `write()` 持续发送事件,并由调用方在结束时关闭响应。
|
|
376
|
+
*
|
|
377
|
+
* @param res 响应对象
|
|
378
|
+
* @returns 无返回值
|
|
379
|
+
*/
|
|
361
380
|
export declare function writeEventStreamHead(res: http2.Http2ServerResponse | http.ServerResponse): void;
|
|
362
381
|
/**
|
|
363
382
|
* --- 向 res 发送数据 ---
|
package/lib/core.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2019-5-3 23:54
|
|
4
|
-
* Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53, 2026-05-20 09:50:00
|
|
4
|
+
* Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53, 2026-05-20 09:50:00, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import * as cp from 'child_process';
|
|
7
7
|
import * as http2 from 'http2';
|
|
@@ -1221,10 +1221,19 @@ export function display(message, ...optionalParams) {
|
|
|
1221
1221
|
console.log(`KE-DISPLAY ${lTime.format(null, 'Y-m-d H:i:s')}`, message, ...optionalParams);
|
|
1222
1222
|
}
|
|
1223
1223
|
/**
|
|
1224
|
-
* ---
|
|
1224
|
+
* --- 提交 HTTP 响应状态和头部,兼容 HTTP/1.1 与 HTTP/2 ---
|
|
1225
|
+
*
|
|
1226
|
+
* `setHeader()` 只暂存或修改单个头部,不会发送响应头,也不能提交状态码;在响应头提交前可反复调用。
|
|
1227
|
+
* 本方法会立即提交状态码和此前设置的全部头部,必须在所有 `setHeader()` 调用之后、首次
|
|
1228
|
+
* `write()`、`end()` 或 `pipe()` 之前调用。提交后再调用 `setHeader()` 会抛出 `ERR_HTTP_HEADERS_SENT`。
|
|
1229
|
+
*
|
|
1230
|
+
* 普通控制器应设置 `_httpCode`、调用 `_res.setHeader()` 并直接返回内容,由路由层统一提交响应头。
|
|
1231
|
+
* 仅框架内部或手动接管响应(错误、重定向、代理、流式输出等)时才应直接调用本方法。
|
|
1232
|
+
*
|
|
1225
1233
|
* @param res 响应对象
|
|
1226
|
-
* @param statusCode 状态码
|
|
1227
|
-
* @param headers
|
|
1234
|
+
* @param statusCode HTTP 状态码
|
|
1235
|
+
* @param headers 随本次提交附加的头部;通常优先在提交前使用 `setHeader()`
|
|
1236
|
+
* @returns 无返回值
|
|
1228
1237
|
*/
|
|
1229
1238
|
export function writeHead(res, statusCode, headers) {
|
|
1230
1239
|
if (res instanceof http2.Http2ServerResponse) {
|
|
@@ -1234,6 +1243,16 @@ export function writeHead(res, statusCode, headers) {
|
|
|
1234
1243
|
res.writeHead(statusCode, headers);
|
|
1235
1244
|
}
|
|
1236
1245
|
}
|
|
1246
|
+
/**
|
|
1247
|
+
* --- 提交服务器发送事件(SSE)响应头 ---
|
|
1248
|
+
*
|
|
1249
|
+
* 在发送第一条事件前调用一次,固定提交状态码 200、事件流内容类型和禁止缓存头部。
|
|
1250
|
+
* SSE 的内容长度和结束时间未知,因此不设置 `content-length`;调用后响应头已经提交,
|
|
1251
|
+
* 不能再调用 `setHeader()`,后续应使用 `write()` 持续发送事件,并由调用方在结束时关闭响应。
|
|
1252
|
+
*
|
|
1253
|
+
* @param res 响应对象
|
|
1254
|
+
* @returns 无返回值
|
|
1255
|
+
*/
|
|
1237
1256
|
export function writeEventStreamHead(res) {
|
|
1238
1257
|
writeHead(res, 200, {
|
|
1239
1258
|
'content-type': 'text/event-stream; charset=utf-8',
|
package/lib/db/conn.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as lCore from '#kebab/lib/core.js';
|
|
|
3
3
|
import * as lTime from '#kebab/lib/time.js';
|
|
4
4
|
import * as lDb from '#kebab/lib/db.js';
|
|
5
5
|
import * as lSqlValue from '#kebab/lib/sql/value.js';
|
|
6
|
+
import * as lText from '#kebab/lib/text.js';
|
|
6
7
|
// --- 注册解析器 ---
|
|
7
8
|
// --- pg 库对以下类型默认返回 string,为与 MySQL 行为保持一致,在此注册解析器转为 JS 原生类型 ---
|
|
8
9
|
// --- POLYGON: 返回如 ((1,1),(2,2)) 的字符串,解析为 {x, y}[] ---
|
|
@@ -301,11 +302,16 @@ export class Connection {
|
|
|
301
302
|
}
|
|
302
303
|
return true;
|
|
303
304
|
}
|
|
304
|
-
catch {
|
|
305
|
+
catch (e) {
|
|
306
|
+
this._transaction = false;
|
|
307
|
+
this._using = false;
|
|
308
|
+
this._lost = true;
|
|
309
|
+
lCore.log({}, '[DB][Connection][beginTransaction] ' + lText.stringifyError(e), '-error');
|
|
305
310
|
return false;
|
|
306
311
|
}
|
|
307
312
|
}
|
|
308
313
|
else {
|
|
314
|
+
lCore.log({}, '[DB][Connection][beginTransaction] connection is not in use', '-error');
|
|
309
315
|
return false;
|
|
310
316
|
}
|
|
311
317
|
}
|
|
@@ -322,7 +328,9 @@ export class Connection {
|
|
|
322
328
|
this._using = false;
|
|
323
329
|
return true;
|
|
324
330
|
}
|
|
325
|
-
catch {
|
|
331
|
+
catch (e) {
|
|
332
|
+
this._lost = true;
|
|
333
|
+
lCore.log({}, '[DB][Connection][commit] ' + lText.stringifyError(e), '-error');
|
|
326
334
|
return false;
|
|
327
335
|
}
|
|
328
336
|
}
|
|
@@ -339,7 +347,9 @@ export class Connection {
|
|
|
339
347
|
this._using = false;
|
|
340
348
|
return true;
|
|
341
349
|
}
|
|
342
|
-
catch {
|
|
350
|
+
catch (e) {
|
|
351
|
+
this._lost = true;
|
|
352
|
+
lCore.log({}, '[DB][Connection][rollback] ' + lText.stringifyError(e), '-error');
|
|
343
353
|
return false;
|
|
344
354
|
}
|
|
345
355
|
}
|
package/lib/db/pool.js
CHANGED
|
@@ -8,6 +8,10 @@ import { Connection } from './conn.js';
|
|
|
8
8
|
import { Transaction } from './tran.js';
|
|
9
9
|
/** --- 连接列表池 --- */
|
|
10
10
|
const connections = [];
|
|
11
|
+
/** --- 开启事务最大尝试次数:首次尝试加一次换连接重试 --- */
|
|
12
|
+
const BEGIN_TRANSACTION_MAX_ATTEMPTS = 2;
|
|
13
|
+
/** --- 创建连接最大尝试次数:首次尝试加两次瞬时故障重试 --- */
|
|
14
|
+
const CREATE_CONNECTION_MAX_ATTEMPTS = 3;
|
|
11
15
|
/**
|
|
12
16
|
* --- 获取当前连接池中所有连接的信息 ---
|
|
13
17
|
*/
|
|
@@ -157,14 +161,19 @@ export class Pool {
|
|
|
157
161
|
* --- 开启事务,返回事务对象并锁定连接,别人任何人不可用,有 ctr 的话必传 this,独立执行时可传 null ---
|
|
158
162
|
*/
|
|
159
163
|
async beginTransaction(ctr) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
for (let i = 0; i < BEGIN_TRANSACTION_MAX_ATTEMPTS; ++i) {
|
|
165
|
+
const conn = await this._getConnection();
|
|
166
|
+
if (!conn) {
|
|
167
|
+
lCore.log(ctr ?? {}, `[DB][Pool][beginTransaction] failed to get connection, service: ${lDb.ESERVICE[this._service]}, database: ${this._etc.name ?? ''}`, '-error');
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
if (!await conn.beginTransaction()) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
return new Transaction(ctr, conn);
|
|
166
174
|
}
|
|
167
|
-
|
|
175
|
+
lCore.log(ctr ?? {}, `[DB][Pool][beginTransaction] failed after retry, service: ${lDb.ESERVICE[this._service]}, database: ${this._etc.name ?? ''}`, '-error');
|
|
176
|
+
return null;
|
|
168
177
|
}
|
|
169
178
|
/**
|
|
170
179
|
* --- 获取一个连接,自动变为 using 状态,;连接失败会返回 null ---
|
|
@@ -192,7 +201,7 @@ export class Pool {
|
|
|
192
201
|
}
|
|
193
202
|
if (!conn) {
|
|
194
203
|
// --- 没有找到合适的连接,创建一个 ---
|
|
195
|
-
loop: for (let i = 0; i <
|
|
204
|
+
loop: for (let i = 0; i < CREATE_CONNECTION_MAX_ATTEMPTS; ++i) {
|
|
196
205
|
try {
|
|
197
206
|
switch (this._service) {
|
|
198
207
|
case lDb.ESERVICE.MYSQL: {
|
|
@@ -267,12 +276,13 @@ export class Pool {
|
|
|
267
276
|
}
|
|
268
277
|
}
|
|
269
278
|
catch (err) {
|
|
270
|
-
|
|
271
|
-
|
|
279
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
280
|
+
const transient = ['ETIMEOUT', 'EHOSTUNREACH', 'ECONNREFUSED'].some(code => message.includes(code));
|
|
281
|
+
if (transient && i < CREATE_CONNECTION_MAX_ATTEMPTS - 1) {
|
|
272
282
|
await lCore.sleep(300);
|
|
273
283
|
continue;
|
|
274
284
|
}
|
|
275
|
-
const msg = `[DB][_getConnection][${lDb.ESERVICE[this._service]}] ${
|
|
285
|
+
const msg = `[DB][_getConnection][${lDb.ESERVICE[this._service]}] ${message}(${this._etc.host}:${this._etc.port})`;
|
|
276
286
|
lCore.debug(msg);
|
|
277
287
|
lCore.log({}, msg, '-error');
|
|
278
288
|
break;
|
package/lib/text.d.ts
CHANGED
|
@@ -170,6 +170,12 @@ export declare function parseJson<T>(str: string): T | false;
|
|
|
170
170
|
* @param space 美化方式
|
|
171
171
|
*/
|
|
172
172
|
export declare function stringifyJson(obj: kebab.Json, space?: string | number): string;
|
|
173
|
+
/**
|
|
174
|
+
* --- 将未知异常转换为适合单行日志的文本,Error 优先返回完整堆栈 ---
|
|
175
|
+
* @param error 异常对象
|
|
176
|
+
* @returns 已转义的单行错误文本
|
|
177
|
+
*/
|
|
178
|
+
export declare function stringifyError(error: unknown): string;
|
|
173
179
|
/**
|
|
174
180
|
* --- 输出文本格式的 buffer ---
|
|
175
181
|
* @param buf 原始 buffer
|
package/lib/text.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2019-5-15 16:49:39
|
|
4
|
-
* Last: 2020-04-06 20:51:06, 2022-9-29 15:18:16, 2022-12-29 00:01:30, 2024-3-6 17:53:14, 2024-5-31 17:29:52, 2025-6-13 15:47:02, 2025-9-23 12:51:49
|
|
4
|
+
* Last: 2020-04-06 20:51:06, 2022-9-29 15:18:16, 2022-12-29 00:01:30, 2024-3-6 17:53:14, 2024-5-31 17:29:52, 2025-6-13 15:47:02, 2025-9-23 12:51:49, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import * as net from 'net';
|
|
7
7
|
import * as kebab from '#kebab/index.js';
|
|
@@ -624,6 +624,29 @@ export function stringifyJson(obj, space) {
|
|
|
624
624
|
return v;
|
|
625
625
|
}, space).replace(/"-mybigint-([-+0-9]+?)"/g, '$1');
|
|
626
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* --- 将未知异常转换为适合单行日志的文本,Error 优先返回完整堆栈 ---
|
|
629
|
+
* @param error 异常对象
|
|
630
|
+
* @returns 已转义的单行错误文本
|
|
631
|
+
*/
|
|
632
|
+
export function stringifyError(error) {
|
|
633
|
+
let message;
|
|
634
|
+
if (error instanceof Error) {
|
|
635
|
+
message = error.stack ?? error.message;
|
|
636
|
+
}
|
|
637
|
+
else if (typeof error === 'string') {
|
|
638
|
+
message = error;
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
try {
|
|
642
|
+
message = stringifyJson(error);
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
message = String(error);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return stringifyJson(message).slice(1, -1);
|
|
649
|
+
}
|
|
627
650
|
/**
|
|
628
651
|
* --- 输出文本格式的 buffer ---
|
|
629
652
|
* @param buf 原始 buffer
|
package/lib/undici/request.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2020-4-9 20:02:39
|
|
4
|
-
* Last: 2020-4-9 20:47:58, 2022-09-10 01:35:34, 2025-9-23 12:41:58
|
|
4
|
+
* Last: 2020-4-9 20:47:58, 2022-09-10 01:35:34, 2025-9-23 12:41:58, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import * as stream from 'stream';
|
|
7
7
|
import * as lCookie from '#kebab/lib/cookie.js';
|
|
@@ -47,6 +47,11 @@ export declare class Request {
|
|
|
47
47
|
* @param timeout 秒
|
|
48
48
|
*/
|
|
49
49
|
timeout(timeout: number): this;
|
|
50
|
+
/**
|
|
51
|
+
* --- 设置网络异常后的重试次数 ---
|
|
52
|
+
* @param retry 重试次数,默认为 1;非幂等请求需由调用方保证安全
|
|
53
|
+
*/
|
|
54
|
+
retry(retry?: number): this;
|
|
50
55
|
/**
|
|
51
56
|
* --- 设置是否跟随请求方的 location,留空为跟随,不设置为不跟随 ---
|
|
52
57
|
* @param follow
|
package/lib/undici/request.js
CHANGED
|
@@ -59,6 +59,14 @@ export class Request {
|
|
|
59
59
|
this._opt['timeout'] = timeout;
|
|
60
60
|
return this;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* --- 设置网络异常后的重试次数 ---
|
|
64
|
+
* @param retry 重试次数,默认为 1;非幂等请求需由调用方保证安全
|
|
65
|
+
*/
|
|
66
|
+
retry(retry = 1) {
|
|
67
|
+
this._opt['retry'] = retry;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
62
70
|
/**
|
|
63
71
|
* --- 设置是否跟随请求方的 location,留空为跟随,不设置为不跟随 ---
|
|
64
72
|
* @param follow
|
package/lib/undici/response.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2020-4-9 15:33:06
|
|
4
|
-
* Last: 2020-4-12 11:12:03, 2022-09-10 12:43:23, 2022-12-25 15:12:57, 2023-9-26 14:20:41
|
|
4
|
+
* Last: 2020-4-12 11:12:03, 2022-09-10 12:43:23, 2022-12-25 15:12:57, 2023-9-26 14:20:41, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import * as undici from 'undici';
|
|
7
7
|
import * as zlib from 'zlib';
|
|
@@ -31,7 +31,7 @@ export declare class Response {
|
|
|
31
31
|
/**
|
|
32
32
|
* --- 获取响应读取流对象 ---
|
|
33
33
|
*/
|
|
34
|
-
getStream(): (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) |
|
|
34
|
+
getStream(): zlib.BrotliDecompress | zlib.Gunzip | zlib.Inflate | (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | null;
|
|
35
35
|
/**
|
|
36
36
|
* --- 获取原生响应读取流对象 ---
|
|
37
37
|
*/
|
package/lib/undici/response.js
CHANGED
|
@@ -28,7 +28,8 @@ export class Response {
|
|
|
28
28
|
return this._req ? await lBuffer.getFull(stream) : null;
|
|
29
29
|
}
|
|
30
30
|
catch (e) {
|
|
31
|
-
|
|
31
|
+
this.error = e instanceof Error ? e : new Error(String(e));
|
|
32
|
+
lCore.log({}, '[Undici][Response][getContent] ' + lText.stringifyError(e), '-error');
|
|
32
33
|
return null;
|
|
33
34
|
}
|
|
34
35
|
}
|
package/lib/undici.d.ts
CHANGED
|
@@ -36,27 +36,28 @@ export declare function post(u: string, data: Record<string, kebab.Json> | Buffe
|
|
|
36
36
|
*/
|
|
37
37
|
export declare function postJson(u: string, data: kebab.Json[] | Record<string, kebab.Json>, opt?: IRequestOptions): Promise<lResponse.Response>;
|
|
38
38
|
/**
|
|
39
|
-
* --- 发起 JSON 请求并解析 JSON
|
|
39
|
+
* --- 发起 JSON 请求并解析 JSON 响应 ---
|
|
40
40
|
* @param u 网址
|
|
41
41
|
* @param data 数据
|
|
42
42
|
* @param opt 选项
|
|
43
|
+
* @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
|
|
43
44
|
*/
|
|
44
|
-
export declare function postJsonResponseJson(u: string, data: kebab.Json[] | Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null>;
|
|
45
|
+
export declare function postJsonResponseJson(u: string, data: kebab.Json[] | Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null | false>;
|
|
45
46
|
/**
|
|
46
47
|
* --- 发起 POST 请求并解析 JSON 响应 ---
|
|
47
48
|
* @param u 网址
|
|
48
49
|
* @param data 数据
|
|
49
50
|
* @param opt 选项
|
|
50
|
-
* @returns JSON
|
|
51
|
+
* @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
|
|
51
52
|
*/
|
|
52
|
-
export declare function postResponseJson(u: string, data: Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null>;
|
|
53
|
+
export declare function postResponseJson(u: string, data: Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null | false>;
|
|
53
54
|
/**
|
|
54
55
|
* --- 发起 GET 请求并解析 JSON 响应 ---
|
|
55
56
|
* @param u 网址
|
|
56
57
|
* @param opt 选项
|
|
57
|
-
* @returns JSON
|
|
58
|
+
* @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
|
|
58
59
|
*/
|
|
59
|
-
export declare function getResponseJson(u: string, opt?: IRequestOptions): Promise<kebab.Json | null>;
|
|
60
|
+
export declare function getResponseJson(u: string, opt?: IRequestOptions): Promise<kebab.Json | null | false>;
|
|
60
61
|
/**
|
|
61
62
|
* --- 发起一个完全兼容 fetch 的请求 ---
|
|
62
63
|
* @param input 请求的 URL 或 Request 对象
|
|
@@ -71,6 +72,8 @@ export declare function fetch(input: string | URL | Request, init?: RequestInit
|
|
|
71
72
|
};
|
|
72
73
|
/** --- 自定义 host 映射,如 {'www.maiyun.net': '127.0.0.1'},或全部映射到一个 host --- */
|
|
73
74
|
'hosts'?: Record<string, string> | string;
|
|
75
|
+
/** --- 网络异常后的重试次数,默认 0;流式请求体不可重试,非幂等请求需由调用方保证安全 --- */
|
|
76
|
+
'retry'?: number;
|
|
74
77
|
}): Promise<Response>;
|
|
75
78
|
/**
|
|
76
79
|
* --- 发起一个请求 ---
|
|
@@ -126,6 +129,10 @@ export interface IRequestOptions {
|
|
|
126
129
|
'type'?: 'form' | 'json';
|
|
127
130
|
/** --- 秒数,默认 300 秒 --- */
|
|
128
131
|
'timeout'?: number;
|
|
132
|
+
/** --- 网络异常后的重试次数,默认 0;流式请求体不可重试,非幂等请求需由调用方保证安全 --- */
|
|
133
|
+
'retry'?: number;
|
|
134
|
+
/** --- JSON 解析失败后的重试次数,默认 0;仅适用于 ResponseJson 快捷方法,非幂等请求需由调用方保证安全 --- */
|
|
135
|
+
'retryJson'?: number;
|
|
129
136
|
/** --- 追踪 location 次数,0 为不追踪,默认为 0 --- */
|
|
130
137
|
'follow'?: number;
|
|
131
138
|
/** --- 自定义 host 映射,如 {'www.maiyun.net': '127.0.0.1'},或全部映射到一个 host --- */
|
package/lib/undici.js
CHANGED
|
@@ -26,6 +26,25 @@ function getMproxyUrl(u, mproxy) {
|
|
|
26
26
|
}
|
|
27
27
|
/** --- 复用的 undici.agent 对象列表 --- */
|
|
28
28
|
const agents = new Map();
|
|
29
|
+
/** --- 可重试的网络异常代码 --- */
|
|
30
|
+
const retryErrorCodes = [
|
|
31
|
+
'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'ENETDOWN', 'ENETUNREACH',
|
|
32
|
+
'EHOSTDOWN', 'EHOSTUNREACH', 'EPIPE', 'ETIMEDOUT', 'UND_ERR_SOCKET',
|
|
33
|
+
'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT'
|
|
34
|
+
];
|
|
35
|
+
/** --- JSON 解析重试的初始等待时间,单位毫秒 --- */
|
|
36
|
+
const RETRY_JSON_MIN_TIMEOUT = 500;
|
|
37
|
+
/** --- JSON 解析重试的最大等待时间,单位毫秒 --- */
|
|
38
|
+
const RETRY_JSON_MAX_TIMEOUT = 30_000;
|
|
39
|
+
/**
|
|
40
|
+
* --- 将重试次数规范为非负整数 ---
|
|
41
|
+
* @param retry 重试次数
|
|
42
|
+
* @returns 可执行的重试次数
|
|
43
|
+
*/
|
|
44
|
+
function normalizeRetry(retry) {
|
|
45
|
+
return (typeof retry === 'number') && Number.isFinite(retry) ?
|
|
46
|
+
Math.max(0, Math.floor(retry)) : 0;
|
|
47
|
+
}
|
|
29
48
|
/** --- 获取或创建 undici.agent 对象 --- */
|
|
30
49
|
function getAgent(opt = {}) {
|
|
31
50
|
let k = opt.reuse ?? 'default';
|
|
@@ -59,6 +78,26 @@ function getAgent(opt = {}) {
|
|
|
59
78
|
}
|
|
60
79
|
return agents.get(k);
|
|
61
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* --- 获取请求使用的 dispatcher ---
|
|
83
|
+
* @param opt 请求选项
|
|
84
|
+
* @param method 请求方法
|
|
85
|
+
* @param data 请求数据
|
|
86
|
+
* @returns 普通或带网络异常重试能力的 dispatcher
|
|
87
|
+
*/
|
|
88
|
+
function getDispatcher(opt, method, data) {
|
|
89
|
+
const agent = getAgent(opt);
|
|
90
|
+
const retry = normalizeRetry(opt.retry);
|
|
91
|
+
if ((retry === 0) || (data instanceof stream.Readable)) {
|
|
92
|
+
return agent;
|
|
93
|
+
}
|
|
94
|
+
return new undici.RetryAgent(agent, {
|
|
95
|
+
'maxRetries': retry,
|
|
96
|
+
'methods': [method],
|
|
97
|
+
'statusCodes': [],
|
|
98
|
+
'errorCodes': retryErrorCodes,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
62
101
|
/**
|
|
63
102
|
* --- 创建一个请求对象 ---
|
|
64
103
|
* @param u
|
|
@@ -96,62 +135,70 @@ export async function postJson(u, data, opt = {}) {
|
|
|
96
135
|
return request(u, data, opt);
|
|
97
136
|
}
|
|
98
137
|
/**
|
|
99
|
-
* ---
|
|
138
|
+
* --- 发起请求并解析 JSON 响应,可在解析失败时重新请求 ---
|
|
139
|
+
* @param u 网址
|
|
140
|
+
* @param data 请求数据
|
|
141
|
+
* @param opt 请求选项
|
|
142
|
+
* @param action 日志动作名
|
|
143
|
+
* @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
|
|
144
|
+
*/
|
|
145
|
+
async function requestResponseJson(u, data, opt, action) {
|
|
146
|
+
const retryJson = normalizeRetry(opt.retryJson);
|
|
147
|
+
for (let i = 0; i <= retryJson; ++i) {
|
|
148
|
+
const res = await request(u, data, opt);
|
|
149
|
+
const rtn = await res.getContent();
|
|
150
|
+
if (!rtn) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const rtnStr = rtn.toString();
|
|
154
|
+
const json = lText.parseJson(rtnStr);
|
|
155
|
+
if (json) {
|
|
156
|
+
return json;
|
|
157
|
+
}
|
|
158
|
+
if (i < retryJson) {
|
|
159
|
+
const timeout = Math.min(RETRY_JSON_MIN_TIMEOUT * (2 ** i), RETRY_JSON_MAX_TIMEOUT);
|
|
160
|
+
await lCore.sleep(timeout);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (opt.log === undefined || opt.log) {
|
|
164
|
+
const requestData = data === undefined ? '' : `, data: ${lText.stringifyJson(data)}`;
|
|
165
|
+
lCore.log({}, `[UNDICI][${action}] parse json failed, url: ${u}${requestData}, content: ${rtnStr}`, '-neterror');
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* --- 发起 JSON 请求并解析 JSON 响应 ---
|
|
100
173
|
* @param u 网址
|
|
101
174
|
* @param data 数据
|
|
102
175
|
* @param opt 选项
|
|
176
|
+
* @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
|
|
103
177
|
*/
|
|
104
178
|
export async function postJsonResponseJson(u, data, opt = {}) {
|
|
105
179
|
opt.method = 'POST';
|
|
106
180
|
opt.type = 'json';
|
|
107
|
-
|
|
108
|
-
const rtn = await res.getContent();
|
|
109
|
-
if (!rtn) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
const json = lText.parseJson(rtn.toString());
|
|
113
|
-
if (!json) {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
return json;
|
|
181
|
+
return requestResponseJson(u, data, opt, 'POSTJSONRESPONSEJSON');
|
|
117
182
|
}
|
|
118
183
|
/**
|
|
119
184
|
* --- 发起 POST 请求并解析 JSON 响应 ---
|
|
120
185
|
* @param u 网址
|
|
121
186
|
* @param data 数据
|
|
122
187
|
* @param opt 选项
|
|
123
|
-
* @returns JSON
|
|
188
|
+
* @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
|
|
124
189
|
*/
|
|
125
190
|
export async function postResponseJson(u, data, opt = {}) {
|
|
126
191
|
opt.method = 'POST';
|
|
127
|
-
|
|
128
|
-
const rtn = await res.getContent();
|
|
129
|
-
if (!rtn) {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
const json = lText.parseJson(rtn.toString());
|
|
133
|
-
if (!json) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
return json;
|
|
192
|
+
return requestResponseJson(u, data, opt, 'POSTRESPONSEJSON');
|
|
137
193
|
}
|
|
138
194
|
/**
|
|
139
195
|
* --- 发起 GET 请求并解析 JSON 响应 ---
|
|
140
196
|
* @param u 网址
|
|
141
197
|
* @param opt 选项
|
|
142
|
-
* @returns JSON
|
|
198
|
+
* @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
|
|
143
199
|
*/
|
|
144
200
|
export async function getResponseJson(u, opt = {}) {
|
|
145
|
-
|
|
146
|
-
const rtn = await res.getContent();
|
|
147
|
-
if (!rtn) {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
const json = lText.parseJson(rtn.toString());
|
|
151
|
-
if (!json) {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
return json;
|
|
201
|
+
return requestResponseJson(u, undefined, opt, 'GETRESPONSEJSON');
|
|
155
202
|
}
|
|
156
203
|
/**
|
|
157
204
|
* --- 发起一个完全兼容 fetch 的请求 ---
|
|
@@ -244,6 +291,7 @@ export async function fetch(input, init = {}) {
|
|
|
244
291
|
'headers': headers,
|
|
245
292
|
'hosts': init.hosts,
|
|
246
293
|
'mproxy': init.mproxy,
|
|
294
|
+
'retry': init.retry,
|
|
247
295
|
'signal': init.signal ?? undefined,
|
|
248
296
|
'follow': init.redirect === 'follow' ? 10 : 0,
|
|
249
297
|
};
|
|
@@ -387,7 +435,7 @@ export async function request(u, data, opt = {}) {
|
|
|
387
435
|
}
|
|
388
436
|
return res;
|
|
389
437
|
}
|
|
390
|
-
const
|
|
438
|
+
const dispatcher = getDispatcher(opt, method, data);
|
|
391
439
|
req = await undici.request(opt.mproxy ? getMproxyUrl(u, opt.mproxy) : u, {
|
|
392
440
|
'method': method,
|
|
393
441
|
'body': data,
|
|
@@ -395,7 +443,7 @@ export async function request(u, data, opt = {}) {
|
|
|
395
443
|
'headersTimeout': timeout * 1_000,
|
|
396
444
|
'bodyTimeout': timeout * 1_000,
|
|
397
445
|
'signal': opt.signal,
|
|
398
|
-
'dispatcher':
|
|
446
|
+
'dispatcher': dispatcher,
|
|
399
447
|
});
|
|
400
448
|
}
|
|
401
449
|
catch (err) {
|
package/package.json
CHANGED
package/sys/child.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Project: Kebab, User: JianSuoQiYue
|
|
3
3
|
* Date: 2019-5-3 23:54
|
|
4
|
-
* Last: 2020-3-31 15:01:07, 2020-4-9 22:28:50, 2022-07-22 14:19:46, 2022-9-29 22:11:07, 2023-5-1 18:26:57, 2024-1-12 13:32:00, 2024-3-4 16:49:19, 2026-3-8 16:21:40
|
|
4
|
+
* Last: 2020-3-31 15:01:07, 2020-4-9 22:28:50, 2022-07-22 14:19:46, 2022-9-29 22:11:07, 2023-5-1 18:26:57, 2024-1-12 13:32:00, 2024-3-4 16:49:19, 2026-3-8 16:21:40, 2026-8-22
|
|
5
5
|
*/
|
|
6
6
|
import * as http2 from 'http2';
|
|
7
7
|
import * as tls from 'tls';
|
|
@@ -57,15 +57,18 @@ const http2Sessions = new Set();
|
|
|
57
57
|
* @param method 请求方法
|
|
58
58
|
*/
|
|
59
59
|
function wrapWithLinkCount(key, handler, errorPrefix, method = 'GET') {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
const queryIndex = key.search(/[?#]/u);
|
|
61
|
+
/** --- 查询参数可能含敏感信息,请求计数与诊断仅保留路径 --- */
|
|
62
|
+
const requestKey = queryIndex === -1 ? key : key.slice(0, queryIndex);
|
|
63
|
+
linkCount[requestKey] = (linkCount[requestKey] ?? 0) + 1;
|
|
64
|
+
const trackId = sMonitor.track(requestKey, method);
|
|
65
|
+
Promise.resolve().then(handler).catch((e) => {
|
|
66
|
+
lCore.log({}, `${errorPrefix} ${lText.stringifyError(e)}`, '-error');
|
|
64
67
|
}).finally(() => {
|
|
65
68
|
sMonitor.untrack(trackId);
|
|
66
|
-
--linkCount[
|
|
67
|
-
if (!linkCount[
|
|
68
|
-
delete linkCount[
|
|
69
|
+
--linkCount[requestKey];
|
|
70
|
+
if (!linkCount[requestKey]) {
|
|
71
|
+
delete linkCount[requestKey];
|
|
69
72
|
}
|
|
70
73
|
});
|
|
71
74
|
}
|
|
@@ -295,8 +298,8 @@ async function requestHandler(req, res, https) {
|
|
|
295
298
|
'req': req,
|
|
296
299
|
'get': uri.query ? lText.queryParse(uri.query) : {},
|
|
297
300
|
'cookie': {},
|
|
298
|
-
'headers': {}
|
|
299
|
-
}, '[CHILD][requestHandler][E0]' + lText.
|
|
301
|
+
'headers': {},
|
|
302
|
+
}, '[CHILD][requestHandler][E0]' + lText.stringifyError(e), '-error');
|
|
300
303
|
const content = '<h1>500 Server Error</h1><hr>Kebab';
|
|
301
304
|
if (!res.headersSent) {
|
|
302
305
|
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
@@ -339,7 +342,7 @@ async function requestHandler(req, res, https) {
|
|
|
339
342
|
}
|
|
340
343
|
}
|
|
341
344
|
catch (e) {
|
|
342
|
-
lCore.log({}, '[CHILD][requestHandler][E1]' + lText.
|
|
345
|
+
lCore.log({}, '[CHILD][requestHandler][E1]' + lText.stringifyError(e), '-error');
|
|
343
346
|
const content = '<h1>500 Server Error</h1><hr>Kebab';
|
|
344
347
|
if (!res.headersSent) {
|
|
345
348
|
res.setHeader('content-type', 'text/html; charset=utf-8');
|
|
@@ -618,7 +621,7 @@ process.on('message', function (msg) {
|
|
|
618
621
|
}
|
|
619
622
|
}
|
|
620
623
|
})().catch(function (e) {
|
|
621
|
-
lCore.log({}, '[CHILD][process][message] ' + lText.
|
|
624
|
+
lCore.log({}, '[CHILD][process][message] ' + lText.stringifyError(e), '-error');
|
|
622
625
|
});
|
|
623
626
|
});
|
|
624
627
|
/**
|