xshell 1.3.3 → 1.3.5
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/file.d.ts +1 -1
- package/git.js +1 -1
- package/net.d.ts +21 -4
- package/net.js +86 -31
- package/package.json +8 -8
- package/process.d.ts +4 -4
- package/server.js +42 -32
- package/utils.common.d.ts +12 -15
- package/utils.common.js +103 -95
- package/utils.d.ts +0 -1
- package/utils.js +0 -5
- package/xlint.js +0 -3
package/file.d.ts
CHANGED
package/git.js
CHANGED
package/net.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
1
2
|
import { type Readable } from 'node:stream';
|
|
2
3
|
import type { Cookie, CookieJar, MemoryCookieStore } from 'tough-cookie';
|
|
3
4
|
import type { Encoding } from './file.ts';
|
|
4
|
-
import { inspect } from './utils.ts';
|
|
5
|
+
import { inspect, type Deferred2 } from './utils.ts';
|
|
5
6
|
import { type BasicAuth, type BearerAuth } from './net.common.ts';
|
|
6
7
|
export * from './net.common.ts';
|
|
7
8
|
export declare enum MyProxy {
|
|
@@ -48,7 +49,6 @@ export interface RequestOptions {
|
|
|
48
49
|
retry: boolean;
|
|
49
50
|
timeout: boolean;
|
|
50
51
|
};
|
|
51
|
-
long_connection?: true;
|
|
52
52
|
}
|
|
53
53
|
export interface RequestFullOptions extends RequestOptions {
|
|
54
54
|
full: true;
|
|
@@ -97,8 +97,7 @@ export declare class StatusCodeError extends Error {
|
|
|
97
97
|
- decode?: `true` 根据 content-encoding: br 等解码压缩后的 response.body
|
|
98
98
|
- print?:
|
|
99
99
|
- retry: `true` 是否打印 "等待 1 秒后重试 request" 提示信息
|
|
100
|
-
- timeout: `true` 是否打印最后一次超时重试失败时的错误
|
|
101
|
-
- long_connection?: 用于保持长 tcp 连接,保留 nat 端口映射记录 */
|
|
100
|
+
- timeout: `true` 是否打印最后一次超时重试失败时的错误 */
|
|
102
101
|
export declare function request(url: string | URL): Promise<string>;
|
|
103
102
|
export declare function request(url: string | URL, options: RequestRawOptions): Promise<RawResponse>;
|
|
104
103
|
export declare function request(url: string | URL, options: RequestFullOptions & {
|
|
@@ -111,3 +110,21 @@ export declare function request(url: string | URL, options: RequestOptions & {
|
|
|
111
110
|
export declare function request(url: string | URL, options: RequestOptions): Promise<string>;
|
|
112
111
|
/** 发起 http 请求并将响应体作为 json 解析 */
|
|
113
112
|
export declare function request_json<T = any>(url: string | URL, options?: RequestOptions): Promise<T>;
|
|
113
|
+
export interface ConnectOptions {
|
|
114
|
+
timeout?: number;
|
|
115
|
+
print?: {
|
|
116
|
+
error: boolean;
|
|
117
|
+
connect: boolean;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** 建立 tcp 连接,超时时间为 2 秒 (timeout 选项),返回已连接的 socket
|
|
121
|
+
连接中遇到的错误会 reject,之后取消监听 error 事件,由调用者 attach 处理
|
|
122
|
+
- hostname: 要连接到的 ip 或域名
|
|
123
|
+
- port: 要连接的端口
|
|
124
|
+
- options?:
|
|
125
|
+
- timeout?: `2000` 超时时间
|
|
126
|
+
- print?: 打印连接日志,错误日志
|
|
127
|
+
- error?: `true`
|
|
128
|
+
- connect: `false` */
|
|
129
|
+
export declare function connect(hostname: string, port: number, { timeout: _timeout, print }?: ConnectOptions): Deferred2<net.Socket>;
|
|
130
|
+
export declare function get_socket_ip(socket: net.Socket): string;
|
package/net.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import zlib from 'node:zlib';
|
|
2
|
+
import net from 'node:net';
|
|
2
3
|
import { buffer as stream_to_buffer, text as stream_to_text } from 'node:stream/consumers';
|
|
3
|
-
import { isReadable } from 'node:stream';
|
|
4
|
-
import {
|
|
4
|
+
import { pipeline, isReadable } from 'node:stream';
|
|
5
|
+
import { noop } from "./prototype.js";
|
|
6
|
+
import { inspect, assert, delay, map_values, unique, timeout, check, colored, encode, TimeoutError, set_error_message, defer2 } from "./utils.js";
|
|
5
7
|
import { drop_request_headers } from "./net.common.js";
|
|
6
8
|
export * from "./net.common.js";
|
|
7
9
|
export var MyProxy;
|
|
@@ -32,27 +34,25 @@ async function request_retry(url, options, _timeout, retries = 0, count = 0, pri
|
|
|
32
34
|
// 设置给 undici 设置 timeout, signal 不一定管用,还是得自己兜底
|
|
33
35
|
options.signal = AbortSignal.timeout(_timeout);
|
|
34
36
|
return await timeout(_timeout + 300, // 为 undici 兜底
|
|
35
|
-
undici.request(url, options), undefined, print.timeout && count >= retries // 只打印最后一次超时的错误,避免太多冗余输出
|
|
36
|
-
);
|
|
37
|
+
undici.request(url, options), undefined, print.timeout && count >= retries); // 只打印最后一次超时的错误,避免太多冗余输出
|
|
37
38
|
}
|
|
38
39
|
else
|
|
39
40
|
return await undici.request(url, options);
|
|
40
41
|
}
|
|
41
42
|
catch (error) {
|
|
42
|
-
if (error.name
|
|
43
|
-
if (count < retries) {
|
|
44
|
-
const duration = 2 ** count;
|
|
45
|
-
if (print.retry)
|
|
46
|
-
console.log(`${`等待 ${duration} 秒后重试请求 (已尝试 ${count + 1} 次) ··`.yellow} ${url.toString().blue.underline}`);
|
|
47
|
-
await delay(1000 * duration);
|
|
48
|
-
return request_retry(url, options, _timeout, retries, count + 1, print);
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
const seconds = _timeout / 1000;
|
|
52
|
-
throw Object.assign(new Error(`请求超过 ${seconds.toFixed(seconds < 1 ? 1 : 0)} 秒等待时间: ${url.toString()}`), { name: 'TimeoutError' });
|
|
53
|
-
}
|
|
54
|
-
else
|
|
43
|
+
if (error.name !== 'TimeoutError')
|
|
55
44
|
throw error;
|
|
45
|
+
// 重试次数内
|
|
46
|
+
if (count < retries) {
|
|
47
|
+
const duration = 2 ** count;
|
|
48
|
+
if (print.retry)
|
|
49
|
+
console.log(`等待 ${duration} 秒后重试请求 (已尝试 ${count + 1} 次) ··`.yellow + ' ' +
|
|
50
|
+
url.toString().blue.underline);
|
|
51
|
+
await delay(1000 * duration);
|
|
52
|
+
return request_retry(url, options, _timeout, retries, count + 1, print);
|
|
53
|
+
}
|
|
54
|
+
const seconds = _timeout / 1000;
|
|
55
|
+
throw new TimeoutError(`请求超过 ${seconds.toFixed(seconds < 1 ? 1 : 0)} 秒等待时间: ${url.toString()}`);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
export class StatusCodeError extends Error {
|
|
@@ -71,7 +71,7 @@ export async function request(url, options = {}) {
|
|
|
71
71
|
const { queries, headers: _headers, body, type = 'application/json', timeout = 5 * 1000, auth, cookies: _cookies, raw = false, full = false, redirect = 'follow', decode = true, print = {
|
|
72
72
|
timeout: true,
|
|
73
73
|
retry: true
|
|
74
|
-
}
|
|
74
|
+
} } = options;
|
|
75
75
|
let { method, retries, encoding, proxy, } = options;
|
|
76
76
|
url = new URL(url);
|
|
77
77
|
if (queries)
|
|
@@ -128,7 +128,7 @@ export async function request(url, options = {}) {
|
|
|
128
128
|
proxy = MyProxy.socks5;
|
|
129
129
|
let undici_options = {
|
|
130
130
|
...method ? { method } : {},
|
|
131
|
-
dispatcher: get_dispatcher(proxy, redirect
|
|
131
|
+
dispatcher: get_dispatcher(proxy, redirect),
|
|
132
132
|
// 下面这些 timeout 都不是总的时间
|
|
133
133
|
headersTimeout: timeout,
|
|
134
134
|
// 从收完 headers 开始算
|
|
@@ -155,30 +155,26 @@ export async function request(url, options = {}) {
|
|
|
155
155
|
switch (content_encoding) {
|
|
156
156
|
case 'gzip':
|
|
157
157
|
case 'x-gzip':
|
|
158
|
-
body =
|
|
158
|
+
body = pipeline(_body, zlib.createGunzip({
|
|
159
159
|
// Be less strict when decoding compressed responses, since sometimes
|
|
160
160
|
// servers send slightly invalid responses that are still accepted
|
|
161
161
|
// by common browsers.
|
|
162
162
|
// Always using Z_SYNC_FLUSH is what cURL does.
|
|
163
163
|
flush: zlib.constants.Z_SYNC_FLUSH,
|
|
164
164
|
finishFlush: zlib.constants.Z_SYNC_FLUSH
|
|
165
|
-
}));
|
|
165
|
+
}), noop);
|
|
166
166
|
break;
|
|
167
167
|
case 'deflate':
|
|
168
|
-
body =
|
|
168
|
+
body = pipeline(_body, zlib.createInflate(), noop);
|
|
169
169
|
break;
|
|
170
170
|
case 'br':
|
|
171
|
-
body =
|
|
171
|
+
body = pipeline(_body, zlib.createBrotliDecompress(), noop);
|
|
172
172
|
break;
|
|
173
173
|
default:
|
|
174
174
|
throw new Error(`不支持 content-encoding: ${content_encoding.quote()} 的 http 请求`);
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
-
response = {
|
|
178
|
-
status,
|
|
179
|
-
headers,
|
|
180
|
-
body
|
|
181
|
-
};
|
|
177
|
+
response = { status, headers, body };
|
|
182
178
|
if (!((200 <= status && status <= 299) || status === 304 || (redirect === 'manual' && 300 <= status && status < 400)))
|
|
183
179
|
throw new StatusCodeError(status, url.toString());
|
|
184
180
|
}
|
|
@@ -234,8 +230,8 @@ export async function request(url, options = {}) {
|
|
|
234
230
|
:
|
|
235
231
|
body_;
|
|
236
232
|
}
|
|
237
|
-
function get_dispatcher(proxy, redirect
|
|
238
|
-
const key = `${proxy ||
|
|
233
|
+
function get_dispatcher(proxy, redirect) {
|
|
234
|
+
const key = `${proxy || 'direct'}.${redirect}`;
|
|
239
235
|
let dispatcher = dispatchers[key];
|
|
240
236
|
if (dispatcher)
|
|
241
237
|
return dispatcher;
|
|
@@ -243,7 +239,7 @@ function get_dispatcher(proxy, redirect, long_connection) {
|
|
|
243
239
|
// @ts-ignore
|
|
244
240
|
new undici.ProxyAgent({ uri: proxy })
|
|
245
241
|
:
|
|
246
|
-
new undici.Agent({
|
|
242
|
+
new undici.Agent({ allowH2: true });
|
|
247
243
|
if (redirect === 'follow')
|
|
248
244
|
dispatcher = dispatcher.compose(
|
|
249
245
|
// todo: 强制手动处理重定向,来正确处理 cookie ?
|
|
@@ -297,4 +293,63 @@ export async function request_json(url, options) {
|
|
|
297
293
|
throw error;
|
|
298
294
|
}
|
|
299
295
|
}
|
|
296
|
+
/** 建立 tcp 连接,超时时间为 2 秒 (timeout 选项),返回已连接的 socket
|
|
297
|
+
连接中遇到的错误会 reject,之后取消监听 error 事件,由调用者 attach 处理
|
|
298
|
+
- hostname: 要连接到的 ip 或域名
|
|
299
|
+
- port: 要连接的端口
|
|
300
|
+
- options?:
|
|
301
|
+
- timeout?: `2000` 超时时间
|
|
302
|
+
- print?: 打印连接日志,错误日志
|
|
303
|
+
- error?: `true`
|
|
304
|
+
- connect: `false` */
|
|
305
|
+
export function connect(hostname, port, { timeout: _timeout = 2000, print = { error: true, connect: false } } = {}) {
|
|
306
|
+
let pconnected = defer2();
|
|
307
|
+
let timeouted = false;
|
|
308
|
+
function get_message(message) {
|
|
309
|
+
return `tcp://${hostname}:${port} ${message}`;
|
|
310
|
+
}
|
|
311
|
+
let socket = net.connect({
|
|
312
|
+
host: hostname,
|
|
313
|
+
port,
|
|
314
|
+
noDelay: true
|
|
315
|
+
}, () => {
|
|
316
|
+
if (pconnected.settled) {
|
|
317
|
+
if (print)
|
|
318
|
+
console.warn(get_message('连接建立了,但是已超时'.yellow));
|
|
319
|
+
socket.resetAndDestroy();
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
clearTimeout(timeout);
|
|
323
|
+
socket.off('error', on_error);
|
|
324
|
+
if (print.connect)
|
|
325
|
+
console.log(get_message('已连接'));
|
|
326
|
+
pconnected.resolve(socket);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
const on_error = (error) => {
|
|
330
|
+
if (pconnected.settled) {
|
|
331
|
+
// 超时后可能会走到这里
|
|
332
|
+
if (timeouted && error.code === 'ERR_SOCKET_CLOSED')
|
|
333
|
+
return;
|
|
334
|
+
// 待观察:不太可能走到这里
|
|
335
|
+
if (print.error)
|
|
336
|
+
console.error(get_message(error.message.red));
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
clearTimeout(timeout);
|
|
340
|
+
pconnected.reject(set_error_message(error, get_message(error.message)));
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
socket.once('error', on_error);
|
|
344
|
+
let timeout = setTimeout(() => {
|
|
345
|
+
// 执行到这里一定没有 settled
|
|
346
|
+
timeouted = true;
|
|
347
|
+
socket.resetAndDestroy();
|
|
348
|
+
pconnected.reject(new TimeoutError(get_message('超时了')));
|
|
349
|
+
}, _timeout);
|
|
350
|
+
return pconnected;
|
|
351
|
+
}
|
|
352
|
+
export function get_socket_ip(socket) {
|
|
353
|
+
return socket.remoteAddress.strip_if_start('::ffff:');
|
|
354
|
+
}
|
|
300
355
|
//# sourceMappingURL=net.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xshell",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"bin": {
|
|
@@ -56,19 +56,19 @@
|
|
|
56
56
|
"@stylistic/eslint-plugin": "^5.5.0",
|
|
57
57
|
"@svgr/webpack": "^8.1.0",
|
|
58
58
|
"@types/sass-loader": "^8.0.10",
|
|
59
|
-
"@typescript-eslint/eslint-plugin": "^8.46.
|
|
60
|
-
"@typescript-eslint/parser": "^8.46.
|
|
61
|
-
"@typescript-eslint/utils": "^8.46.
|
|
59
|
+
"@typescript-eslint/eslint-plugin": "^8.46.3",
|
|
60
|
+
"@typescript-eslint/parser": "^8.46.3",
|
|
61
|
+
"@typescript-eslint/utils": "^8.46.3",
|
|
62
62
|
"archiver": "^7.0.1",
|
|
63
63
|
"chalk": "^5.6.2",
|
|
64
64
|
"commander": "^14.0.2",
|
|
65
65
|
"css-loader": "^7.1.2",
|
|
66
66
|
"emoji-regex": "^10.6.0",
|
|
67
|
-
"eslint": "^9.39.
|
|
67
|
+
"eslint": "^9.39.1",
|
|
68
68
|
"eslint-plugin-import": "^2.32.0",
|
|
69
69
|
"eslint-plugin-react": "^7.37.5",
|
|
70
70
|
"https-proxy-agent": "^7.0.6",
|
|
71
|
-
"i18next": "^25.6.
|
|
71
|
+
"i18next": "^25.6.1",
|
|
72
72
|
"i18next-scanner": "^4.6.0",
|
|
73
73
|
"koa": "^3.1.1",
|
|
74
74
|
"koa-compress": "^5.1.1",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"mime-types": "^3.0.1",
|
|
77
77
|
"p-map": "^7.0.3",
|
|
78
78
|
"react": "^19.2.0",
|
|
79
|
-
"react-i18next": "^16.2.
|
|
79
|
+
"react-i18next": "^16.2.4",
|
|
80
80
|
"resolve-path": "^1.4.0",
|
|
81
81
|
"sass": "^1.93.3",
|
|
82
82
|
"sass-loader": "^16.0.6",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"@types/koa": "^3.0.1",
|
|
102
102
|
"@types/koa-compress": "^4.0.7",
|
|
103
103
|
"@types/mime-types": "^3.0.1",
|
|
104
|
-
"@types/node": "^24.
|
|
104
|
+
"@types/node": "^24.10.0",
|
|
105
105
|
"@types/react": "^19.2.2",
|
|
106
106
|
"@types/tough-cookie": "^4.0.5",
|
|
107
107
|
"@types/vscode": "^1.105.0",
|
package/process.d.ts
CHANGED
|
@@ -31,10 +31,10 @@ interface BaseOptions {
|
|
|
31
31
|
envs?: Record<string, string>;
|
|
32
32
|
/** 创建子进程时添加 http_proxy, https_proxy, no_proxy 环境变量以启用代理 */
|
|
33
33
|
proxy?: MyProxy | true;
|
|
34
|
-
/** 控制子进程 stdin,默认为 `
|
|
34
|
+
/** 控制子进程 stdin,默认为 `Boolean(input)` (false 时读空设备),除非传入了 {@link CallOptions.input} 属性
|
|
35
35
|
- 默认值:
|
|
36
36
|
- start(): false ('ignore', 空设备)
|
|
37
|
-
- (call|launch)():
|
|
37
|
+
- (call|launch)(): false (pipe, 作为 child.stdin)
|
|
38
38
|
- `true` 设置子进程 stdin 为 'pipe'
|
|
39
39
|
- `false` 设置子进程 stdin 为 'ignore' (空设备)
|
|
40
40
|
- `string` 设置子进程 stdin 为某个文件路径 (打开文件,将句柄设置为子进程 stdin) */
|
|
@@ -42,8 +42,8 @@ interface BaseOptions {
|
|
|
42
42
|
/** 控制子进程 stdout
|
|
43
43
|
- 默认值:
|
|
44
44
|
- start(): false ('ignore', 空设备)
|
|
45
|
-
- (call|launch)(): true (pipe, 作为 child.stdout
|
|
46
|
-
- `true` 设置子进程 stdout 为 'pipe' (作为 child.stdout
|
|
45
|
+
- (call|launch)(): true (pipe, 作为 child.stdout)
|
|
46
|
+
- `true` 设置子进程 stdout 为 'pipe' (作为 child.stdout)
|
|
47
47
|
- `false` 设置子进程 stdout 为 'ignore' (空设备)
|
|
48
48
|
- `string` 设置子进程 stdout 某个文件路径 (打开文件,将句柄设置为子进程 stdout) */
|
|
49
49
|
stdout?: boolean | string;
|
package/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { default as http, } from 'node:http';
|
|
2
|
+
import { default as http2 } from 'node:http2';
|
|
3
3
|
import { createSecureContext } from 'node:tls';
|
|
4
4
|
import zlib from 'node:zlib';
|
|
5
5
|
import fs from 'node:fs';
|
|
@@ -12,8 +12,8 @@ import resolve_safely from 'resolve-path';
|
|
|
12
12
|
import { contentType as get_content_type } from 'mime-types';
|
|
13
13
|
// --- my libs
|
|
14
14
|
import { t } from "./i18n/instance.js";
|
|
15
|
-
import { request as _request, Remote } from "./net.js";
|
|
16
|
-
import { inspect, check, range_to_numbers, encode, filter_keys, filter_values, consume_stream, colored, url_width } from "./utils.js";
|
|
15
|
+
import { request as _request, Remote, get_socket_ip } from "./net.js";
|
|
16
|
+
import { inspect, check, range_to_numbers, encode, filter_keys, filter_values, consume_stream, colored, url_width, defer2 } from "./utils.js";
|
|
17
17
|
import { flist, fread, fstat } from "./file.js";
|
|
18
18
|
import { exe_nodejs, sea } from "./process.js";
|
|
19
19
|
// ------------ my server
|
|
@@ -138,9 +138,8 @@ export class Server {
|
|
|
138
138
|
app.use(this._router.bind(this));
|
|
139
139
|
this.app = app;
|
|
140
140
|
this.handler = this.app.callback();
|
|
141
|
-
this.http_server =
|
|
142
|
-
|
|
143
|
-
if (http2) {
|
|
141
|
+
this.http_server = http.createServer({ optimizeEmptyRequests: true, noDelay: true }, this.handler);
|
|
142
|
+
if (this.http2) {
|
|
144
143
|
const { fpd_certs } = this;
|
|
145
144
|
let lazy_secure_ctxs = Object.fromEntries(await Promise.all(
|
|
146
145
|
// fpd_certs 文件夹下面的每个 .key 对应一个证书及域名
|
|
@@ -157,7 +156,7 @@ export class Server {
|
|
|
157
156
|
})));
|
|
158
157
|
const default_ctx = lazy_secure_ctxs[this.default_hostnames.find(hostname => hostname in lazy_secure_ctxs)];
|
|
159
158
|
check(default_ctx);
|
|
160
|
-
this.http2_server =
|
|
159
|
+
this.http2_server = http2.createSecureServer({
|
|
161
160
|
SNICallback(servername, callback) {
|
|
162
161
|
let lazy_ctx = lazy_secure_ctxs[servername] ||
|
|
163
162
|
lazy_secure_ctxs[servername.replace(/^.*?\./, '*.')] ||
|
|
@@ -166,6 +165,7 @@ export class Server {
|
|
|
166
165
|
callback(null, lazy_ctx.ctx ??= createSecureContext(lazy_ctx));
|
|
167
166
|
},
|
|
168
167
|
allowHTTP1: true,
|
|
168
|
+
noDelay: true
|
|
169
169
|
}, this.handler);
|
|
170
170
|
}
|
|
171
171
|
// websocket rpc
|
|
@@ -229,20 +229,16 @@ export class Server {
|
|
|
229
229
|
}
|
|
230
230
|
await this.init_server();
|
|
231
231
|
await Promise.all([
|
|
232
|
-
http &&
|
|
233
|
-
|
|
234
|
-
}),
|
|
235
|
-
http2 && new Promise(resolve => {
|
|
236
|
-
this.http2_server.listen(this.http2_port, resolve);
|
|
237
|
-
}),
|
|
232
|
+
this.http && listen_http_server(this.http_server, this.http_port),
|
|
233
|
+
this.http2 && listen_http_server(this.http2_server, this.http2_port)
|
|
238
234
|
]);
|
|
239
235
|
if (this.print.info)
|
|
240
236
|
console.log(t('{{name}} 启动成功,正在监听 {{ports}} 端口', {
|
|
241
237
|
name: this.name,
|
|
242
238
|
ports: [
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
].join(', ')
|
|
239
|
+
this.http && this.http_port,
|
|
240
|
+
this.http2 && this.http2_port
|
|
241
|
+
].filter(Boolean).join(', ')
|
|
246
242
|
}));
|
|
247
243
|
}
|
|
248
244
|
try_write_stdio_subscribers(chunk) {
|
|
@@ -258,25 +254,25 @@ export class Server {
|
|
|
258
254
|
}
|
|
259
255
|
stop() {
|
|
260
256
|
this.http_server.close();
|
|
261
|
-
|
|
262
|
-
this.http2_server.close();
|
|
257
|
+
this.http2_server?.close();
|
|
263
258
|
}
|
|
264
259
|
/** 可被子类重写定义错误处理逻辑 */
|
|
265
260
|
on_error(error, ctx) {
|
|
266
|
-
if (this.print.errors)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
261
|
+
if (!this.print.errors)
|
|
262
|
+
return;
|
|
263
|
+
const code = error?.code;
|
|
264
|
+
if (code === 'EPIPE' || code === 'ECONNRESET')
|
|
265
|
+
console.log(`${error.code}:`, ctx?.request?.url);
|
|
266
|
+
else {
|
|
267
|
+
console.error(error);
|
|
268
|
+
if (ctx)
|
|
269
|
+
console.log('ctx:', ctx);
|
|
275
270
|
}
|
|
276
271
|
}
|
|
277
272
|
on_upgrade(request, socket, head) {
|
|
278
273
|
// url 只有路径部分
|
|
279
274
|
const { url, headers, headers: { host = '' }, } = request;
|
|
275
|
+
const ip = get_socket_ip(request.socket);
|
|
280
276
|
if (this.print.logs) {
|
|
281
277
|
let s =
|
|
282
278
|
// 时间
|
|
@@ -289,7 +285,7 @@ export class Server {
|
|
|
289
285
|
url;
|
|
290
286
|
s = s.pad(url_width);
|
|
291
287
|
// ip
|
|
292
|
-
s += ` <- ${
|
|
288
|
+
s += ` <- ${ip}`;
|
|
293
289
|
// 客户端信息
|
|
294
290
|
const client_info = this.get_client_info(headers);
|
|
295
291
|
if (client_info)
|
|
@@ -327,7 +323,7 @@ export class Server {
|
|
|
327
323
|
console.error(error);
|
|
328
324
|
response.status = error.status || 500;
|
|
329
325
|
response.set('content-type', text_plain);
|
|
330
|
-
response.body =
|
|
326
|
+
response.body = inspect_error(error);
|
|
331
327
|
}
|
|
332
328
|
}
|
|
333
329
|
/** 解析 req.body to request.body
|
|
@@ -638,7 +634,8 @@ export class Server {
|
|
|
638
634
|
if (this.print.errors)
|
|
639
635
|
console.log(error);
|
|
640
636
|
response.status = 500;
|
|
641
|
-
response.
|
|
637
|
+
response.set('content-type', text_plain);
|
|
638
|
+
response.body = inspect_error(error);
|
|
642
639
|
}
|
|
643
640
|
}
|
|
644
641
|
return true;
|
|
@@ -800,7 +797,7 @@ export class Server {
|
|
|
800
797
|
- reverse?: `false` 在 range 内从后往前尝试 */
|
|
801
798
|
static async get_available_port(range, reverse = false) {
|
|
802
799
|
for (const port of range_to_numbers(range, reverse)) {
|
|
803
|
-
let server =
|
|
800
|
+
let server = http.createServer();
|
|
804
801
|
try {
|
|
805
802
|
await new Promise((resolve, reject) => {
|
|
806
803
|
server.once('error', reject);
|
|
@@ -825,4 +822,17 @@ export class Server {
|
|
|
825
822
|
}
|
|
826
823
|
export const text_plain = 'text/plain; charset=utf-8';
|
|
827
824
|
const devtools_trash = '/.well-known/appspecific/com.chrome.devtools.json';
|
|
825
|
+
function listen_http_server(server, port) {
|
|
826
|
+
let plisten = defer2();
|
|
827
|
+
server.once('error', plisten.reject);
|
|
828
|
+
server.listen(port, () => {
|
|
829
|
+
server.off('error', plisten.reject);
|
|
830
|
+
plisten.resolve();
|
|
831
|
+
});
|
|
832
|
+
return plisten;
|
|
833
|
+
}
|
|
834
|
+
const no_color = { colors: false };
|
|
835
|
+
function inspect_error(error) {
|
|
836
|
+
return inspect(error, no_color);
|
|
837
|
+
}
|
|
828
838
|
//# sourceMappingURL=server.js.map
|
package/utils.common.d.ts
CHANGED
|
@@ -50,7 +50,7 @@ export declare function filter_values<TObj extends Record<string, any>>(obj: TOb
|
|
|
50
50
|
/** 简单选择对象中的部分 keys, 返回新对象 */
|
|
51
51
|
export declare function pick<TObject = any>(obj: TObject, keys: (keyof TObject)[]): Partial<TObject>;
|
|
52
52
|
/** 忽略对象中的 keys, 返回新对象 */
|
|
53
|
-
export declare function omit<TObj>(obj: TObj, omit_keys: string[]): TObj;
|
|
53
|
+
export declare function omit<TObj>(obj: TObj, omit_keys: string[] | Set<string>): TObj;
|
|
54
54
|
/** 拼接 TypedArrays 生成一个完整的 Uint8Array */
|
|
55
55
|
export declare function concat(arrays: ArrayBufferView[]): Uint8Array<ArrayBufferLike>;
|
|
56
56
|
export declare let encoder: TextEncoder;
|
|
@@ -66,11 +66,15 @@ export declare function buffer_equals(left: Uint8Array, right: Uint8Array | stri
|
|
|
66
66
|
/** 在指定的时间 (milliseconds) 内运行某个任务,超时之后抛出错误或调用 on_timeout
|
|
67
67
|
- milliseconds: 限时毫秒数
|
|
68
68
|
- action?: 要等待运行的任务, async function 或 promise
|
|
69
|
-
- on_timeout?:
|
|
70
|
-
- 若传:
|
|
71
|
-
|
|
69
|
+
- on_timeout?: 超时后调用的函数,或者设置错误消息
|
|
70
|
+
- 若传:
|
|
71
|
+
- 传 string: 作为 TimeoutError 的 error message
|
|
72
|
+
- 传 function: 超时时调用会调用 on_timeout,参数为 TimeoutError,然后等待 on_timeout 执行完成
|
|
73
|
+
- on_timeout 函数正常运行: timeout 函数返回 null
|
|
74
|
+
- on_timeout 报错: timeout 函数最终抛出这个错误
|
|
75
|
+
- 若不传: 直接抛出 TimeoutError
|
|
72
76
|
- print?: `true` 打印已超时任务的错误 */
|
|
73
|
-
export declare function timeout<TReturn>(milliseconds: number, action: Promise<TReturn> | (() => Promise<TReturn>), on_timeout?: (error: TimeoutError) => void | Promise<void
|
|
77
|
+
export declare function timeout<TReturn>(milliseconds: number, action: Promise<TReturn> | (() => Promise<TReturn>), on_timeout?: string | ((error: TimeoutError) => void | Promise<void>), print?: boolean): Deferred2<TReturn>;
|
|
74
78
|
/** 轮询尝试 action 共 times 次,每次间隔 duration
|
|
75
79
|
action 返回 trusy 值时认为成功,返回 action 的结果
|
|
76
80
|
如果次数用尽仍然失败,返回 null */
|
|
@@ -118,7 +122,7 @@ export type StrictArrayBuffer = ArrayBuffer & {
|
|
|
118
122
|
};
|
|
119
123
|
export interface Deferred<TValue> extends Promise<TValue> {
|
|
120
124
|
resolve(value: TValue | PromiseLike<TValue>): void;
|
|
121
|
-
reject(
|
|
125
|
+
reject(error?: Error): void;
|
|
122
126
|
}
|
|
123
127
|
/** 创建一个 promise,后续可调用 promise.resolve, promise.reject 方法设置其状态和值
|
|
124
128
|
- initial?: `undefined` 传入非 undefined 值(包括 null)时直接设置为 resolved 状态
|
|
@@ -126,7 +130,7 @@ export interface Deferred<TValue> extends Promise<TValue> {
|
|
|
126
130
|
export declare function defer<TValue>(initial?: TValue): Deferred<TValue>;
|
|
127
131
|
export interface Deferred2<TValue> extends Promise<TValue> {
|
|
128
132
|
resolve(value: TValue | PromiseLike<TValue>): void;
|
|
129
|
-
reject(
|
|
133
|
+
reject(error?: Error): void;
|
|
130
134
|
get settled(): boolean;
|
|
131
135
|
}
|
|
132
136
|
/** 有 settled 状态的 defer */
|
|
@@ -167,7 +171,7 @@ export declare function lowercase_first_letter(str: string): string;
|
|
|
167
171
|
export declare function ceil2(n: number): number;
|
|
168
172
|
/** 节流,最多只在时间间隔末尾调用一次,可以设置首次调用是否延后 */
|
|
169
173
|
export declare function throttle(duration: number, func: Function, delay_first?: boolean): (this: any, ...args: any[]) => void;
|
|
170
|
-
/**
|
|
174
|
+
/** 防抖,间隔一段时间不再触发时调用,同时大幅优化减少 setTimeout, clearTimeout 的调用次数 */
|
|
171
175
|
export declare function debounce(duration: number, func: Function): (this: any, ...args: any[]) => void;
|
|
172
176
|
export declare function tomorrow(date?: Date | string | number): Date;
|
|
173
177
|
export declare function to_csv_field(str: string): string;
|
|
@@ -176,10 +180,3 @@ export declare function set_error_message(error: Error, message: string): Error;
|
|
|
176
180
|
/** 比较两个数组中的元素完全相同,数组元素用引用比较 */
|
|
177
181
|
export declare function array_equals(a: any[], b: any[]): boolean;
|
|
178
182
|
export declare function nowstr(with_date?: boolean): string;
|
|
179
|
-
interface MyTimeout {
|
|
180
|
-
clear(): void;
|
|
181
|
-
ref: NodeJS.Timeout | number;
|
|
182
|
-
}
|
|
183
|
-
/** 面向对象的 timeout, callback 在后的 timeout */
|
|
184
|
-
export declare function set_timeout(delay: number, callback: VoidFunction): MyTimeout;
|
|
185
|
-
export {};
|
package/utils.common.js
CHANGED
|
@@ -136,8 +136,11 @@ export function pick(obj, keys) {
|
|
|
136
136
|
}
|
|
137
137
|
/** 忽略对象中的 keys, 返回新对象 */
|
|
138
138
|
export function omit(obj, omit_keys) {
|
|
139
|
-
const
|
|
140
|
-
return filter_keys(obj,
|
|
139
|
+
const set = omit_keys instanceof Set;
|
|
140
|
+
return filter_keys(obj, set ?
|
|
141
|
+
key => !(omit_keys.has(key))
|
|
142
|
+
:
|
|
143
|
+
key => !(omit_keys.includes(key)));
|
|
141
144
|
}
|
|
142
145
|
/** 拼接 TypedArrays 生成一个完整的 Uint8Array */
|
|
143
146
|
export function concat(arrays) {
|
|
@@ -183,55 +186,52 @@ export function buffer_equals(left, right) {
|
|
|
183
186
|
/** 在指定的时间 (milliseconds) 内运行某个任务,超时之后抛出错误或调用 on_timeout
|
|
184
187
|
- milliseconds: 限时毫秒数
|
|
185
188
|
- action?: 要等待运行的任务, async function 或 promise
|
|
186
|
-
- on_timeout?:
|
|
187
|
-
- 若传:
|
|
188
|
-
|
|
189
|
+
- on_timeout?: 超时后调用的函数,或者设置错误消息
|
|
190
|
+
- 若传:
|
|
191
|
+
- 传 string: 作为 TimeoutError 的 error message
|
|
192
|
+
- 传 function: 超时时调用会调用 on_timeout,参数为 TimeoutError,然后等待 on_timeout 执行完成
|
|
193
|
+
- on_timeout 函数正常运行: timeout 函数返回 null
|
|
194
|
+
- on_timeout 报错: timeout 函数最终抛出这个错误
|
|
195
|
+
- 若不传: 直接抛出 TimeoutError
|
|
189
196
|
- print?: `true` 打印已超时任务的错误 */
|
|
190
|
-
export
|
|
191
|
-
const error = new TimeoutError();
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
await delay(milliseconds);
|
|
197
|
-
if (!done)
|
|
198
|
-
if (on_timeout)
|
|
199
|
-
try {
|
|
200
|
-
await on_timeout(error);
|
|
201
|
-
resolve(null);
|
|
202
|
-
}
|
|
203
|
-
catch (error) {
|
|
204
|
-
if (rejected)
|
|
205
|
-
throw error; // 会成为 unhandled rejection
|
|
206
|
-
else {
|
|
207
|
-
rejected = true;
|
|
208
|
-
reject(error);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
rejected = true;
|
|
213
|
-
reject(error);
|
|
214
|
-
}
|
|
215
|
-
})();
|
|
216
|
-
(async () => {
|
|
197
|
+
export function timeout(milliseconds, action, on_timeout, print = true) {
|
|
198
|
+
const error = new TimeoutError(typeof on_timeout === 'string' ? on_timeout : undefined);
|
|
199
|
+
let presult = defer2();
|
|
200
|
+
let waiting_on_timeout = false;
|
|
201
|
+
let timeout = setTimeout(async () => {
|
|
202
|
+
if (typeof on_timeout === 'function')
|
|
217
203
|
try {
|
|
218
|
-
|
|
204
|
+
waiting_on_timeout = true;
|
|
205
|
+
await on_timeout(error);
|
|
206
|
+
presult.resolve(null);
|
|
219
207
|
}
|
|
220
208
|
catch (error) {
|
|
221
|
-
|
|
222
|
-
if (print)
|
|
223
|
-
console.log(`已超时任务的错误: ${error.message}`);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
rejected = true;
|
|
227
|
-
reject(error);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
finally {
|
|
231
|
-
done = true;
|
|
209
|
+
presult.reject(error);
|
|
232
210
|
}
|
|
233
|
-
|
|
211
|
+
else
|
|
212
|
+
presult.reject(error);
|
|
213
|
+
}, milliseconds);
|
|
214
|
+
const paction = typeof action === 'function' ? action() : action;
|
|
215
|
+
paction.then(result => {
|
|
216
|
+
if (presult.settled) {
|
|
217
|
+
if (print)
|
|
218
|
+
console.log('已超时任务最终完成了');
|
|
219
|
+
}
|
|
220
|
+
else if (!waiting_on_timeout) {
|
|
221
|
+
presult.resolve(result);
|
|
222
|
+
clearTimeout(timeout);
|
|
223
|
+
}
|
|
224
|
+
}, error => {
|
|
225
|
+
if (presult.settled) {
|
|
226
|
+
if (print)
|
|
227
|
+
console.error(`已超时任务的错误: ${error.message}`);
|
|
228
|
+
}
|
|
229
|
+
else if (!waiting_on_timeout) {
|
|
230
|
+
presult.reject(error);
|
|
231
|
+
clearTimeout(timeout);
|
|
232
|
+
}
|
|
234
233
|
});
|
|
234
|
+
return presult;
|
|
235
235
|
}
|
|
236
236
|
/** 轮询尝试 action 共 times 次,每次间隔 duration
|
|
237
237
|
action 返回 trusy 值时认为成功,返回 action 的结果
|
|
@@ -382,19 +382,18 @@ export class TimeoutError extends Error {
|
|
|
382
382
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
383
383
|
export function defer(initial) {
|
|
384
384
|
if (initial === undefined) {
|
|
385
|
-
let resolve;
|
|
386
|
-
let
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
385
|
+
let { promise, resolve, reject } = Promise.withResolvers();
|
|
386
|
+
let p = promise;
|
|
387
|
+
p.resolve = resolve;
|
|
388
|
+
p.reject = reject;
|
|
389
|
+
return p;
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
let p = Promise.resolve(initial);
|
|
393
|
+
p.resolve = noop;
|
|
394
|
+
p.reject = noop;
|
|
395
|
+
return p;
|
|
392
396
|
}
|
|
393
|
-
else
|
|
394
|
-
return Object.assign(Promise.resolve(initial), {
|
|
395
|
-
resolve: noop,
|
|
396
|
-
reject: noop
|
|
397
|
-
});
|
|
398
397
|
}
|
|
399
398
|
/** 有 settled 状态的 defer */
|
|
400
399
|
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
@@ -402,26 +401,29 @@ export function defer2(initial) {
|
|
|
402
401
|
if (initial === undefined) {
|
|
403
402
|
let settled = false;
|
|
404
403
|
let { promise, resolve, reject } = Promise.withResolvers();
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
404
|
+
let p = promise;
|
|
405
|
+
p.resolve = (value) => {
|
|
406
|
+
settled = true;
|
|
407
|
+
resolve(value);
|
|
408
|
+
};
|
|
409
|
+
p.reject = (error) => {
|
|
410
|
+
settled = true;
|
|
411
|
+
reject(error);
|
|
412
|
+
};
|
|
413
|
+
Object.defineProperty(p, 'settled', {
|
|
414
|
+
get() { return settled; },
|
|
415
|
+
enumerable: true,
|
|
416
|
+
configurable: true
|
|
417
417
|
});
|
|
418
|
+
return p;
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
let p = Promise.resolve(initial);
|
|
422
|
+
p.resolve = noop;
|
|
423
|
+
p.reject = noop;
|
|
424
|
+
p.settled = true;
|
|
425
|
+
return p;
|
|
418
426
|
}
|
|
419
|
-
else
|
|
420
|
-
return Object.assign(Promise.resolve(initial), {
|
|
421
|
-
resolve: noop,
|
|
422
|
-
reject: noop,
|
|
423
|
-
settled: true
|
|
424
|
-
});
|
|
425
427
|
}
|
|
426
428
|
/** @example
|
|
427
429
|
let lock = new Lock(redis)
|
|
@@ -508,8 +510,8 @@ export function ceil2(n) {
|
|
|
508
510
|
export function throttle(duration, func, delay_first = false) {
|
|
509
511
|
let timeout = 0;
|
|
510
512
|
let last = 0;
|
|
511
|
-
let saved_this
|
|
512
|
-
let saved_args
|
|
513
|
+
let saved_this;
|
|
514
|
+
let saved_args;
|
|
513
515
|
return function throttled(...args) {
|
|
514
516
|
// 当前时间间隔已预定执行,本次调用仅更新调用参数
|
|
515
517
|
if (timeout) {
|
|
@@ -539,14 +541,33 @@ export function throttle(duration, func, delay_first = false) {
|
|
|
539
541
|
}
|
|
540
542
|
};
|
|
541
543
|
}
|
|
542
|
-
/**
|
|
544
|
+
/** 防抖,间隔一段时间不再触发时调用,同时大幅优化减少 setTimeout, clearTimeout 的调用次数 */
|
|
543
545
|
export function debounce(duration, func) {
|
|
544
|
-
|
|
546
|
+
const half = Math.floor(duration / 2);
|
|
547
|
+
let timeout;
|
|
548
|
+
let last = 0;
|
|
549
|
+
let saved_args;
|
|
550
|
+
let saved_this;
|
|
551
|
+
function debounce_callback() {
|
|
552
|
+
timeout = null;
|
|
553
|
+
func.apply(saved_this, saved_args);
|
|
554
|
+
saved_this = null;
|
|
555
|
+
saved_args = null;
|
|
556
|
+
}
|
|
545
557
|
return function debounced(...args) {
|
|
558
|
+
if (!timeout) {
|
|
559
|
+
timeout = setTimeout(debounce_callback, duration);
|
|
560
|
+
last = Date.now();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const now = Date.now();
|
|
564
|
+
saved_args = args;
|
|
565
|
+
saved_this = this;
|
|
566
|
+
if (now - last < half)
|
|
567
|
+
return;
|
|
546
568
|
clearTimeout(timeout);
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
}, duration);
|
|
569
|
+
setTimeout(debounce_callback, duration);
|
|
570
|
+
last = now;
|
|
550
571
|
};
|
|
551
572
|
}
|
|
552
573
|
export function tomorrow(date) {
|
|
@@ -576,17 +597,4 @@ export function nowstr(with_date = false) {
|
|
|
576
597
|
const date = new Date();
|
|
577
598
|
return with_date ? date.to_str() : date.to_time_str();
|
|
578
599
|
}
|
|
579
|
-
/** 面向对象的 timeout, callback 在后的 timeout */
|
|
580
|
-
export function set_timeout(delay, callback) {
|
|
581
|
-
let ret = {
|
|
582
|
-
ref: setTimeout(callback, delay),
|
|
583
|
-
clear() {
|
|
584
|
-
if (!ret.ref)
|
|
585
|
-
return;
|
|
586
|
-
clearTimeout(ret.ref);
|
|
587
|
-
ret.ref = null;
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
return ret;
|
|
591
|
-
}
|
|
592
600
|
//# sourceMappingURL=utils.common.js.map
|
package/utils.d.ts
CHANGED
|
@@ -45,7 +45,6 @@ export declare function map_stream<Out, In = any>(mapper: (obj: In, cb: Function
|
|
|
45
45
|
failures?: boolean;
|
|
46
46
|
}): Duplex;
|
|
47
47
|
export declare function stream_to_lines(stream: Readable): AsyncGenerator<string, void, unknown>;
|
|
48
|
-
export declare function pipe_with_error<TWritable extends Writable>(readable: Readable, writable: TWritable): TWritable;
|
|
49
48
|
export declare class WritableMemoryStream extends Writable {
|
|
50
49
|
chunks: Uint8Array[];
|
|
51
50
|
pbuffer: import("./utils.common.ts").Deferred<Buffer<ArrayBufferLike>>;
|
package/utils.js
CHANGED
|
@@ -225,11 +225,6 @@ export async function* stream_to_lines(stream) {
|
|
|
225
225
|
buf = chunk.slice(j);
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
-
export function pipe_with_error(readable, writable) {
|
|
229
|
-
// 不知道 transform 作为 AsyncIterable 使用时, emit error 是否会在 for await (...) 循环中触发错误
|
|
230
|
-
readable.once('error', error => { writable.emit('error', error); });
|
|
231
|
-
return readable.pipe(writable);
|
|
232
|
-
}
|
|
233
228
|
export class WritableMemoryStream extends Writable {
|
|
234
229
|
chunks = [];
|
|
235
230
|
pbuffer = defer();
|
package/xlint.js
CHANGED
|
@@ -787,9 +787,6 @@ export const xlint_config = {
|
|
|
787
787
|
'@typescript-eslint/dot-notation': 'error',
|
|
788
788
|
// 必须 throw Error
|
|
789
789
|
'@typescript-eslint/only-throw-error': 'error',
|
|
790
|
-
// ------------ async
|
|
791
|
-
// 返回 Promise 的函数一定要标记为 async 函数
|
|
792
|
-
'@typescript-eslint/promise-function-async': 'error',
|
|
793
790
|
// 不要 return await promise, 直接 return promise, 除非外面有 try catch
|
|
794
791
|
'@typescript-eslint/return-await': 'error',
|
|
795
792
|
// ------------ 括号
|