@jetlinks-web/core 2.0.8 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jetlinks-web/core",
3
- "version": "2.0.8",
3
+ "version": "2.1.1",
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
- "@jetlinks-web/types": "^1.0.2",
19
+ "rxjs": "^7.8.1",
20
20
  "@jetlinks-web/constants": "^1.0.7",
21
- "@jetlinks-web/utils": "^1.2.5"
21
+ "@jetlinks-web/types": "^1.0.2",
22
+ "@jetlinks-web/utils": "^1.2.6"
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
 
@@ -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();