@jetlinks-web/core 2.2.18 → 2.3.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/src/fetch.ts CHANGED
@@ -1,241 +1,271 @@
1
- import {getToken} from "@jetlinks-web/utils";
2
- import {BASE_API, TOKEN_KEY} from "@jetlinks-web/constants";
3
- import {isFunction, isObject} from "lodash-es";
4
- import { Observable, } from 'rxjs'
1
+ import { getToken } from "@jetlinks-web/utils";
2
+ import { BASE_API, TOKEN_KEY } from "@jetlinks-web/constants";
3
+ import { isFunction, isObject } from "lodash-es";
4
+ import { Observable, Subscriber } from 'rxjs';
5
+
6
+ /**
7
+ * NdJson 配置选项
8
+ */
9
+ export interface NdJsonOptions {
10
+ /** 成功状态码 */
11
+ code?: number;
12
+ /** 状态码字段名 */
13
+ codeKey?: string;
14
+ /** 不需要 token 的 URL 列表 */
15
+ filter_url?: string[];
16
+ /** token 过期回调 */
17
+ tokenExpiration?: () => void;
18
+ /** 自定义请求配置 */
19
+ requestOptions?: (config: RequestInit) => Record<string, unknown>;
20
+ /** 自定义响应处理 */
21
+ handleResponse?: <T>(response: T) => T;
22
+ /** 基础 API 地址,默认使用 BASE_API 常量 */
23
+ baseURL?: string;
24
+ }
25
+
26
+ interface RequestContext {
27
+ controller: AbortController;
28
+ isActive: boolean;
29
+ }
30
+
31
+ type HttpMethod = 'GET' | 'POST';
5
32
 
6
33
  export class NdJson {
7
- options: any = {
34
+ private options: NdJsonOptions = {
8
35
  code: 200,
9
36
  codeKey: 'status'
10
- }
11
- isRead = false
12
- controller = null
13
- constructor() {}
37
+ };
38
+
39
+ private activeRequests = new Set<RequestContext>();
14
40
 
15
- create(options) {
16
- this.options = Object.assign(this.options, options)
41
+ constructor(options?: NdJsonOptions) {
42
+ if (options) {
43
+ this.options = { ...this.options, ...options };
44
+ }
17
45
  }
18
46
 
19
- getUrl(url) {
20
- return BASE_API + url
47
+ /**
48
+ * 初始化/更新配置
49
+ */
50
+ create(options: NdJsonOptions): void {
51
+ this.options = { ...this.options, ...options };
21
52
  }
22
53
 
23
- get(url, data = '{}', extra = {}) {
24
- const _url = this.getUrl(url)
25
- const that = this
26
- const controller = this.controller = new AbortController();
27
-
28
- return new Observable(observer => {
29
- let reader
30
- fetch(
31
- _url,
32
- {
33
- method: 'GET',
34
- signal: controller.signal,
35
- keepalive: true,
36
- ...extra,
37
- ...this.handleRequest(_url)
38
- }
39
- ).then(resp => {
40
- reader = resp.body?.getReader();
41
- const decoder = new TextDecoder();
42
- let data_buf = "";
43
-
44
- if (!reader) {
45
- observer.error(new Error('No readable stream available'));
46
- return;
47
- }
54
+ /**
55
+ * 获取完整 URL
56
+ */
57
+ private getUrl(url: string): string {
58
+ const baseURL = this.options.baseURL ?? BASE_API;
59
+ return baseURL + url;
60
+ }
48
61
 
49
- const read = () => {
62
+ /**
63
+ * 处理 NDJSON 流的核心逻辑
64
+ */
65
+ private processStream<T>(
66
+ reader: ReadableStreamDefaultReader<Uint8Array>,
67
+ observer: Subscriber<T>,
68
+ context: RequestContext
69
+ ): void {
70
+ const decoder = new TextDecoder();
71
+ let buffer = '';
72
+
73
+ const read = (): void => {
74
+ if (!context.isActive) {
75
+ reader.cancel();
76
+ observer.complete();
77
+ return;
78
+ }
50
79
 
51
- if (!that.isRead) {
52
- reader.cancel()
80
+ reader.read()
81
+ .then(({ done, value }) => {
82
+ if (done) {
83
+ this.flushBuffer(buffer, observer);
53
84
  observer.complete();
54
- return
85
+ return;
55
86
  }
56
87
 
57
- reader.read().then(({ done, value }) => {
58
- if (done) {
59
- if (data_buf.trim().length > 0) {
60
- try {
61
- observer.next(JSON.parse(data_buf.trim()));
62
- } catch (e) {
63
- observer.error(e);
64
- }
65
- }
66
- observer.complete();
67
- return;
68
- }
69
-
70
- const data = decoder.decode(value, { stream: true });
71
- data_buf += data;
72
-
73
- let lines = data_buf.split('\n');
74
- for (let i = 0; i < lines.length - 1; ++i) {
75
- const line = lines[i].trim();
76
- if (line.length > 0) {
77
- try {
78
- observer.next(JSON.parse(line.startsWith('data:') ? line.slice(5) : line));
79
- } catch (e) {
80
- observer.error(e);
81
- return;
82
- }
83
- }
84
- }
85
- data_buf = lines[lines.length - 1];
86
- read();
87
- }).catch(err => {
88
- if (err.name !== 'AbortError') {
89
- observer.error(err);
90
- }
91
- });
92
- };
93
- that.isRead = true
94
- read();
95
- }).catch(e => {
96
- observer.error(e)
97
- })
88
+ buffer += decoder.decode(value, { stream: true });
89
+ buffer = this.parseLines(buffer, observer);
90
+ read();
91
+ })
92
+ .catch(err => {
93
+ if (err.name !== 'AbortError') {
94
+ observer.error(err);
95
+ }
96
+ });
97
+ };
98
98
 
99
- return () => {
100
- that.cancel()
101
- }
102
- })
99
+ read();
103
100
  }
104
101
 
105
- post(url, data: BodyInit | any ={}, extra = {}) {
106
- const _url = this.getUrl(url)
107
- const that = this
108
- const controller = this.controller = new AbortController();
109
-
110
- return new Observable(observer => {
111
- let reader
112
- fetch(
113
- _url,
114
- {
115
- method: 'POST',
116
- signal: controller.signal,
117
- keepalive: true,
118
- body: isObject(data) ? JSON.stringify(data) : data,
119
- ...extra,
120
- ...this.handleRequest(_url)
121
- }
122
- ).then(async resp => {
123
- reader = resp.body?.getReader();
124
- const decoder = new TextDecoder();
125
- let data_buf = "";
126
-
127
- if (!reader) {
128
- observer.error(new Error('No readable stream available'));
129
- return;
102
+ /**
103
+ * 解析缓冲区中的完整行
104
+ */
105
+ private parseLines<T>(buffer: string, observer: Subscriber<T>): string {
106
+ const lines = buffer.split('\n');
107
+
108
+ for (let i = 0; i < lines.length - 1; i++) {
109
+ const line = lines[i].trim();
110
+ if (line.length > 0) {
111
+ try {
112
+ const data = line.startsWith('data:') ? line.slice(5) : line;
113
+ observer.next(JSON.parse(data));
114
+ } catch (e) {
115
+ observer.error(e);
116
+ return '';
130
117
  }
118
+ }
119
+ }
131
120
 
132
- const read = () => {
121
+ return lines[lines.length - 1];
122
+ }
133
123
 
134
- if (!that.isRead) {
135
- reader.cancel()
136
- observer.complete();
137
- return
124
+ /**
125
+ * 刷新剩余缓冲区
126
+ */
127
+ private flushBuffer<T>(buffer: string, observer: Subscriber<T>): void {
128
+ const trimmed = buffer.trim();
129
+ if (trimmed.length > 0) {
130
+ try {
131
+ observer.next(JSON.parse(trimmed));
132
+ } catch (e) {
133
+ observer.error(e);
134
+ }
135
+ }
136
+ }
137
+
138
+ /**
139
+ * 创建请求的 Observable
140
+ */
141
+ private request<T>(
142
+ method: HttpMethod,
143
+ url: string,
144
+ data?: BodyInit | Record<string, unknown>,
145
+ extra: RequestInit = {}
146
+ ): Observable<T> {
147
+ const fullUrl = this.getUrl(url);
148
+
149
+ return new Observable<T>(observer => {
150
+ const controller = new AbortController();
151
+ const context: RequestContext = {
152
+ controller,
153
+ isActive: true
154
+ };
155
+
156
+ this.activeRequests.add(context);
157
+
158
+ const requestInit: RequestInit = {
159
+ method,
160
+ signal: controller.signal,
161
+ keepalive: true,
162
+ ...extra,
163
+ ...this.handleRequest(fullUrl, method)
164
+ };
165
+
166
+ // POST 请求添加 body
167
+ if (method === 'POST' && data !== undefined) {
168
+ requestInit.body = isObject(data) ? JSON.stringify(data) : data as BodyInit;
169
+ }
170
+
171
+ fetch(fullUrl, requestInit)
172
+ .then(resp => {
173
+ const reader = resp.body?.getReader();
174
+
175
+ if (!reader) {
176
+ observer.error(new Error('No readable stream available'));
177
+ return;
138
178
  }
139
179
 
140
- reader.read().then(({ done, value }) => {
141
- if (done) {
142
- if (data_buf.trim().length > 0) {
143
- try {
144
- observer.next(JSON.parse(data_buf.trim()));
145
- } catch (e) {
146
- observer.error(e);
147
- }
148
- }
149
- observer.complete();
150
- return;
151
- }
152
-
153
- const data = decoder.decode(value, { stream: true });
154
- data_buf += data;
155
-
156
- let lines = data_buf.split('\n');
157
- for (let i = 0; i < lines.length - 1; ++i) {
158
- const line = lines[i].trim();
159
- if (line.length > 0) {
160
- try {
161
- observer.next(JSON.parse(line.startsWith('data:') ? line.slice(5) : line));
162
- } catch (e) {
163
- observer.error(e);
164
- return;
165
- }
166
- }
167
- }
168
- data_buf = lines[lines.length - 1];
169
- read();
170
- }).catch(err => {
171
- if (err.name !== 'AbortError') {
172
- observer.error(err);
173
- }
174
- });
175
- };
176
- that.isRead = true
177
- read();
178
- }).catch(e => {
179
- observer.error(e)
180
- })
180
+ context.isActive = true;
181
+ this.processStream(reader, observer, context);
182
+ })
183
+ .catch(e => {
184
+ observer.error(e);
185
+ });
181
186
 
187
+ // 返回清理函数
182
188
  return () => {
183
- that.cancel()
184
- }
185
- })
189
+ context.isActive = false;
190
+ controller.abort();
191
+ this.activeRequests.delete(context);
192
+ };
193
+ });
186
194
  }
187
- handleRequest(url): RequestInit {
188
- const config: RequestInit = {
189
- headers: {
190
- 'Content-Type': 'application/x-ndjson',
191
- }
195
+
196
+ get<T = unknown>(url: string, _data = '{}', extra: RequestInit = {}): Observable<T> {
197
+ return this.request<T>('GET', url, undefined, extra);
198
+ }
199
+
200
+ post<T = unknown>(url: string, data: BodyInit | Record<string, unknown> = {}, extra: RequestInit = {}): Observable<T> {
201
+ return this.request<T>('POST', url, data, extra);
202
+ }
203
+
204
+ private handleRequest(url: string, method: HttpMethod): RequestInit {
205
+ const headers: Record<string, string> = {};
206
+
207
+ // 只有 POST 请求才设置 Content-Type
208
+ if (method === 'POST') {
209
+ headers['Content-Type'] = 'application/x-ndjson';
192
210
  }
193
211
 
194
- const token = getToken()
212
+ const config: RequestInit = { headers };
213
+ const token = getToken();
195
214
 
196
- if (!token && this.options.filter_url?.some(_url => _url.includes(url))) {
197
- this.options.tokenExpiration?.()
198
- return config
215
+ if (!token && this.options.filter_url?.some(_url => url.includes(_url))) {
216
+ this.options.tokenExpiration?.();
217
+ return config;
199
218
  }
200
219
 
201
- if (!config.headers[TOKEN_KEY]) {
202
- config.headers[TOKEN_KEY] = token
220
+ if (token) {
221
+ headers[TOKEN_KEY] = token;
203
222
  }
204
223
 
205
224
  if (this.options.requestOptions && isFunction(this.options.requestOptions)) {
206
- const extraOptions = this.options.requestOptions(config)
225
+ const extraOptions = this.options.requestOptions(config);
207
226
  if (extraOptions && isObject(extraOptions)) {
208
- for (const key in extraOptions) {
209
- config[key] = extraOptions[key]
210
- }
227
+ Object.assign(config, extraOptions);
211
228
  }
212
229
  }
213
230
 
214
- return config
231
+ return config;
215
232
  }
216
233
 
217
- handleResponse(response) {
218
-
234
+ handleResponse<T>(response: T): T {
219
235
  if (this.options.handleResponse && isFunction(this.options.handleResponse)) {
220
- return this.options.handleResponse(response)
236
+ return this.options.handleResponse(response);
221
237
  }
222
-
223
- // const status = response[this.options.codeKey || 'status']
224
- // response.success = status === this.options.code
225
-
226
- return response
238
+ return response;
227
239
  }
228
240
 
229
- cancel() {
230
- if (this.isRead) {
231
- this.isRead = false
232
- }
233
-
234
- if (this.controller) {
235
- this.controller.abort()
236
- }
241
+ /**
242
+ * 取消所有活跃的请求
243
+ */
244
+ cancelAll(): void {
245
+ this.activeRequests.forEach(context => {
246
+ context.isActive = false;
247
+ context.controller.abort();
248
+ });
249
+ this.activeRequests.clear();
237
250
  }
238
251
  }
239
252
 
240
-
241
- export const ndJson = new NdJson()
253
+ // 默认实例
254
+ const defaultNdJson = new NdJson();
255
+
256
+ /**
257
+ * 创建新的 NdJson 实例
258
+ */
259
+ export const createNdJson = (options?: NdJsonOptions): NdJson => {
260
+ return new NdJson(options);
261
+ };
262
+
263
+ /**
264
+ * 初始化默认实例
265
+ */
266
+ export const createNdJsonService = (options: NdJsonOptions): void => {
267
+ defaultNdJson.create(options);
268
+ };
269
+
270
+ // 导出默认实例 (保持向后兼容)
271
+ export const ndJson = defaultNdJson;
package/src/type.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import type {AxiosError, AxiosResponse, InternalAxiosRequestConfig} from "axios";
2
2
 
3
+ export interface AxiosResponseRewrite<T = any>
4
+ extends AxiosResponse<T, any> {
5
+ result: T
6
+ success: boolean
7
+ }
8
+
9
+
3
10
  export interface Options {
4
- tokenExpiration: (err?: AxiosError<any>, response?: AxiosResponse) => void
11
+ tokenExpiration: (err?: AxiosError<any>, response?: AxiosResponseRewrite) => void
5
12
  handleReconnect: () => Promise<any>
6
13
  filter_url?: Array<string>
7
14
  code?: number
@@ -16,7 +23,7 @@ export interface Options {
16
23
  * response处理函数
17
24
  * @param response AxiosResponse实例
18
25
  */
19
- handleResponse?: (response: AxiosResponse) => void
26
+ handleResponse?: (response: AxiosResponseRewrite) => void
20
27
  /**
21
28
  * 错误处理函数
22
29
  * @param msg 错误消息
@@ -30,15 +37,17 @@ export interface Options {
30
37
 
31
38
  export interface ExpandRequestConfig extends InternalAxiosRequestConfig {
32
39
  __requestKey?: string
40
+ _retry?: boolean
33
41
  }
34
42
 
35
- export interface ExpandAxiosResponse extends AxiosResponse {
43
+ export interface ExpandAxiosResponse<T = any> extends AxiosResponseRewrite<T> {
36
44
  config: ExpandRequestConfig
37
45
  message: string
38
46
  }
39
47
 
40
- export interface ExpandAxiosError<T> extends AxiosError<T> {
41
- response: ExpandAxiosResponse
48
+ export interface ExpandAxiosError<T = any> extends Omit<AxiosError<T>, 'config'> {
49
+ config?: ExpandRequestConfig
50
+ response?: ExpandAxiosResponse<T>
42
51
  }
43
52
 
44
53
  export interface PageResult<T> {