@jetlinks-web/core 2.3.1 → 2.3.2
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/index.d.ts +6 -1
- package/dist/index.mjs +3 -2
- package/package.json +3 -3
- package/src/fetch.ts +351 -271
package/dist/index.d.ts
CHANGED
|
@@ -256,6 +256,7 @@ interface NdJsonOptions {
|
|
|
256
256
|
/** 基础 API 地址,默认使用 BASE_API 常量 */
|
|
257
257
|
baseURL?: string;
|
|
258
258
|
}
|
|
259
|
+
type RequestData = BodyInit | Record<string, unknown>;
|
|
259
260
|
declare class NdJson {
|
|
260
261
|
private options;
|
|
261
262
|
private activeRequests;
|
|
@@ -280,14 +281,18 @@ declare class NdJson {
|
|
|
280
281
|
* 刷新剩余缓冲区
|
|
281
282
|
*/
|
|
282
283
|
private flushBuffer;
|
|
284
|
+
private emitLine;
|
|
283
285
|
/**
|
|
284
286
|
* 创建请求的 Observable
|
|
285
287
|
*/
|
|
286
288
|
private request;
|
|
287
289
|
get<T = unknown>(url: string, _data?: string, extra?: RequestInit): Observable<T>;
|
|
288
|
-
post<T = unknown>(url: string, data?:
|
|
290
|
+
post<T = unknown>(url: string, data?: RequestData, extra?: RequestInit): Observable<T>;
|
|
289
291
|
private handleRequest;
|
|
290
292
|
handleResponse<T>(response: T): T;
|
|
293
|
+
private mergeRequestInit;
|
|
294
|
+
private mergeHeaders;
|
|
295
|
+
private isAbortError;
|
|
291
296
|
/**
|
|
292
297
|
* 取消所有活跃的请求
|
|
293
298
|
*/
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import{TOKEN_KEY as
|
|
2
|
-
`);for(
|
|
1
|
+
import{TOKEN_KEY as d,BASE_API as A,LOCAL_BASE_API as E}from"@jetlinks-web/constants";import{getToken as w,randomString as O}from"@jetlinks-web/utils";import b from"axios";import{isFunction as R,isObject as k}from"lodash-es";var f=class{instance=null;options;failedQueue=[];isRefreshing=!1;pendingRequests=new Map;isApp=window.__MICRO_APP_ENVIRONMENT__;constructor(e){this.options={filter_url:[],code:200,codeKey:"status",timeout:1e3*15,handleRequest:void 0,handleResponse:void 0,handleError:void 0,langKey:"lang",requestOptions:t=>({}),tokenExpiration:()=>{},handleReconnect:()=>Promise.resolve(),isCreateTokenRefresh:!1,cancelDuplicateRequests:!1,...e},window.JetlinksCore?.instance&&(this.instance=window.JetlinksCore.instance)}initialize(e){e&&(this.options={...this.options,...e}),this.instance=b.create({withCredentials:!1,timeout:this.options.timeout,baseURL:A}),this.instance.interceptors.request.use(t=>this.handleRequest(t),t=>this.errorHandler(t)),this.instance.interceptors.response.use(t=>this.handleResponse(t),t=>this.errorHandler(t))}getInstance(){return this.instance||this.initialize(),this.instance}generateRequestKey(e){let t=e.method?.toUpperCase()||"GET",s=e.url||"",n=e.params||{},i=e.data||{},r=t==="GET"?n:i,p="";try{p=JSON.stringify(r,Object.keys(r).sort())}catch{p=O(16)}return`${t}:${s}:${p}`}requestRecords(e){if(!this.options.cancelDuplicateRequests)return;let t=this.generateRequestKey(e);this.pendingRequests.has(t)&&(this.pendingRequests.get(t)?.abort(),this.pendingRequests.delete(t));let s=new AbortController;e.signal=s.signal,e.__requestKey=t,this.pendingRequests.set(t,s)}handleRequest(e){this.requestRecords(e);let t=w(),s=localStorage.getItem(this.options.langKey),n=localStorage.getItem(E);if(s&&(e.headers[this.options.langKey]=s),n&&!e.baseURL){let i=e.url.startsWith("/")?e.url:`/${e.url}`;e.url=n+i}if(!t&&!this.options.filter_url?.some(i=>e.url?.includes(i)))return this.options.tokenExpiration?.(),e;if(e.headers[d]||(e.headers[d]=t),this.options.requestOptions&&R(this.options.requestOptions)){let i=this.options.requestOptions(e);if(i&&k(i))for(let r in i)e[r]=i[r]}return e}handleResponse(e){let t=e.config?.__requestKey;if(t&&this.pendingRequests.delete(t),this.options.handleResponse&&R(this.options.handleResponse))return this.options.handleResponse(e);if(e.data instanceof ArrayBuffer)return e;let s=e.data[this.options.codeKey||"status"];return typeof e.data=="object"&&typeof e.data.success>"u"&&(e.data.success=s===this.options.code),e.data}async createTokenRefreshHandler(e){let t=e.config;if(this.isRefreshing)return new Promise((s,n)=>{this.failedQueue.push({resolve:s,reject:n})}).then(s=>t.signal?.aborted?Promise.reject(new b.Cancel("Request aborted")):(t.headers[d]=s,this.instance(t))).catch(s=>Promise.reject(s));t._retry=!0,this.isRefreshing=!0;try{if(await this.options.handleReconnect?.()){let n=w();return t.headers[d]=n,this.failedQueue.forEach(i=>i.resolve(n)),this.instance(t)}}catch(s){throw this.failedQueue.forEach(n=>n.reject(s)),s}finally{this.failedQueue=[],this.isRefreshing=!1}}async errorHandler(e){let t=e.config?.__requestKey;if(t&&this.pendingRequests.delete(t),b.isCancel(e))return Promise.reject(e);let s=e.response?.message||"Error",n=0,i=e.response;if(i){let{data:r,status:p}=i;switch(n=p,p){case 400:case 403:case 500:s=`${r?.message}`.substring(0,90);break;case 401:if(s=r?.result?.text||"\u7528\u6237\u672A\u767B\u5F55",this.options.tokenExpiration?.(e),this.options.isCreateTokenRefresh)return this.createTokenRefreshHandler(e);break;case 404:s=r?.message||`${r?.error} ${r?.path}`;break;default:break}}else{let r=e;r.message&&(s=r.message.includes("timeout")?"\u63A5\u53E3\u54CD\u5E94\u8D85\u65F6":r.message,n="timeout")}if(this.options.handleError&&R(this.options.handleError)){let r=this.options.handleError(s,n,e);if(r&&typeof r.then=="function")return r}return Promise.reject(e)}abortAllRequests(){this.pendingRequests.forEach(e=>e.abort()),this.pendingRequests.clear()}abortRequest(e){let t=this.pendingRequests.get(e);t&&(t.abort(),this.pendingRequests.delete(e))}getPendingRequestsCount(){return this.pendingRequests.size}post(e,t={},s){return this.getInstance()({method:"POST",url:e,data:t,...s})}get(e,t=void 0,s){return this.getInstance()({method:"GET",url:e,params:t,...s})}put(e,t={},s){return this.getInstance()({method:"PUT",url:e,data:t,...s})}patch(e,t={},s){return this.getInstance()({method:"PATCH",url:e,data:t,...s})}remove(e,t=void 0,s){return this.getInstance()({method:"DELETE",url:e,params:t,...s})}getStream(e,t,s){return this.get(e,t,{responseType:"arraybuffer",...s})}postStream(e,t,s){return this.post(e,t,{responseType:"arraybuffer",...s})}},o=new f,V=a=>{let e=new f(a);return e.initialize(),e},x=class{constructor(e,t){this.basePath=e;this.basePath=e.startsWith("/")?e:`/${e}`,this._instance=t}_instance;get instance(){return this._instance||o.getInstance()}requestWrapper(e,t,s={},n={}){let{url:i=e,method:r=t,...p}=n;return this[r].call(this,i,s,p)}page(e={},t={url:void 0,method:void 0}){return this.requestWrapper("/_query","post",e,t)}noPage(e={},t={url:void 0,method:void 0}){return this.requestWrapper("/_query/no-paging","post",{paging:!1,...e},t)}detail(e,t,s={url:void 0,method:void 0}){return this.requestWrapper(`/${e}/detail`,"get",t,s)}save(e={},t={url:void 0,method:void 0}){return this.requestWrapper("","post",e,t)}update(e={},t={url:void 0,method:void 0}){return this.requestWrapper("","patch",e,t)}delete(e,t,s={url:void 0,method:void 0}){return this.requestWrapper(`/${e}`,"remove",t,s)}batch(e={},t,s){let n=`/_batch${t?"/"+t:""}`;return this.requestWrapper(n,"post",e,s)}post(e,t,s){return this.instance({method:"POST",url:`${this.basePath}${e}`,data:t,...s})}get(e,t,s){return this.instance({method:"GET",url:`${this.basePath}${e}`,params:t,...s})}put(e,t,s){return this.instance({method:"PUT",url:`${this.basePath}${e}`,data:t,...s})}patch(e,t,s){return this.instance({method:"PATCH",url:`${this.basePath}${e}`,data:t,...s})}remove(e,t,s){return this.instance({method:"DELETE",url:`${this.basePath}${e}`,params:t,...s})}getStream(e,t,s){return this.get(`${e}`,t,{responseType:"arraybuffer",...s})}postStream(e,t,s){return this.post(`${e}`,t,{responseType:"arraybuffer",...s})}},X={post:o.post.bind(o),get:o.get.bind(o),put:o.put.bind(o),patch:o.patch.bind(o),remove:o.remove.bind(o),getStream:o.getStream.bind(o),postStream:o.postStream.bind(o)},Z=o.post.bind(o),ee=o.get.bind(o),te=o.put.bind(o),se=o.patch.bind(o),ne=o.remove.bind(o),ie=o.getStream.bind(o),re=o.postStream.bind(o),oe=()=>o.abortAllRequests(),ae=()=>o.getInstance(),P,pe=a=>{o.initialize(a),P=o.getInstance()};import{getToken as C}from"@jetlinks-web/utils";import{BASE_API as _,TOKEN_KEY as I}from"@jetlinks-web/constants";import{Observable as W}from"rxjs";var M="application/x-ndjson",S=a=>typeof a=="object"&&a!==null,N=a=>{if(!S(a))return!1;let e=Object.getPrototypeOf(a);return e===Object.prototype||e===null},T=a=>typeof a=="function",g=class{options={code:200,codeKey:"status"};activeRequests=new Set;constructor(e){e&&(this.options={...this.options,...e})}create(e){this.options={...this.options,...e}}getUrl(e){return(this.options.baseURL??_)+e}processStream(e,t,s){let n=new TextDecoder,i="";(async()=>{try{for(;s.isActive;){let{done:p,value:u}=await e.read();if(p){let h=n.decode();h&&(i+=h),this.flushBuffer(i,t),t.closed||t.complete();return}if(i+=n.decode(u,{stream:!0}),i=this.parseLines(i,t),t.closed)return}}catch(p){!this.isAbortError(p)&&!t.closed&&t.error(p)}})()}parseLines(e,t){let s=0,n=e.indexOf(`
|
|
2
|
+
`);for(;n!==-1;){let i=e.slice(s,n).trim();if(i.length>0&&!this.emitLine(i,t))return"";s=n+1,n=e.indexOf(`
|
|
3
|
+
`,s)}return e.slice(s)}flushBuffer(e,t){let s=e.trim();s.length>0&&this.emitLine(s,t)}emitLine(e,t){let s=e.startsWith("data:")?e.slice(5).trimStart():e;try{return t.next(this.handleResponse(JSON.parse(s))),!0}catch(n){return t.error(n),!1}}request(e,t,s,n={}){let i=this.getUrl(t);return new W(r=>{let p=new AbortController,u={controller:p,isActive:!0};this.activeRequests.add(u);let h=this.mergeRequestInit({method:e,signal:p.signal,keepalive:!0},this.handleRequest(i,e),n,{method:e,signal:p.signal});return e==="POST"&&s!==void 0&&(h.body=N(s)?JSON.stringify(s):s),fetch(i,h).then(l=>{if(l.status!==this.options.code){this.isAbortError(l)||r.error(l);return}let y=l.body?.getReader();if(!y){r.error(new Error("No readable stream available"));return}u.isActive=!0,this.processStream(y,r,u)}).catch(l=>{this.isAbortError(l)||r.error(l)}),()=>{u.isActive=!1,p.abort(),this.activeRequests.delete(u)}})}get(e,t="{}",s={}){return this.request("GET",e,void 0,s)}post(e,t={},s={}){return this.request("POST",e,t,s)}handleRequest(e,t){let s={};t==="POST"&&(s["Content-Type"]=M);let n={headers:s},i=C();if(!i&&!this.options.filter_url?.some(r=>e.includes(r)))return this.options.tokenExpiration?.(),n;if(i&&(s[I]=i),T(this.options.requestOptions)){let r=this.options.requestOptions(n);if(S(r))return this.mergeRequestInit(n,r)}return n}handleResponse(e){return T(this.options.handleResponse)?this.options.handleResponse(e):e}mergeRequestInit(...e){let t={},s=new Headers,n=!1;return e.forEach(i=>{if(!i)return;let{headers:r,...p}=i;Object.assign(t,p),n=this.mergeHeaders(s,r)||n}),n&&(t.headers=s),t}mergeHeaders(e,t){if(!t)return!1;let s=!1;return new Headers(t).forEach((n,i)=>{e.set(i,n),s=!0}),s}isAbortError(e){return e instanceof Error&&e.name==="AbortError"}cancelAll(){this.activeRequests.forEach(e=>{e.isActive=!1,e.controller.abort()}),this.activeRequests.clear()}},q=new g,fe=a=>new g(a),ge=a=>{q.create(a)},be=q;import{webSocket as H}from"rxjs/webSocket";import{Observable as j,Subject as $,timer as v,EMPTY as L}from"rxjs";import{retry as J,catchError as D}from"rxjs/operators";import{notification as K}from"ant-design-vue";var c=window.__MICRO_APP_ENVIRONMENT__,m=class{ws=null;subscriptions=new Map;pendingSubscriptions=new Map;heartbeatSubscription=null;reconnectAttempts=0;maxReconnectAttempts=2;isConnected=!1;tempQueue=[];url="";options={};wsClient;constructor(e){this.setOptions(e),this.setupConnectionMonitor(),c&&window.microApp.addGlobalDataListener(t=>{this.wsClient=t.wsClient})}setOptions(e){this.options=e||{}}initWebSocket(e){this.url=e}setupConnectionMonitor(){c||(window.addEventListener("online",()=>{console.log("Network is online, attempting to reconnect..."),this.reconnect()}),window.addEventListener("offline",()=>{console.log("Network is offline, caching subscriptions..."),this.cacheSubscriptions()}),window.addEventListener("beforeunload",()=>{this.disconnect()}))}getReconnectDelay(){return this.reconnectAttempts<=10?5e3:this.reconnectAttempts<=20?15e3:6e4}setupWebSocket(){if(c&&this.wsClient){this.wsClient.setupWebSocket();return}this.ws||!this.url||(this.ws=H({url:this.url,openObserver:{next:()=>{console.log("WebSocket connected"),this.isConnected=!0,this.reconnectAttempts=0,this.startHeartbeat(),this.restoreSubscriptions(),this.processTempQueue()}},closeObserver:{next:()=>{console.log("WebSocket disconnected"),this.isConnected=!1;let e=this.getReconnectDelay();setTimeout(()=>{this.reconnectAttempts+=1,!(this.reconnectAttempts>this.maxReconnectAttempts)&&(this.cacheSubscriptions(),this.stopHeartbeat(),this.reconnect())},e)}}}),this.ws.pipe(D(e=>(console.error("WebSocket error:",e),L)),J({delay:(e,t)=>{if(this.reconnectAttempts=t,t>this.maxReconnectAttempts)throw new Error("Max reconnection attempts reached");return v(this.getReconnectDelay())}})).subscribe(e=>this.handleMessage(e),e=>console.error("WebSocket error:",e)))}startHeartbeat(){if(c&&this.wsClient){this.wsClient.startHeartbeat();return}this.stopHeartbeat(),this.heartbeatSubscription=v(0,2e3).subscribe(()=>{this.send({type:"ping"})})}stopHeartbeat(){if(c&&this.wsClient){this.wsClient.stopHeartbeat();return}this.heartbeatSubscription&&(this.heartbeatSubscription.unsubscribe(),this.heartbeatSubscription=null)}handleMessage(e){if(c&&this.wsClient){this.wsClient.handleMessage(e);return}if(e.type==="pong")return;if(e.type==="error"){this.options.onError?this.options.onError(e):K.error({key:"error",message:e.message});return}let t=this.subscriptions.get(e.requestId||"");t&&(e.type==="complete"?(t.complete(),this.subscriptions.delete(e.requestId||"")):e.type==="result"&&t.next(e))}processTempQueue(){if(c&&this.wsClient){this.wsClient.processTempQueue();return}for(;this.tempQueue.length>0;){let e=this.tempQueue.shift();e&&this.send(e)}}cacheSubscriptions(){if(c&&this.wsClient){this.wsClient.cacheSubscriptions();return}this.pendingSubscriptions=new Map(this.subscriptions),this.subscriptions.clear()}restoreSubscriptions(){if(c&&this.wsClient){this.wsClient.restoreSubscriptions();return}this.pendingSubscriptions.forEach((e,t)=>{this.subscriptions.set(t,e)}),this.pendingSubscriptions.clear()}reconnect(){if(c&&this.wsClient){this.wsClient.reconnect();return}!this.isConnected&&navigator.onLine&&(this.ws=null,this.setupWebSocket())}connect(){if(c&&this.wsClient){this.wsClient.connect();return}this.setupWebSocket()}disconnect(){if(c&&this.wsClient){this.wsClient.disconnect();return}this.ws&&(this.ws.complete(),this.ws=null),this.stopHeartbeat(),this.subscriptions.clear(),this.pendingSubscriptions.clear(),this.tempQueue=[]}send(e){if(c&&this.wsClient){this.wsClient.send(e);return}this.ws&&this.isConnected?this.ws.next(e):this.tempQueue.push(e)}getWebSocket(e,t,s={}){if(console.log("getWebSocket",this.wsClient,e),c&&this.wsClient)return this.wsClient.getWebSocket(e,t,s);let n=new $;this.subscriptions.set(e,n);let i={id:e,topic:t,parameter:s,type:"sub"};return this.send(i),new j(r=>{let p=n.subscribe(r);return()=>{p.unsubscribe(),this.send({id:e,type:"unsub"}),this.subscriptions.delete(e)}})}},Se=new m;var U,ve=a=>{U=a};var Q={},Ee=(a={})=>{Q=a};var B,ke=a=>{B=a};export{f as AxiosService,g as NdJson,x as Request,m as WebSocketClient,oe as abortAllRequests,pe as crateAxios,V as createAxiosService,fe as createNdJson,ge as createNdJsonService,ee as get,ae as getInstance,ie as getStream,ke as installLocales,ve as installRouter,Ee as installStores,P as instance,B as locales,be as ndJson,se as patch,Z as post,re as postStream,te as put,ne as remove,X as request,U as router,Q as stores,Se as wsClient};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jetlinks-web/core",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"main": "dist/index.mjs",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"axios": "^1.7.4",
|
|
30
30
|
"rxjs": "^7.8.1",
|
|
31
31
|
"@jetlinks-web/constants": "^1.0.9",
|
|
32
|
-
"@jetlinks-web/
|
|
33
|
-
"@jetlinks-web/
|
|
32
|
+
"@jetlinks-web/types": "^1.0.2",
|
|
33
|
+
"@jetlinks-web/utils": "^1.3.0"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"registry": "https://registry.npmjs.org/",
|
package/src/fetch.ts
CHANGED
|
@@ -1,271 +1,351 @@
|
|
|
1
|
-
import { getToken } from "@jetlinks-web/utils";
|
|
2
|
-
import { BASE_API, TOKEN_KEY } from "@jetlinks-web/constants";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
type
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1
|
+
import { getToken } from "@jetlinks-web/utils";
|
|
2
|
+
import { BASE_API, TOKEN_KEY } from "@jetlinks-web/constants";
|
|
3
|
+
import { Observable, Subscriber } from "rxjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* NdJson 配置选项
|
|
7
|
+
*/
|
|
8
|
+
export interface NdJsonOptions {
|
|
9
|
+
/** 成功状态码 */
|
|
10
|
+
code?: number;
|
|
11
|
+
/** 状态码字段名 */
|
|
12
|
+
codeKey?: string;
|
|
13
|
+
/** 不需要 token 的 URL 列表 */
|
|
14
|
+
filter_url?: string[];
|
|
15
|
+
/** token 过期回调 */
|
|
16
|
+
tokenExpiration?: () => void;
|
|
17
|
+
/** 自定义请求配置 */
|
|
18
|
+
requestOptions?: (config: RequestInit) => Record<string, unknown>;
|
|
19
|
+
/** 自定义响应处理 */
|
|
20
|
+
handleResponse?: <T>(response: T) => T;
|
|
21
|
+
/** 基础 API 地址,默认使用 BASE_API 常量 */
|
|
22
|
+
baseURL?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface RequestContext {
|
|
26
|
+
controller: AbortController;
|
|
27
|
+
isActive: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type HttpMethod = "GET" | "POST";
|
|
31
|
+
type RequestData = BodyInit | Record<string, unknown>;
|
|
32
|
+
|
|
33
|
+
const NDJSON_CONTENT_TYPE = "application/x-ndjson";
|
|
34
|
+
|
|
35
|
+
const isObjectLike = (value: unknown): value is Record<string, unknown> =>
|
|
36
|
+
typeof value === "object" && value !== null;
|
|
37
|
+
|
|
38
|
+
const isPlainObject = (value: unknown): value is Record<string, unknown> => {
|
|
39
|
+
if (!isObjectLike(value)) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const proto = Object.getPrototypeOf(value);
|
|
44
|
+
return proto === Object.prototype || proto === null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const isFunction = (value: unknown): value is (...args: unknown[]) => unknown =>
|
|
48
|
+
typeof value === "function";
|
|
49
|
+
|
|
50
|
+
export class NdJson {
|
|
51
|
+
private options: NdJsonOptions = {
|
|
52
|
+
code: 200,
|
|
53
|
+
codeKey: "status"
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
private activeRequests = new Set<RequestContext>();
|
|
57
|
+
|
|
58
|
+
constructor(options?: NdJsonOptions) {
|
|
59
|
+
if (options) {
|
|
60
|
+
this.options = { ...this.options, ...options };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 初始化/更新配置
|
|
66
|
+
*/
|
|
67
|
+
create(options: NdJsonOptions): void {
|
|
68
|
+
this.options = { ...this.options, ...options };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 获取完整 URL
|
|
73
|
+
*/
|
|
74
|
+
private getUrl(url: string): string {
|
|
75
|
+
const baseURL = this.options.baseURL ?? BASE_API;
|
|
76
|
+
return baseURL + url;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 处理 NDJSON 流的核心逻辑
|
|
81
|
+
*/
|
|
82
|
+
private processStream<T>(
|
|
83
|
+
reader: ReadableStreamDefaultReader<Uint8Array>,
|
|
84
|
+
observer: Subscriber<T>,
|
|
85
|
+
context: RequestContext
|
|
86
|
+
): void {
|
|
87
|
+
const decoder = new TextDecoder();
|
|
88
|
+
let buffer = "";
|
|
89
|
+
|
|
90
|
+
const read = async (): Promise<void> => {
|
|
91
|
+
try {
|
|
92
|
+
while (context.isActive) {
|
|
93
|
+
const { done, value } = await reader.read();
|
|
94
|
+
if (done) {
|
|
95
|
+
const finalText = decoder.decode();
|
|
96
|
+
if (finalText) {
|
|
97
|
+
buffer += finalText;
|
|
98
|
+
}
|
|
99
|
+
this.flushBuffer(buffer, observer);
|
|
100
|
+
if (!observer.closed) {
|
|
101
|
+
observer.complete();
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
buffer += decoder.decode(value, { stream: true });
|
|
107
|
+
buffer = this.parseLines(buffer, observer);
|
|
108
|
+
if (observer.closed) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!this.isAbortError(error) && !observer.closed) {
|
|
114
|
+
observer.error(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
void read();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 解析缓冲区中的完整行
|
|
124
|
+
*/
|
|
125
|
+
private parseLines<T>(buffer: string, observer: Subscriber<T>): string {
|
|
126
|
+
let start = 0;
|
|
127
|
+
let lineEnd = buffer.indexOf("\n");
|
|
128
|
+
|
|
129
|
+
while (lineEnd !== -1) {
|
|
130
|
+
const line = buffer.slice(start, lineEnd).trim();
|
|
131
|
+
if (line.length > 0 && !this.emitLine(line, observer)) {
|
|
132
|
+
return "";
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
start = lineEnd + 1;
|
|
136
|
+
lineEnd = buffer.indexOf("\n", start);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return buffer.slice(start);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 刷新剩余缓冲区
|
|
144
|
+
*/
|
|
145
|
+
private flushBuffer<T>(buffer: string, observer: Subscriber<T>): void {
|
|
146
|
+
const trimmed = buffer.trim();
|
|
147
|
+
if (trimmed.length > 0) {
|
|
148
|
+
this.emitLine(trimmed, observer);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private emitLine<T>(line: string, observer: Subscriber<T>): boolean {
|
|
153
|
+
const data = line.startsWith("data:") ? line.slice(5).trimStart() : line;
|
|
154
|
+
try {
|
|
155
|
+
observer.next(this.handleResponse(JSON.parse(data)));
|
|
156
|
+
return true;
|
|
157
|
+
} catch (error) {
|
|
158
|
+
observer.error(error);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 创建请求的 Observable
|
|
165
|
+
*/
|
|
166
|
+
private request<T>(
|
|
167
|
+
method: HttpMethod,
|
|
168
|
+
url: string,
|
|
169
|
+
data?: RequestData,
|
|
170
|
+
extra: RequestInit = {}
|
|
171
|
+
): Observable<T> {
|
|
172
|
+
const fullUrl = this.getUrl(url);
|
|
173
|
+
|
|
174
|
+
return new Observable<T>(observer => {
|
|
175
|
+
const controller = new AbortController();
|
|
176
|
+
const context: RequestContext = {
|
|
177
|
+
controller,
|
|
178
|
+
isActive: true
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
this.activeRequests.add(context);
|
|
182
|
+
|
|
183
|
+
const requestInit = this.mergeRequestInit(
|
|
184
|
+
{
|
|
185
|
+
method,
|
|
186
|
+
signal: controller.signal,
|
|
187
|
+
keepalive: true
|
|
188
|
+
},
|
|
189
|
+
this.handleRequest(fullUrl, method),
|
|
190
|
+
extra,
|
|
191
|
+
{
|
|
192
|
+
method,
|
|
193
|
+
signal: controller.signal
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// POST 请求添加 body
|
|
198
|
+
if (method === "POST" && data !== undefined) {
|
|
199
|
+
requestInit.body = isPlainObject(data) ? JSON.stringify(data) : (data as BodyInit);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fetch(fullUrl, requestInit)
|
|
203
|
+
.then(resp => {
|
|
204
|
+
if (resp.status !== this.options.code) {
|
|
205
|
+
if (!this.isAbortError(resp)) {
|
|
206
|
+
observer.error(resp);
|
|
207
|
+
}
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const reader = resp.body?.getReader();
|
|
212
|
+
|
|
213
|
+
if (!reader) {
|
|
214
|
+
observer.error(new Error("No readable stream available"));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
context.isActive = true;
|
|
219
|
+
this.processStream(reader, observer, context);
|
|
220
|
+
})
|
|
221
|
+
.catch(e => {
|
|
222
|
+
if (!this.isAbortError(e)) {
|
|
223
|
+
observer.error(e);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// 返回清理函数
|
|
228
|
+
return () => {
|
|
229
|
+
context.isActive = false;
|
|
230
|
+
controller.abort();
|
|
231
|
+
this.activeRequests.delete(context);
|
|
232
|
+
};
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
get<T = unknown>(url: string, _data = "{}", extra: RequestInit = {}): Observable<T> {
|
|
237
|
+
return this.request<T>("GET", url, undefined, extra);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
post<T = unknown>(url: string, data: RequestData = {}, extra: RequestInit = {}): Observable<T> {
|
|
241
|
+
return this.request<T>("POST", url, data, extra);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private handleRequest(url: string, method: HttpMethod): RequestInit {
|
|
245
|
+
const headers: Record<string, string> = {};
|
|
246
|
+
|
|
247
|
+
// 只有 POST 请求才设置 Content-Type
|
|
248
|
+
if (method === "POST") {
|
|
249
|
+
headers["Content-Type"] = NDJSON_CONTENT_TYPE;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const config: RequestInit = { headers };
|
|
253
|
+
const token = getToken();
|
|
254
|
+
|
|
255
|
+
if (!token && !this.options.filter_url?.some(_url => url.includes(_url))) {
|
|
256
|
+
this.options.tokenExpiration?.();
|
|
257
|
+
return config;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (token) {
|
|
261
|
+
headers[TOKEN_KEY] = token;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (isFunction(this.options.requestOptions)) {
|
|
265
|
+
const extraOptions = this.options.requestOptions(config);
|
|
266
|
+
if (isObjectLike(extraOptions)) {
|
|
267
|
+
return this.mergeRequestInit(config, extraOptions as RequestInit);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return config;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
handleResponse<T>(response: T): T {
|
|
275
|
+
if (isFunction(this.options.handleResponse)) {
|
|
276
|
+
return this.options.handleResponse(response);
|
|
277
|
+
}
|
|
278
|
+
return response;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private mergeRequestInit(...configs: Array<RequestInit | undefined>): RequestInit {
|
|
282
|
+
const merged: RequestInit = {};
|
|
283
|
+
const mergedHeaders = new Headers();
|
|
284
|
+
let hasHeaders = false;
|
|
285
|
+
|
|
286
|
+
configs.forEach((config) => {
|
|
287
|
+
if (!config) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const { headers, ...rest } = config;
|
|
292
|
+
Object.assign(merged, rest);
|
|
293
|
+
hasHeaders = this.mergeHeaders(mergedHeaders, headers) || hasHeaders;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (hasHeaders) {
|
|
297
|
+
merged.headers = mergedHeaders;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return merged;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private mergeHeaders(target: Headers, source?: HeadersInit): boolean {
|
|
304
|
+
if (!source) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let merged = false;
|
|
309
|
+
new Headers(source).forEach((value, key) => {
|
|
310
|
+
target.set(key, value);
|
|
311
|
+
merged = true;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return merged;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private isAbortError(error: unknown): boolean {
|
|
318
|
+
return error instanceof Error && error.name === "AbortError";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 取消所有活跃的请求
|
|
323
|
+
*/
|
|
324
|
+
cancelAll(): void {
|
|
325
|
+
this.activeRequests.forEach(context => {
|
|
326
|
+
context.isActive = false;
|
|
327
|
+
context.controller.abort();
|
|
328
|
+
});
|
|
329
|
+
this.activeRequests.clear();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 默认实例
|
|
334
|
+
const defaultNdJson = new NdJson();
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* 创建新的 NdJson 实例
|
|
338
|
+
*/
|
|
339
|
+
export const createNdJson = (options?: NdJsonOptions): NdJson => {
|
|
340
|
+
return new NdJson(options);
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 初始化默认实例
|
|
345
|
+
*/
|
|
346
|
+
export const createNdJsonService = (options: NdJsonOptions): void => {
|
|
347
|
+
defaultNdJson.create(options);
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// 导出默认实例 (保持向后兼容)
|
|
351
|
+
export const ndJson = defaultNdJson;
|