@jetlinks-web/core 2.0.8 → 2.1.0
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/package.json +4 -3
- package/src/axios.ts +126 -0
- package/src/websocket.ts +243 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetlinks-web/core",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
"license": "ISC",
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"axios": "^1.7.4",
|
|
19
|
-
"
|
|
19
|
+
"rxjs": "^7.8.1",
|
|
20
20
|
"@jetlinks-web/constants": "^1.0.7",
|
|
21
|
-
"@jetlinks-web/utils": "^1.2.5"
|
|
21
|
+
"@jetlinks-web/utils": "^1.2.5",
|
|
22
|
+
"@jetlinks-web/types": "^1.0.2"
|
|
22
23
|
},
|
|
23
24
|
"publishConfig": {
|
|
24
25
|
"registry": "https://registry.npmjs.org/",
|
package/src/axios.ts
CHANGED
|
@@ -38,6 +38,14 @@ interface Options {
|
|
|
38
38
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
interface RequestOptions {
|
|
42
|
+
url?: string
|
|
43
|
+
method?: string
|
|
44
|
+
params?: any
|
|
45
|
+
data?: any
|
|
46
|
+
[key: string]: any
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
let instance: AxiosInstance
|
|
42
50
|
|
|
43
51
|
let _options: Options = {
|
|
@@ -223,5 +231,123 @@ export const request = {
|
|
|
223
231
|
post, get, put, patch, remove, getStream, postStream
|
|
224
232
|
}
|
|
225
233
|
|
|
234
|
+
export class Request {
|
|
235
|
+
modulePath: string
|
|
236
|
+
|
|
237
|
+
constructor(modulePath: string) {
|
|
238
|
+
this.modulePath = modulePath
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 分页查询
|
|
243
|
+
* @param {object} data 查询参数
|
|
244
|
+
* @param {object} options 请求配置
|
|
245
|
+
* @returns {Promise<AxiosResponse<any>>} 分页查询结果
|
|
246
|
+
*/
|
|
247
|
+
page(data: any, options: RequestOptions= {
|
|
248
|
+
url: undefined,
|
|
249
|
+
method: undefined,
|
|
250
|
+
}) {
|
|
251
|
+
const { url='/_query', method = 'post', ...rest } = options
|
|
252
|
+
return request[method](`${this.modulePath}${url}`, data, rest)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 不分页查询
|
|
257
|
+
* @param {object} data 查询参数
|
|
258
|
+
* @param {object} options 请求配置
|
|
259
|
+
* @returns {Promise<AxiosResponse<any>>} 不分页查询结果
|
|
260
|
+
*/
|
|
261
|
+
noPage(data?: any, options: RequestOptions = {
|
|
262
|
+
url: undefined,
|
|
263
|
+
method: undefined,
|
|
264
|
+
}) {
|
|
265
|
+
const { url='/_query/no-page', method = 'post', ...rest } = options
|
|
266
|
+
return request[method](`${this.modulePath}${url}`, { paging: false, ...data}, rest)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* 详情查询
|
|
271
|
+
* @param {string} id 详情ID
|
|
272
|
+
* @param {object} params 查询参数
|
|
273
|
+
* @param {object} options 请求配置
|
|
274
|
+
* @returns {Promise<AxiosResponse<any>>} 详情查询结果
|
|
275
|
+
*/
|
|
276
|
+
detail(id: string, params?: any, options: RequestOptions= {
|
|
277
|
+
url: undefined,
|
|
278
|
+
method: undefined,
|
|
279
|
+
}) {
|
|
280
|
+
const { url=`/${id}/detail`, method = 'get', ...rest } = options
|
|
281
|
+
return request[method](`${this.modulePath}${url}`, params, rest)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 保存
|
|
286
|
+
* @param {object} data 保存参数
|
|
287
|
+
* @param {object} options 请求配置
|
|
288
|
+
* @returns {Promise<AxiosResponse<any>>} 保存结果
|
|
289
|
+
*/
|
|
290
|
+
save(data: any, options: RequestOptions = {
|
|
291
|
+
url: undefined,
|
|
292
|
+
method: undefined,
|
|
293
|
+
}) {
|
|
294
|
+
const { url=``, method = 'post', ...rest } = options
|
|
295
|
+
return request[method](`${this.modulePath}${url}`, data, rest)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 更新
|
|
300
|
+
* @param {object} data 更新参数
|
|
301
|
+
* @param {object} options 请求配置
|
|
302
|
+
* @returns {Promise<AxiosResponse<any>>} 更新结果
|
|
303
|
+
*/
|
|
304
|
+
update(data: any, options: RequestOptions = {
|
|
305
|
+
url: undefined,
|
|
306
|
+
method: undefined,
|
|
307
|
+
}) {
|
|
308
|
+
const { url=``, method = 'post', ...rest } = options
|
|
309
|
+
return patch(`${this.modulePath}${url}`, data, rest)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 删除
|
|
314
|
+
* @param {string} id 删除ID
|
|
315
|
+
* @param {object} options 请求配置
|
|
316
|
+
* @returns {Promise<AxiosResponse<any>>} 删除结果
|
|
317
|
+
*/
|
|
318
|
+
delete(id: string, params?: any, options: RequestOptions = {
|
|
319
|
+
url: undefined,
|
|
320
|
+
method: undefined,
|
|
321
|
+
}) {
|
|
322
|
+
const { url=`/${id}`, method = 'post', ...rest } = options
|
|
323
|
+
return remove(`${this.modulePath}${url}`, params, rest)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
post(...args) {
|
|
327
|
+
const [url, data, options] = args
|
|
328
|
+
return post(`${this.modulePath}${url}`, data, options)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
get(...args) {
|
|
332
|
+
const [url, params, options] = args
|
|
333
|
+
return get(`${this.modulePath}${url}`, params, options)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
put(...args) {
|
|
337
|
+
const [url, data, options] = args
|
|
338
|
+
return put(`${this.modulePath}${url}`, data, options)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
patch(...args) {
|
|
342
|
+
const [url, data, options] = args
|
|
343
|
+
return patch(`${this.modulePath}${url}`, data, options)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
remove(...args) {
|
|
347
|
+
const [url, params, options] = args
|
|
348
|
+
return remove(`${this.modulePath}${url}`, params, options)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
226
352
|
|
|
227
353
|
|
package/src/websocket.ts
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
|
|
2
|
+
import { Observable, Subject, timer, Subscription, EMPTY } from 'rxjs';
|
|
3
|
+
import { retry, catchError } from 'rxjs/operators';
|
|
4
|
+
import { notification } from 'ant-design-vue';
|
|
5
|
+
|
|
6
|
+
interface WebSocketMessage {
|
|
7
|
+
type: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
topic?: string;
|
|
10
|
+
parameter?: Record<string, any>;
|
|
11
|
+
requestId?: string;
|
|
12
|
+
message?: string;
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type WS_Options = {
|
|
17
|
+
onError?: (message: WebSocketMessage) => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class WebSocketClient {
|
|
21
|
+
private ws: WebSocketSubject<WebSocketMessage> | null = null;
|
|
22
|
+
private subscriptions = new Map<string, Subject<WebSocketMessage>>();
|
|
23
|
+
private pendingSubscriptions = new Map<string, Subject<WebSocketMessage>>();
|
|
24
|
+
private heartbeatSubscription: Subscription | null = null;
|
|
25
|
+
private reconnectAttempts = 0;
|
|
26
|
+
private readonly maxReconnectAttempts = 100;
|
|
27
|
+
private isConnected = false;
|
|
28
|
+
private tempQueue: WebSocketMessage[] = []; // 缓存消息队列
|
|
29
|
+
private url: string = '';
|
|
30
|
+
private options: WS_Options = {}
|
|
31
|
+
|
|
32
|
+
constructor(options?: WS_Options) {
|
|
33
|
+
this.options = options || {};
|
|
34
|
+
this.setupConnectionMonitor();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public initWebSocket(url: string) {
|
|
38
|
+
this.url = url;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private setupConnectionMonitor() {
|
|
42
|
+
window.addEventListener('online', () => {
|
|
43
|
+
console.log('Network is online, attempting to reconnect...');
|
|
44
|
+
this.reconnect();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
window.addEventListener('offline', () => {
|
|
48
|
+
console.log('Network is offline, caching subscriptions...');
|
|
49
|
+
this.cacheSubscriptions();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
window.addEventListener('beforeunload', () => {
|
|
53
|
+
this.disconnect();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private getReconnectDelay(): number {
|
|
58
|
+
if (this.reconnectAttempts <= 10) {
|
|
59
|
+
return 5000; // 5s
|
|
60
|
+
} else if (this.reconnectAttempts <= 20) {
|
|
61
|
+
return 15000; // 15s
|
|
62
|
+
}
|
|
63
|
+
return 60000; // 60s
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private setupWebSocket() {
|
|
67
|
+
if (this.ws || !this.url) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.ws = webSocket<WebSocketMessage>({
|
|
72
|
+
url: this.url,
|
|
73
|
+
openObserver: {
|
|
74
|
+
next: () => {
|
|
75
|
+
console.log('WebSocket connected');
|
|
76
|
+
this.isConnected = true;
|
|
77
|
+
this.reconnectAttempts = 0;
|
|
78
|
+
this.startHeartbeat();
|
|
79
|
+
this.restoreSubscriptions();
|
|
80
|
+
this.processTempQueue();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
closeObserver: {
|
|
84
|
+
next: () => {
|
|
85
|
+
console.log('WebSocket disconnected');
|
|
86
|
+
this.isConnected = false;
|
|
87
|
+
this.cacheSubscriptions();
|
|
88
|
+
this.stopHeartbeat();
|
|
89
|
+
this.reconnect();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.ws.pipe(
|
|
95
|
+
catchError(error => {
|
|
96
|
+
console.error('WebSocket error:', error);
|
|
97
|
+
return EMPTY;
|
|
98
|
+
}),
|
|
99
|
+
retry({
|
|
100
|
+
delay: (error, retryCount) => {
|
|
101
|
+
this.reconnectAttempts = retryCount;
|
|
102
|
+
if (retryCount > this.maxReconnectAttempts) {
|
|
103
|
+
throw new Error('Max reconnection attempts reached');
|
|
104
|
+
}
|
|
105
|
+
return timer(this.getReconnectDelay());
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
).subscribe(
|
|
109
|
+
(message) => this.handleMessage(message),
|
|
110
|
+
(error) => console.error('WebSocket error:', error)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private startHeartbeat() {
|
|
115
|
+
this.stopHeartbeat();
|
|
116
|
+
this.heartbeatSubscription = timer(0, 2000).subscribe(() => {
|
|
117
|
+
this.send({ type: 'ping' });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private stopHeartbeat() {
|
|
122
|
+
if (this.heartbeatSubscription) {
|
|
123
|
+
this.heartbeatSubscription.unsubscribe();
|
|
124
|
+
this.heartbeatSubscription = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private handleMessage(message: WebSocketMessage) {
|
|
129
|
+
if (message.type === 'pong') {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (message.type === 'error') {
|
|
134
|
+
if (this.options.onError) {
|
|
135
|
+
this.options.onError(message)
|
|
136
|
+
} else {
|
|
137
|
+
notification.error({ key: 'error', message: message.message });
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const subscriber = this.subscriptions.get(message.requestId || '');
|
|
143
|
+
if (subscriber) {
|
|
144
|
+
if (message.type === 'complete') {
|
|
145
|
+
subscriber.complete();
|
|
146
|
+
this.subscriptions.delete(message.requestId || '');
|
|
147
|
+
} else if (message.type === 'result') {
|
|
148
|
+
subscriber.next(message);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private processTempQueue() {
|
|
154
|
+
while (this.tempQueue.length > 0) {
|
|
155
|
+
const message = this.tempQueue.shift();
|
|
156
|
+
if (message) {
|
|
157
|
+
this.send(message);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private cacheSubscriptions() {
|
|
163
|
+
this.pendingSubscriptions = new Map(this.subscriptions);
|
|
164
|
+
this.subscriptions.clear();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private restoreSubscriptions() {
|
|
168
|
+
this.pendingSubscriptions.forEach((subject, id) => {
|
|
169
|
+
this.subscriptions.set(id, subject);
|
|
170
|
+
});
|
|
171
|
+
this.pendingSubscriptions.clear();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private reconnect() {
|
|
175
|
+
if (!this.isConnected && navigator.onLine) {
|
|
176
|
+
this.ws = null;
|
|
177
|
+
this.setupWebSocket();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public connect() {
|
|
182
|
+
this.setupWebSocket();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public disconnect() {
|
|
186
|
+
if (this.ws) {
|
|
187
|
+
this.ws.complete();
|
|
188
|
+
this.ws = null;
|
|
189
|
+
}
|
|
190
|
+
this.stopHeartbeat();
|
|
191
|
+
this.subscriptions.clear();
|
|
192
|
+
this.pendingSubscriptions.clear();
|
|
193
|
+
this.tempQueue = [];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public send(message: WebSocketMessage) {
|
|
197
|
+
if (this.ws && this.isConnected) {
|
|
198
|
+
this.ws.next(message);
|
|
199
|
+
} else {
|
|
200
|
+
this.tempQueue.push(message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
public getWebSocket(id: string, topic: string, parameter: Record<string, any> = {}): Observable<WebSocketMessage> {
|
|
205
|
+
const subject = new Subject<WebSocketMessage>();
|
|
206
|
+
this.subscriptions.set(id, subject);
|
|
207
|
+
|
|
208
|
+
const message: WebSocketMessage = {
|
|
209
|
+
id,
|
|
210
|
+
topic,
|
|
211
|
+
parameter,
|
|
212
|
+
type: 'sub'
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
this.send(message);
|
|
216
|
+
|
|
217
|
+
return new Observable(subscriber => {
|
|
218
|
+
const subscription = subject.subscribe(subscriber);
|
|
219
|
+
|
|
220
|
+
return () => {
|
|
221
|
+
subscription.unsubscribe();
|
|
222
|
+
this.send({ id, type: 'unsub' });
|
|
223
|
+
this.subscriptions.delete(id);
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 创建单例
|
|
231
|
+
* @example
|
|
232
|
+
* wsClient.initWebSocket('ws://example.com/ws');
|
|
233
|
+
* wsClient.connect();
|
|
234
|
+
* const subscription = wsClient.getWebSocket('id1', 'topic1', { param: 'value' })
|
|
235
|
+
* .subscribe(
|
|
236
|
+
* message => console.log('Received:', message)
|
|
237
|
+
* );
|
|
238
|
+
*
|
|
239
|
+
* // 清理
|
|
240
|
+
* subscription.unsubscribe();
|
|
241
|
+
*
|
|
242
|
+
*/
|
|
243
|
+
export const wsClient = new WebSocketClient();
|