@kevisual/query 0.0.32 → 0.0.34
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/dist/query-adapter.js +1 -1
- package/dist/query-browser.d.ts +16 -4
- package/dist/query-browser.js +8 -3
- package/dist/query.d.ts +14 -3
- package/dist/query.js +8 -3
- package/package.json +8 -8
- package/src/adapter.ts +129 -0
- package/src/index.ts +1 -0
- package/src/query-browser.ts +57 -0
- package/src/query.ts +295 -0
- package/src/utils.ts +24 -0
- package/src/ws.ts +220 -0
package/dist/query-adapter.js
CHANGED
package/dist/query-browser.d.ts
CHANGED
|
@@ -112,6 +112,13 @@ type QueryOpts$1 = {
|
|
|
112
112
|
adapter?: typeof adapter;
|
|
113
113
|
[key: string]: any;
|
|
114
114
|
} & AdapterOpts;
|
|
115
|
+
type QueryOptions = {
|
|
116
|
+
url?: string;
|
|
117
|
+
adapter?: typeof adapter;
|
|
118
|
+
headers?: Record<string, string>;
|
|
119
|
+
timeout?: number;
|
|
120
|
+
isClient?: boolean;
|
|
121
|
+
};
|
|
115
122
|
type Data = {
|
|
116
123
|
path?: string;
|
|
117
124
|
key?: string;
|
|
@@ -172,7 +179,8 @@ declare class Query {
|
|
|
172
179
|
*/
|
|
173
180
|
stop?: boolean;
|
|
174
181
|
qws: QueryWs;
|
|
175
|
-
|
|
182
|
+
isClient: boolean;
|
|
183
|
+
constructor(opts?: QueryOptions);
|
|
176
184
|
setQueryWs(qws: QueryWs): void;
|
|
177
185
|
/**
|
|
178
186
|
* 突然停止请求
|
|
@@ -209,7 +217,6 @@ declare class Query {
|
|
|
209
217
|
fetchText(urlOrOptions?: string | QueryOpts$1, options?: QueryOpts$1): Promise<Result<any>>;
|
|
210
218
|
}
|
|
211
219
|
|
|
212
|
-
/** @deprecated */
|
|
213
220
|
declare class BaseQuery<T extends Query = Query, R extends {
|
|
214
221
|
queryChain?: any;
|
|
215
222
|
query?: any;
|
|
@@ -228,6 +235,10 @@ declare class BaseQuery<T extends Query = Query, R extends {
|
|
|
228
235
|
post<R = any, P = any>(data: P, options?: DataOpts): Promise<Result<R>>;
|
|
229
236
|
get<R = any, P = any>(data: P, options?: DataOpts): Promise<Result<R>>;
|
|
230
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* @deprecated
|
|
240
|
+
* 前端调用后端QueryRouter, 默认路径 /client/router
|
|
241
|
+
*/
|
|
231
242
|
declare class ClientQuery extends Query {
|
|
232
243
|
constructor(opts?: QueryOpts$1);
|
|
233
244
|
}
|
|
@@ -237,6 +248,7 @@ type QueryOpts = {
|
|
|
237
248
|
adapter?: typeof adapter;
|
|
238
249
|
headers?: Record<string, string>;
|
|
239
250
|
timeout?: number;
|
|
251
|
+
isClient?: boolean;
|
|
240
252
|
};
|
|
241
253
|
/**
|
|
242
254
|
* 前端调用后端QueryRouter, 封装 beforeRequest 和 wss
|
|
@@ -245,7 +257,7 @@ declare class QueryClient extends Query {
|
|
|
245
257
|
tokenName: string;
|
|
246
258
|
storage: Storage;
|
|
247
259
|
token: string;
|
|
248
|
-
constructor(opts?:
|
|
260
|
+
constructor(opts?: QueryOptions & {
|
|
249
261
|
tokenName?: string;
|
|
250
262
|
storage?: Storage;
|
|
251
263
|
io?: boolean;
|
|
@@ -257,4 +269,4 @@ declare class QueryClient extends Query {
|
|
|
257
269
|
}
|
|
258
270
|
|
|
259
271
|
export { BaseQuery, ClientQuery, Query, QueryClient, QueryWs, adapter, wrapperError };
|
|
260
|
-
export type { Data, DataOpts, QueryOpts, QueryWsOpts, Result };
|
|
272
|
+
export type { Data, DataOpts, QueryOptions, QueryOpts, QueryWsOpts, Result };
|
package/dist/query-browser.js
CHANGED
|
@@ -79,7 +79,7 @@ const adapter = async (opts = {}, overloadOpts) => {
|
|
|
79
79
|
}
|
|
80
80
|
else if (isTextForContentType(contentType)) {
|
|
81
81
|
return {
|
|
82
|
-
code:
|
|
82
|
+
code: response.status,
|
|
83
83
|
status: response.status,
|
|
84
84
|
data: await response.text(), // 直接返回文本内容
|
|
85
85
|
};
|
|
@@ -358,9 +358,11 @@ class Query {
|
|
|
358
358
|
stop;
|
|
359
359
|
// 默认不使用ws
|
|
360
360
|
qws;
|
|
361
|
+
isClient = false;
|
|
361
362
|
constructor(opts) {
|
|
362
363
|
this.adapter = opts?.adapter || adapter;
|
|
363
|
-
|
|
364
|
+
const defaultURL = opts?.isClient ? '/client/router' : '/api/router';
|
|
365
|
+
this.url = opts?.url || defaultURL;
|
|
364
366
|
this.headers = opts?.headers || {
|
|
365
367
|
'Content-Type': 'application/json',
|
|
366
368
|
};
|
|
@@ -502,7 +504,6 @@ class Query {
|
|
|
502
504
|
return res;
|
|
503
505
|
}
|
|
504
506
|
}
|
|
505
|
-
/** @deprecated */
|
|
506
507
|
class BaseQuery {
|
|
507
508
|
query;
|
|
508
509
|
queryDefine;
|
|
@@ -528,6 +529,10 @@ class BaseQuery {
|
|
|
528
529
|
return this.query.get(data, options);
|
|
529
530
|
}
|
|
530
531
|
}
|
|
532
|
+
/**
|
|
533
|
+
* @deprecated
|
|
534
|
+
* 前端调用后端QueryRouter, 默认路径 /client/router
|
|
535
|
+
*/
|
|
531
536
|
class ClientQuery extends Query {
|
|
532
537
|
constructor(opts) {
|
|
533
538
|
super({ ...opts, url: opts?.url || '/client/router' });
|
package/dist/query.d.ts
CHANGED
|
@@ -112,6 +112,13 @@ type QueryOpts = {
|
|
|
112
112
|
adapter?: typeof adapter;
|
|
113
113
|
[key: string]: any;
|
|
114
114
|
} & AdapterOpts;
|
|
115
|
+
type QueryOptions = {
|
|
116
|
+
url?: string;
|
|
117
|
+
adapter?: typeof adapter;
|
|
118
|
+
headers?: Record<string, string>;
|
|
119
|
+
timeout?: number;
|
|
120
|
+
isClient?: boolean;
|
|
121
|
+
};
|
|
115
122
|
type Data = {
|
|
116
123
|
path?: string;
|
|
117
124
|
key?: string;
|
|
@@ -183,7 +190,8 @@ declare class Query {
|
|
|
183
190
|
*/
|
|
184
191
|
stop?: boolean;
|
|
185
192
|
qws: QueryWs;
|
|
186
|
-
|
|
193
|
+
isClient: boolean;
|
|
194
|
+
constructor(opts?: QueryOptions);
|
|
187
195
|
setQueryWs(qws: QueryWs): void;
|
|
188
196
|
/**
|
|
189
197
|
* 突然停止请求
|
|
@@ -220,7 +228,6 @@ declare class Query {
|
|
|
220
228
|
fetchText(urlOrOptions?: string | QueryOpts, options?: QueryOpts): Promise<Result<any>>;
|
|
221
229
|
}
|
|
222
230
|
|
|
223
|
-
/** @deprecated */
|
|
224
231
|
declare class BaseQuery<T extends Query = Query, R extends {
|
|
225
232
|
queryChain?: any;
|
|
226
233
|
query?: any;
|
|
@@ -239,9 +246,13 @@ declare class BaseQuery<T extends Query = Query, R extends {
|
|
|
239
246
|
post<R = any, P = any>(data: P, options?: DataOpts): Promise<Result<R>>;
|
|
240
247
|
get<R = any, P = any>(data: P, options?: DataOpts): Promise<Result<R>>;
|
|
241
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* @deprecated
|
|
251
|
+
* 前端调用后端QueryRouter, 默认路径 /client/router
|
|
252
|
+
*/
|
|
242
253
|
declare class ClientQuery extends Query {
|
|
243
254
|
constructor(opts?: QueryOpts);
|
|
244
255
|
}
|
|
245
256
|
|
|
246
257
|
export { BaseQuery, ClientQuery, Query, adapter, setBaseResponse, wrapperError };
|
|
247
|
-
export type { Data, DataOpts, Fn, QueryOpts, Result };
|
|
258
|
+
export type { Data, DataOpts, Fn, QueryOptions, QueryOpts, Result };
|
package/dist/query.js
CHANGED
|
@@ -79,7 +79,7 @@ const adapter = async (opts = {}, overloadOpts) => {
|
|
|
79
79
|
}
|
|
80
80
|
else if (isTextForContentType(contentType)) {
|
|
81
81
|
return {
|
|
82
|
-
code:
|
|
82
|
+
code: response.status,
|
|
83
83
|
status: response.status,
|
|
84
84
|
data: await response.text(), // 直接返回文本内容
|
|
85
85
|
};
|
|
@@ -161,9 +161,11 @@ class Query {
|
|
|
161
161
|
stop;
|
|
162
162
|
// 默认不使用ws
|
|
163
163
|
qws;
|
|
164
|
+
isClient = false;
|
|
164
165
|
constructor(opts) {
|
|
165
166
|
this.adapter = opts?.adapter || adapter;
|
|
166
|
-
|
|
167
|
+
const defaultURL = opts?.isClient ? '/client/router' : '/api/router';
|
|
168
|
+
this.url = opts?.url || defaultURL;
|
|
167
169
|
this.headers = opts?.headers || {
|
|
168
170
|
'Content-Type': 'application/json',
|
|
169
171
|
};
|
|
@@ -305,7 +307,6 @@ class Query {
|
|
|
305
307
|
return res;
|
|
306
308
|
}
|
|
307
309
|
}
|
|
308
|
-
/** @deprecated */
|
|
309
310
|
class BaseQuery {
|
|
310
311
|
query;
|
|
311
312
|
queryDefine;
|
|
@@ -331,6 +332,10 @@ class BaseQuery {
|
|
|
331
332
|
return this.query.get(data, options);
|
|
332
333
|
}
|
|
333
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* @deprecated
|
|
337
|
+
* 前端调用后端QueryRouter, 默认路径 /client/router
|
|
338
|
+
*/
|
|
334
339
|
class ClientQuery extends Query {
|
|
335
340
|
constructor(opts) {
|
|
336
341
|
super({ ...opts, url: opts?.url || '/client/router' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevisual/query",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"main": "dist/query-browser.js",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
"clean": "rm -rf dist"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
|
-
"dist"
|
|
14
|
+
"dist",
|
|
15
|
+
"src"
|
|
15
16
|
],
|
|
16
17
|
"keywords": [
|
|
17
18
|
"kevisual",
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
25
26
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
26
|
-
"rollup": "^4.
|
|
27
|
+
"rollup": "^4.54.0",
|
|
27
28
|
"rollup-plugin-dts": "^6.3.0",
|
|
28
29
|
"typescript": "^5.9.3",
|
|
29
30
|
"zustand": "^5.0.9"
|
|
@@ -47,10 +48,9 @@
|
|
|
47
48
|
"./ws": {
|
|
48
49
|
"import": "./dist/query-ws.js",
|
|
49
50
|
"require": "./dist/query-ws.js"
|
|
50
|
-
},
|
|
51
|
-
"./query-ai": {
|
|
52
|
-
"import": "./dist/query-ai.js",
|
|
53
|
-
"require": "./dist/query-ai.js"
|
|
54
51
|
}
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"tslib": "^2.8.1"
|
|
55
55
|
}
|
|
56
|
-
}
|
|
56
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export const methods = ['GET', 'POST'] as const;
|
|
2
|
+
export type Method = (typeof methods)[number];
|
|
3
|
+
|
|
4
|
+
type SimpleObject = Record<string, any>;
|
|
5
|
+
export type AdapterOpts = {
|
|
6
|
+
url?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* 只用户POST请求,传递的查询参数,
|
|
10
|
+
* GET请求默认方body自己转化为查询参数
|
|
11
|
+
*/
|
|
12
|
+
params?: Record<string, any>;
|
|
13
|
+
body?: Record<string, any> | FormData; // body 可以是对象、字符串或 FormData
|
|
14
|
+
timeout?: number;
|
|
15
|
+
method?: Method;
|
|
16
|
+
/**
|
|
17
|
+
* @deprecated use responseType
|
|
18
|
+
*/
|
|
19
|
+
isBlob?: boolean; // 是否返回 Blob 对象, 第一优先
|
|
20
|
+
/**
|
|
21
|
+
* @deprecated use responseType
|
|
22
|
+
*/
|
|
23
|
+
isText?: boolean; // 是否返回文本内容, 第二优先
|
|
24
|
+
/**
|
|
25
|
+
* 响应类型,
|
|
26
|
+
* */
|
|
27
|
+
responseType?: 'json' | 'text' | 'blob';
|
|
28
|
+
isPostFile?: boolean; // 是否为文件上传
|
|
29
|
+
};
|
|
30
|
+
export const isTextForContentType = (contentType: string | null) => {
|
|
31
|
+
if (!contentType) return false;
|
|
32
|
+
const textTypes = ['text/', 'xml', 'html', 'javascript', 'css', 'csv', 'plain', 'x-www-form-urlencoded', 'md'];
|
|
33
|
+
return textTypes.some((type) => contentType.includes(type));
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
*
|
|
37
|
+
* @param opts
|
|
38
|
+
* @param overloadOpts 覆盖fetch的默认配置
|
|
39
|
+
* @returns
|
|
40
|
+
*/
|
|
41
|
+
export const adapter = async (opts: AdapterOpts = {}, overloadOpts?: RequestInit) => {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const signal = controller.signal;
|
|
44
|
+
const isPostFile = opts.isPostFile || false; // 是否为文件上传
|
|
45
|
+
let responseType = opts.responseType || 'json'; // 响应类型
|
|
46
|
+
if (opts.isBlob) {
|
|
47
|
+
responseType = 'blob';
|
|
48
|
+
} else if (opts.isText) {
|
|
49
|
+
responseType = 'text';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const timeout = opts.timeout || 60000 * 3; // 默认超时时间为 60s * 3
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
controller.abort();
|
|
55
|
+
}, timeout);
|
|
56
|
+
let method = overloadOpts?.method || opts?.method || 'POST';
|
|
57
|
+
let headers = { ...opts?.headers, ...overloadOpts?.headers };
|
|
58
|
+
let origin = '';
|
|
59
|
+
let url: URL;
|
|
60
|
+
if (opts?.url?.startsWith('http')) {
|
|
61
|
+
url = new URL(opts.url);
|
|
62
|
+
} else {
|
|
63
|
+
origin = window?.location?.origin || 'http://localhost:51015';
|
|
64
|
+
url = new URL(opts.url, origin);
|
|
65
|
+
}
|
|
66
|
+
const isGet = method === 'GET';
|
|
67
|
+
if (isGet) {
|
|
68
|
+
url.search = new URLSearchParams(opts.body as SimpleObject).toString();
|
|
69
|
+
} else {
|
|
70
|
+
const params = opts.params || {};
|
|
71
|
+
url.search = new URLSearchParams(params as SimpleObject).toString();
|
|
72
|
+
}
|
|
73
|
+
let body: string | FormData | undefined = undefined;
|
|
74
|
+
if (isGet) {
|
|
75
|
+
body = undefined;
|
|
76
|
+
} else if (isPostFile) {
|
|
77
|
+
body = opts.body as FormData; // 如果是文件上传,直接使用 FormData
|
|
78
|
+
} else {
|
|
79
|
+
headers = {
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
...headers,
|
|
82
|
+
};
|
|
83
|
+
body = JSON.stringify(opts.body); // 否则将对象转换为 JSON 字符串
|
|
84
|
+
}
|
|
85
|
+
return fetch(url, {
|
|
86
|
+
method: method.toUpperCase(),
|
|
87
|
+
signal,
|
|
88
|
+
body: body,
|
|
89
|
+
...overloadOpts,
|
|
90
|
+
headers: headers,
|
|
91
|
+
})
|
|
92
|
+
.then(async (response) => {
|
|
93
|
+
// 获取 Content-Type 头部信息
|
|
94
|
+
const contentType = response.headers.get('Content-Type');
|
|
95
|
+
if (responseType === 'blob') {
|
|
96
|
+
return await response.blob(); // 直接返回 Blob 对象
|
|
97
|
+
}
|
|
98
|
+
const isText = responseType === 'text';
|
|
99
|
+
const isJson = contentType && contentType.includes('application/json');
|
|
100
|
+
// 判断返回的数据类型
|
|
101
|
+
if (isJson && !isText) {
|
|
102
|
+
return await response.json(); // 解析为 JSON
|
|
103
|
+
} else if (isTextForContentType(contentType)) {
|
|
104
|
+
return {
|
|
105
|
+
code: response.status,
|
|
106
|
+
status: response.status,
|
|
107
|
+
data: await response.text(), // 直接返回文本内容
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
return response;
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.catch((err) => {
|
|
114
|
+
if (err.name === 'AbortError') {
|
|
115
|
+
console.log('Request timed out and was aborted');
|
|
116
|
+
}
|
|
117
|
+
console.error(err);
|
|
118
|
+
return {
|
|
119
|
+
code: 500,
|
|
120
|
+
};
|
|
121
|
+
})
|
|
122
|
+
.finally(() => {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* adapter
|
|
128
|
+
*/
|
|
129
|
+
export const queryFetch = adapter;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './query-browser.ts'
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { adapter } from './adapter.ts';
|
|
2
|
+
import { QueryWs, QueryWsOpts } from './ws.ts';
|
|
3
|
+
import { Query, ClientQuery } from './query.ts';
|
|
4
|
+
import { BaseQuery, QueryOptions, wrapperError } from './query.ts';
|
|
5
|
+
|
|
6
|
+
export { QueryOpts, QueryWs, ClientQuery, Query, QueryWsOpts, adapter, BaseQuery, wrapperError };
|
|
7
|
+
export { QueryOptions }
|
|
8
|
+
export type { DataOpts, Result, Data } from './query.ts';
|
|
9
|
+
|
|
10
|
+
type QueryOpts = {
|
|
11
|
+
url?: string;
|
|
12
|
+
adapter?: typeof adapter;
|
|
13
|
+
headers?: Record<string, string>;
|
|
14
|
+
timeout?: number;
|
|
15
|
+
isClient?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 前端调用后端QueryRouter, 封装 beforeRequest 和 wss
|
|
20
|
+
*/
|
|
21
|
+
export class QueryClient extends Query {
|
|
22
|
+
tokenName: string;
|
|
23
|
+
storage: Storage;
|
|
24
|
+
token: string;
|
|
25
|
+
constructor(opts?: QueryOptions & { tokenName?: string; storage?: Storage; io?: boolean }) {
|
|
26
|
+
super(opts);
|
|
27
|
+
this.tokenName = opts?.tokenName || 'token';
|
|
28
|
+
this.storage = opts?.storage || localStorage;
|
|
29
|
+
this.beforeRequest = async (opts) => {
|
|
30
|
+
const token = this.token || this.getToken();
|
|
31
|
+
if (token) {
|
|
32
|
+
opts.headers = {
|
|
33
|
+
...opts.headers,
|
|
34
|
+
Authorization: `Bearer ${token}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return opts;
|
|
38
|
+
};
|
|
39
|
+
if (opts?.io) {
|
|
40
|
+
this.createWs();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
createWs(opts?: QueryWsOpts) {
|
|
44
|
+
this.qws = new QueryWs({ url: this.url, ...opts });
|
|
45
|
+
}
|
|
46
|
+
getToken() {
|
|
47
|
+
return this.storage.getItem(this.tokenName);
|
|
48
|
+
}
|
|
49
|
+
saveToken(token: string) {
|
|
50
|
+
this.storage.setItem(this.tokenName, token);
|
|
51
|
+
}
|
|
52
|
+
removeToken() {
|
|
53
|
+
this.storage.removeItem(this.tokenName);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// 移除默认生成的实例
|
|
57
|
+
// export const client = new QueryClient();
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { adapter, Method, AdapterOpts } from './adapter.ts';
|
|
2
|
+
import type { QueryWs } from './ws.ts';
|
|
3
|
+
/**
|
|
4
|
+
* 请求前处理函数
|
|
5
|
+
* @param opts 请求配置
|
|
6
|
+
* @returns 请求配置
|
|
7
|
+
*/
|
|
8
|
+
export type Fn = (opts: {
|
|
9
|
+
url?: string;
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
body?: Record<string, any>;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
timeout?: number;
|
|
14
|
+
}) => Promise<Record<string, any> | false>;
|
|
15
|
+
|
|
16
|
+
export type QueryOpts = {
|
|
17
|
+
adapter?: typeof adapter;
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
} & AdapterOpts;
|
|
20
|
+
|
|
21
|
+
export type QueryOptions = {
|
|
22
|
+
url?: string;
|
|
23
|
+
adapter?: typeof adapter;
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
timeout?: number;
|
|
26
|
+
isClient?: boolean;
|
|
27
|
+
}
|
|
28
|
+
export type Data = {
|
|
29
|
+
path?: string;
|
|
30
|
+
key?: string;
|
|
31
|
+
payload?: Record<string, any>;
|
|
32
|
+
[key: string]: any;
|
|
33
|
+
};
|
|
34
|
+
export type Result<S = any, U = {}> = {
|
|
35
|
+
code: number;
|
|
36
|
+
data?: S;
|
|
37
|
+
message?: string;
|
|
38
|
+
} & U;
|
|
39
|
+
// 额外功能
|
|
40
|
+
export type DataOpts = Partial<QueryOpts> & {
|
|
41
|
+
beforeRequest?: Fn;
|
|
42
|
+
afterResponse?: <S = any>(result: Result<S>, ctx?: { req?: any; res?: any; fetch?: any }) => Promise<Result<S>>;
|
|
43
|
+
/**
|
|
44
|
+
* 是否在stop的时候不请求
|
|
45
|
+
*/
|
|
46
|
+
noStop?: boolean;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* 设置基础响应, 设置 success 和 showError,
|
|
50
|
+
* success 是 code 是否等于 200
|
|
51
|
+
* showError 是 如果 success 为 false 且 noMsg 为 false, 则调用 showError
|
|
52
|
+
* @param res 响应
|
|
53
|
+
*/
|
|
54
|
+
export const setBaseResponse = (res: Partial<Result & { success?: boolean; showError?: (fn?: () => void) => void; noMsg?: boolean }>) => {
|
|
55
|
+
res.success = res.code === 200;
|
|
56
|
+
/**
|
|
57
|
+
* 显示错误
|
|
58
|
+
* @param fn 错误处理函数
|
|
59
|
+
*/
|
|
60
|
+
res.showError = (fn?: () => void) => {
|
|
61
|
+
if (!res.success && !res.noMsg) {
|
|
62
|
+
fn?.();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return res as Result;
|
|
66
|
+
};
|
|
67
|
+
export const wrapperError = ({ code, message }: { code?: number; message?: string }) => {
|
|
68
|
+
const result = {
|
|
69
|
+
code: code || 500,
|
|
70
|
+
success: false,
|
|
71
|
+
message: message || 'api request error',
|
|
72
|
+
showError: (fn?: () => void) => {
|
|
73
|
+
//
|
|
74
|
+
},
|
|
75
|
+
noMsg: true,
|
|
76
|
+
};
|
|
77
|
+
return result;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* const query = new Query();
|
|
81
|
+
* const res = await query.post({
|
|
82
|
+
* path: 'demo',
|
|
83
|
+
* key: '1',
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
* U是参数 V是返回值
|
|
87
|
+
*/
|
|
88
|
+
export class Query {
|
|
89
|
+
adapter: typeof adapter;
|
|
90
|
+
url: string;
|
|
91
|
+
/**
|
|
92
|
+
* 请求前处理函数
|
|
93
|
+
*/
|
|
94
|
+
beforeRequest?: DataOpts['beforeRequest'];
|
|
95
|
+
/**
|
|
96
|
+
* 请求后处理函数
|
|
97
|
+
*/
|
|
98
|
+
afterResponse?: DataOpts['afterResponse'];
|
|
99
|
+
headers?: Record<string, string>;
|
|
100
|
+
timeout?: number;
|
|
101
|
+
/**
|
|
102
|
+
* 需要突然停止请求,比如401的时候
|
|
103
|
+
*/
|
|
104
|
+
stop?: boolean;
|
|
105
|
+
// 默认不使用ws
|
|
106
|
+
qws: QueryWs;
|
|
107
|
+
isClient = false;
|
|
108
|
+
constructor(opts?: QueryOptions) {
|
|
109
|
+
this.adapter = opts?.adapter || adapter;
|
|
110
|
+
const defaultURL = opts?.isClient ? '/client/router' : '/api/router';
|
|
111
|
+
this.url = opts?.url || defaultURL;
|
|
112
|
+
this.headers = opts?.headers || {
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
};
|
|
115
|
+
this.timeout = opts?.timeout || 60000 * 3; // 默认超时时间为 60s * 3
|
|
116
|
+
}
|
|
117
|
+
setQueryWs(qws: QueryWs) {
|
|
118
|
+
this.qws = qws;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 突然停止请求
|
|
122
|
+
*/
|
|
123
|
+
setStop(stop: boolean) {
|
|
124
|
+
this.stop = stop;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 发送 get 请求,转到 post 请求
|
|
128
|
+
* T是请求类型自定义
|
|
129
|
+
* S是返回类型自定义
|
|
130
|
+
* @param params 请求参数
|
|
131
|
+
* @param options 请求配置
|
|
132
|
+
* @returns 请求结果
|
|
133
|
+
*/
|
|
134
|
+
async get<R = any, P = any>(params: Data & P, options?: DataOpts): Promise<Result<R>> {
|
|
135
|
+
return this.post(params, options);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 发送 post 请求
|
|
139
|
+
* T是请求类型自定义
|
|
140
|
+
* S是返回类型自定义
|
|
141
|
+
* @param body 请求体
|
|
142
|
+
* @param options 请求配置
|
|
143
|
+
* @returns 请求结果
|
|
144
|
+
*/
|
|
145
|
+
async post<R = any, P = any>(body: Data & P, options?: DataOpts): Promise<Result<R>> {
|
|
146
|
+
const url = options?.url || this.url;
|
|
147
|
+
const { headers, adapter, beforeRequest, afterResponse, timeout, ...rest } = options || {};
|
|
148
|
+
const _headers = { ...this.headers, ...headers };
|
|
149
|
+
const _adapter = adapter || this.adapter;
|
|
150
|
+
const _beforeRequest = beforeRequest || this.beforeRequest;
|
|
151
|
+
const _afterResponse = afterResponse || this.afterResponse;
|
|
152
|
+
const _timeout = timeout || this.timeout;
|
|
153
|
+
const req = {
|
|
154
|
+
url: url,
|
|
155
|
+
headers: _headers,
|
|
156
|
+
body,
|
|
157
|
+
timeout: _timeout,
|
|
158
|
+
...rest,
|
|
159
|
+
};
|
|
160
|
+
try {
|
|
161
|
+
if (_beforeRequest) {
|
|
162
|
+
const res = await _beforeRequest(req);
|
|
163
|
+
if (res === false) {
|
|
164
|
+
return wrapperError({
|
|
165
|
+
code: 500,
|
|
166
|
+
message: 'request is cancel',
|
|
167
|
+
// @ts-ignore
|
|
168
|
+
req: req,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
console.error('request beforeFn error', e, req);
|
|
174
|
+
return wrapperError({
|
|
175
|
+
code: 500,
|
|
176
|
+
message: 'api request beforeFn error',
|
|
177
|
+
// @ts-ignore
|
|
178
|
+
req: req,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (this.stop && !options?.noStop) {
|
|
182
|
+
const that = this;
|
|
183
|
+
await new Promise((resolve) => {
|
|
184
|
+
let timer = 0;
|
|
185
|
+
const detect = setInterval(() => {
|
|
186
|
+
if (!that.stop) {
|
|
187
|
+
clearInterval(detect);
|
|
188
|
+
resolve(true);
|
|
189
|
+
}
|
|
190
|
+
timer++;
|
|
191
|
+
if (timer > 30) {
|
|
192
|
+
console.error('request stop: timeout', req.url, timer);
|
|
193
|
+
}
|
|
194
|
+
}, 1000);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return _adapter(req).then(async (res) => {
|
|
198
|
+
try {
|
|
199
|
+
if (_afterResponse) {
|
|
200
|
+
return await _afterResponse(res, {
|
|
201
|
+
req,
|
|
202
|
+
res,
|
|
203
|
+
fetch: adapter,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return res;
|
|
208
|
+
} catch (e) {
|
|
209
|
+
console.error('request afterFn error', e, req);
|
|
210
|
+
return wrapperError({
|
|
211
|
+
code: 500,
|
|
212
|
+
message: 'api request afterFn error',
|
|
213
|
+
// @ts-ignore
|
|
214
|
+
req: req,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* 设置请求前处理,设置请求前处理函数
|
|
221
|
+
* @param fn 处理函数
|
|
222
|
+
*/
|
|
223
|
+
before(fn: DataOpts['beforeRequest']) {
|
|
224
|
+
this.beforeRequest = fn;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* 设置请求后处理,设置请求后处理函数
|
|
228
|
+
* @param fn 处理函数
|
|
229
|
+
*/
|
|
230
|
+
after(fn: DataOpts['afterResponse']) {
|
|
231
|
+
this.afterResponse = fn;
|
|
232
|
+
}
|
|
233
|
+
async fetchText(urlOrOptions?: string | QueryOpts, options?: QueryOpts): Promise<Result<any>> {
|
|
234
|
+
let _options = { ...options };
|
|
235
|
+
if (typeof urlOrOptions === 'string' && !_options.url) {
|
|
236
|
+
_options.url = urlOrOptions;
|
|
237
|
+
}
|
|
238
|
+
if (typeof urlOrOptions === 'object') {
|
|
239
|
+
_options = { ...urlOrOptions, ..._options };
|
|
240
|
+
}
|
|
241
|
+
const res = await adapter({
|
|
242
|
+
method: 'GET',
|
|
243
|
+
..._options,
|
|
244
|
+
headers: {
|
|
245
|
+
...this.headers,
|
|
246
|
+
...(_options?.headers || {}),
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
if (res && !res.code) {
|
|
250
|
+
return {
|
|
251
|
+
code: 200,
|
|
252
|
+
data: res,
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return res;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export { adapter };
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
export class BaseQuery<T extends Query = Query, R extends { queryChain?: any; query?: any } = { queryChain: any; query?: T }> {
|
|
263
|
+
query: T;
|
|
264
|
+
queryDefine: R;
|
|
265
|
+
constructor(opts?: { query?: T; queryDefine?: R; clientQuery?: T }) {
|
|
266
|
+
if (opts?.clientQuery) {
|
|
267
|
+
this.query = opts.clientQuery;
|
|
268
|
+
} else {
|
|
269
|
+
this.query = opts?.query;
|
|
270
|
+
}
|
|
271
|
+
if (opts.queryDefine) {
|
|
272
|
+
this.queryDefine = opts.queryDefine;
|
|
273
|
+
this.queryDefine.query = this.query;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
get chain(): R['queryChain'] {
|
|
277
|
+
return this.queryDefine.queryChain;
|
|
278
|
+
}
|
|
279
|
+
post<R = any, P = any>(data: P, options?: DataOpts): Promise<Result<R>> {
|
|
280
|
+
return this.query.post(data, options);
|
|
281
|
+
}
|
|
282
|
+
get<R = any, P = any>(data: P, options?: DataOpts): Promise<Result<R>> {
|
|
283
|
+
return this.query.get(data, options);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* @deprecated
|
|
289
|
+
* 前端调用后端QueryRouter, 默认路径 /client/router
|
|
290
|
+
*/
|
|
291
|
+
export class ClientQuery extends Query {
|
|
292
|
+
constructor(opts?: QueryOpts) {
|
|
293
|
+
super({ ...opts, url: opts?.url || '/client/router' });
|
|
294
|
+
}
|
|
295
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const parseUrl = (url: string) => {
|
|
2
|
+
try {
|
|
3
|
+
new URL(url);
|
|
4
|
+
} catch (e) {
|
|
5
|
+
const _url = new URL(url, location.origin);
|
|
6
|
+
return _url.href;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const parseWsUrl = (url: string) => {
|
|
11
|
+
try {
|
|
12
|
+
new URL(url);
|
|
13
|
+
return url;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
const _url = new URL(url, location.origin);
|
|
16
|
+
if (_url.protocol === 'http:') {
|
|
17
|
+
_url.protocol = 'ws:';
|
|
18
|
+
}
|
|
19
|
+
if (_url.protocol === 'https:') {
|
|
20
|
+
_url.protocol = 'wss:';
|
|
21
|
+
}
|
|
22
|
+
return _url.href;
|
|
23
|
+
}
|
|
24
|
+
};
|
package/src/ws.ts
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { createStore, StoreApi } from 'zustand/vanilla';
|
|
2
|
+
import { parseWsUrl } from './utils.ts';
|
|
3
|
+
|
|
4
|
+
type QueryWsStore = {
|
|
5
|
+
connected: boolean;
|
|
6
|
+
status: 'connecting' | 'connected' | 'disconnected';
|
|
7
|
+
setConnected: (connected: boolean) => void;
|
|
8
|
+
setStatus: (status: QuerySelectState) => void;
|
|
9
|
+
};
|
|
10
|
+
export type QuerySelectState = 'connecting' | 'connected' | 'disconnected';
|
|
11
|
+
export type QueryWsStoreListener = (newState: QueryWsStore, oldState: QueryWsStore) => void;
|
|
12
|
+
export type QueryWsOpts = {
|
|
13
|
+
url?: string;
|
|
14
|
+
store?: StoreApi<QueryWsStore>;
|
|
15
|
+
ws?: WebSocket;
|
|
16
|
+
};
|
|
17
|
+
export type WsSend<T = any, U = any> = (data: T, opts?: { isJson?: boolean; wrapper?: (data: T) => U }) => any;
|
|
18
|
+
export type WsOnMessage<T = any, U = any> = (fn: (data: U, event: MessageEvent) => void, opts?: { isJson?: boolean; selector?: (data: T) => U }) => any;
|
|
19
|
+
|
|
20
|
+
export class QueryWs {
|
|
21
|
+
url: string;
|
|
22
|
+
store: StoreApi<QueryWsStore>;
|
|
23
|
+
ws: WebSocket;
|
|
24
|
+
constructor(opts?: QueryWsOpts) {
|
|
25
|
+
const url = opts?.url || '/api/router';
|
|
26
|
+
if (opts?.store) {
|
|
27
|
+
this.store = opts.store;
|
|
28
|
+
} else {
|
|
29
|
+
const store = createStore<QueryWsStore>((set) => ({
|
|
30
|
+
connected: false,
|
|
31
|
+
status: 'connecting',
|
|
32
|
+
setConnected: (connected) => set({ connected }),
|
|
33
|
+
setStatus: (status) => set({ status }),
|
|
34
|
+
}));
|
|
35
|
+
this.store = store;
|
|
36
|
+
}
|
|
37
|
+
const wsUrl = parseWsUrl(url);
|
|
38
|
+
if (opts?.ws && opts.ws instanceof WebSocket) {
|
|
39
|
+
this.ws = opts.ws;
|
|
40
|
+
} else {
|
|
41
|
+
this.ws = new WebSocket(wsUrl);
|
|
42
|
+
}
|
|
43
|
+
this.connect();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 连接 WebSocket
|
|
47
|
+
*/
|
|
48
|
+
async connect(opts?: { timeout?: number }) {
|
|
49
|
+
const store = this.store;
|
|
50
|
+
const that = this;
|
|
51
|
+
const connected = store.getState().connected;
|
|
52
|
+
if (connected) {
|
|
53
|
+
return Promise.resolve(true);
|
|
54
|
+
}
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const ws = that.ws || new WebSocket(that.url);
|
|
57
|
+
const timeout = opts?.timeout || 5 * 60 * 1000; // 默认 5 分钟
|
|
58
|
+
let timer = setTimeout(() => {
|
|
59
|
+
const isOpen = ws.readyState === WebSocket.OPEN;
|
|
60
|
+
if (isOpen) {
|
|
61
|
+
console.log('WebSocket 连接成功 in timer');
|
|
62
|
+
resolve(true);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.error('WebSocket 连接超时', that.url);
|
|
66
|
+
resolve(false);
|
|
67
|
+
}, timeout);
|
|
68
|
+
ws.onopen = (ev) => {
|
|
69
|
+
store.getState().setConnected(true);
|
|
70
|
+
store.getState().setStatus('connected');
|
|
71
|
+
resolve(true);
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
};
|
|
74
|
+
ws.onclose = (ev) => {
|
|
75
|
+
store.getState().setConnected(false);
|
|
76
|
+
store.getState().setStatus('disconnected');
|
|
77
|
+
this.ws = null;
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* ws.onopen 必须用这个去获取,否者会丢失链接信息
|
|
83
|
+
* @param callback
|
|
84
|
+
* @returns
|
|
85
|
+
*/
|
|
86
|
+
listenConnect(callback: () => void) {
|
|
87
|
+
const store = this.store;
|
|
88
|
+
const { connected } = store.getState();
|
|
89
|
+
if (connected) {
|
|
90
|
+
callback();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const subscriptionOne = (selector: (state: QueryWsStore) => QueryWsStore['connected'], listener: QueryWsStoreListener) => {
|
|
94
|
+
const unsubscribe = store.subscribe((newState: any, oldState: any) => {
|
|
95
|
+
if (selector(newState) !== selector(oldState)) {
|
|
96
|
+
listener(newState, oldState);
|
|
97
|
+
unsubscribe();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return unsubscribe;
|
|
101
|
+
};
|
|
102
|
+
const cancel = subscriptionOne(
|
|
103
|
+
(state) => state.connected,
|
|
104
|
+
() => {
|
|
105
|
+
callback();
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
return cancel;
|
|
109
|
+
}
|
|
110
|
+
listenClose(callback: () => void) {
|
|
111
|
+
const store = this.store;
|
|
112
|
+
const { status } = store.getState();
|
|
113
|
+
if (status === 'disconnected') {
|
|
114
|
+
callback();
|
|
115
|
+
}
|
|
116
|
+
const subscriptionOne = (selector: (state: QueryWsStore) => QueryWsStore['status'], listener: QueryWsStoreListener) => {
|
|
117
|
+
const unsubscribe = store.subscribe((newState: any, oldState: any) => {
|
|
118
|
+
if (selector(newState) !== selector(oldState)) {
|
|
119
|
+
listener(newState, oldState);
|
|
120
|
+
unsubscribe();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return unsubscribe;
|
|
124
|
+
};
|
|
125
|
+
const cancel = subscriptionOne(
|
|
126
|
+
(state) => state.status,
|
|
127
|
+
(newState, oldState) => {
|
|
128
|
+
if (newState.status === 'disconnected') {
|
|
129
|
+
callback();
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
return cancel;
|
|
134
|
+
}
|
|
135
|
+
onMessage<T = any, U = any>(
|
|
136
|
+
fn: (data: U, event: MessageEvent) => void,
|
|
137
|
+
opts?: {
|
|
138
|
+
/**
|
|
139
|
+
* 是否将数据转换为 JSON
|
|
140
|
+
*/
|
|
141
|
+
isJson?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* 选择器
|
|
144
|
+
*/
|
|
145
|
+
selector?: (data: T) => U;
|
|
146
|
+
},
|
|
147
|
+
) {
|
|
148
|
+
const ws = this.ws;
|
|
149
|
+
const isJson = opts?.isJson ?? true;
|
|
150
|
+
const selector = opts?.selector;
|
|
151
|
+
const parseIfJson = (data: string) => {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(data);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
return data;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const listener = (event: MessageEvent) => {
|
|
159
|
+
const received = parseIfJson(event.data);
|
|
160
|
+
if (typeof received === 'string' && !isJson) {
|
|
161
|
+
fn(received as any, event);
|
|
162
|
+
} else if (typeof received === 'object' && isJson) {
|
|
163
|
+
fn(selector ? selector(received) : received, event);
|
|
164
|
+
} else {
|
|
165
|
+
// 过滤掉的数据
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
ws.addEventListener('message', listener);
|
|
169
|
+
return () => {
|
|
170
|
+
ws.removeEventListener('message', listener);
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
close() {
|
|
174
|
+
const ws = this.ws;
|
|
175
|
+
const store = this.store;
|
|
176
|
+
ws?.close?.();
|
|
177
|
+
this.ws = null;
|
|
178
|
+
store.getState().setConnected(false);
|
|
179
|
+
store.getState().setStatus('disconnected');
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 发送消息
|
|
183
|
+
*
|
|
184
|
+
* @param data
|
|
185
|
+
* @param opts
|
|
186
|
+
* @returns
|
|
187
|
+
*/
|
|
188
|
+
send<T = any, U = any>(
|
|
189
|
+
data: T,
|
|
190
|
+
opts?: {
|
|
191
|
+
/**
|
|
192
|
+
* 是否将数据转换为 JSON
|
|
193
|
+
*/
|
|
194
|
+
isJson?: boolean;
|
|
195
|
+
/**
|
|
196
|
+
* 包装数据
|
|
197
|
+
*/
|
|
198
|
+
wrapper?: (data: T) => U;
|
|
199
|
+
},
|
|
200
|
+
) {
|
|
201
|
+
const ws = this.ws;
|
|
202
|
+
const isJson = opts?.isJson ?? true;
|
|
203
|
+
const wrapper = opts?.wrapper;
|
|
204
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
205
|
+
console.error('WebSocket is not open');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (isJson) {
|
|
209
|
+
ws.send(JSON.stringify(wrapper ? wrapper(data) : data));
|
|
210
|
+
} else {
|
|
211
|
+
ws.send(data as string);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
getOpen() {
|
|
215
|
+
if (!this.ws) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
return this.ws.readyState === WebSocket.OPEN;
|
|
219
|
+
}
|
|
220
|
+
}
|