ajax-hooker 1.2.0 → 1.2.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/README.md +32 -3
- package/README.zh-CN.md +32 -3
- package/dist/cjs/index.js +621 -1
- package/dist/esm/index.js +613 -1
- package/dist/iife/index.js +610 -1
- package/dist/types/interceptor.d.ts +8 -4
- package/dist/types/xhr.d.ts +1 -0
- package/package.json +22 -5
package/dist/iife/index.js
CHANGED
|
@@ -1 +1,610 @@
|
|
|
1
|
-
var ajaxInterceptor=function(e){"use strict";const t="xhr",r="fetch",s=Symbol("CycleScheduler");class n{req={};resp={};constructor({req:e={}}={}){this.req=e}async execute(e,t){let r=e;for(const e of t){const t=await e(r);t&&(r=t)}return r}}Object.getOwnPropertyDescriptor.bind(Object);const o=Object.prototype.toString.call.bind(Object.prototype.toString),a=(e="")=>new URL(e,window.location.origin).toString(),c=(e,t)=>{const r=Reflect.get(e,t);return"function"!=typeof r?r:function(...t){return Reflect.apply(r,e,t)}},h=({source:e,target:t,prototype:r})=>{Object.keys(e).forEach(r=>{t[r]=e[r]}),t.prototype=r};class i{nativeXhr=window.XMLHttpRequest;nativeXhrPrototype=window.XMLHttpRequest.prototype;hooks=[];xhrResponseEvents=["readystatechange","load","loadend"];xhrInstanceAttr=["response","responseText","responseXML","status","statusText"];xhrInstanceAttrHandler=this.xhrInstanceAttr.reduce((e,t)=>(e[t]=function(e){const r=e[s];return r.xhrAlreadyReturned?r.resp[t]:e[t]},e),{});xhrMethodsHandler={open:function(e,r){return function(...n){const o=r[s];o.xhrReset(),o.req={type:t,method:n[0]||"GET",url:a(n[1]),headers:new Headers,data:null,response:()=>{}},o.xhrOpenRestArgs=n.slice(2),e.nativeXhrPrototype.open.apply(r,[o.req.method,o.req.url,...o.xhrOpenRestArgs||[]])}},send:function(e,t){return async function(r){const n=t[s];n.req.data=r??null,n.req.responseType=t.responseType,n.req.withCredentials=t.withCredentials,n.req.timeout=t.timeout,n.req.headers=new Headers(n.xhrSetRequestHeadersAfterOpen);const o=n.req;let a={...n.req,headers:new Headers(n.req.headers)};try{a=await n.execute(a,e.hooks)}catch(e){console.warn("[AjaxInterceptor] Error in xhr request hooks:",e)}n.req=a;const c=o.method!==a.method||o.url!==a.url;c&&e.nativeXhrPrototype.open.apply(t,[n.req.method,n.req.url,...n.xhrOpenRestArgs||[]]);const h=["responseType","withCredentials","timeout"];for(const e of h)a[e]!==o[e]&&(t[e]=a[e]);!c&&e.headersEqual(o.headers,a.headers)||n.req.headers.forEach((e,r)=>{t.setRequestHeader(r,e)}),e.nativeXhrPrototype.send.apply(t,[n.req.data])}},setRequestHeader:function(e,t){return function(r,n){const o=t[s];e.nativeXhrPrototype.setRequestHeader.apply(t,[r,n]),o.xhrSetRequestHeadersAfterOpen.append(r,n)}},addEventListener:function(e,t,r){return function(s,n,...o){const a=e.xhrResponseEvents.includes(s);t.addEventListener(s,async function(...s){a&&4===t.readyState&&await e.responseProcessor(t),Reflect.apply(n,r,s)},...o)}}};constructor(){}inject(){window.XMLHttpRequest=this._generateProxyXMLHttpRequest()}uninject(){window.XMLHttpRequest=this.nativeXhr}parseHeaders(e){const t={};if(!e)return t;const r=(e,r)=>{const s=e.toLowerCase();t[s]=s in t?`${t[s]}, ${r}`:r},s=o(e);if("[object String]"===s){const t=e;for(const e of t.trim().split(/[\r\n]+/)){const t=e.indexOf(":");if(-1===t)continue;const s=e.slice(0,t).trim(),n=e.slice(t+1).trim();s&&r(s,n)}}else if("[object Headers]"===s){e.forEach((e,t)=>{r(t,e)})}else if("[object Object]"===s){const t=e;for(const[e,s]of Object.entries(t))null!=s&&r(e,String(s))}return t}async responseProcessor(e){const t=e[s];if(!t.xhrAlreadyReturned){t.xhrAlreadyReturned=!0,t.resp={status:e.status,statusText:e.statusText,response:e.response,headers:new Headers(this.parseHeaders(e.getAllResponseHeaders())),finalUrl:e.responseURL||""};try{await t.req.response(t.resp)}catch(e){console.warn("[AjaxInterceptor] Error in xhr response callback:",e)}}}headersEqual(e,t){if(e===t)return!0;const r=e=>{const t=[];return e.forEach((e,r)=>t.push(`${r}: ${e}`)),t.sort().toString()};return r(e)===r(t)}getAttrHandler(e,t,r){return this.xhrInstanceAttr.includes(t)?this.xhrInstanceAttrHandler[t](e):this.xhrMethodsHandler[t]?this.xhrMethodsHandler[t](this,e,r):null}_generateProxyXMLHttpRequest(){const e=this;function t(){const t=new e.nativeXhr;t[s]=new d;return new Proxy(t,{get:(t,r,s)=>e.getAttrHandler(t,r,s)??c(t,r),set(t,r,s,n){if("function"==typeof s&&r.startsWith("on")){const o=e.xhrResponseEvents.includes(r.replace(/^on/,"")),a=async function(...r){o&&4===t.readyState&&await e.responseProcessor(t),Reflect.apply(s,n,r)};return Reflect.set(t,r,a)}return Reflect.set(t,r,s)}})}return h({source:e.nativeXhr,target:t,prototype:this.nativeXhrPrototype}),t}}class d extends n{xhrAlreadyReturned=!1;xhrOpenRestArgs=[];xhrSetRequestHeadersAfterOpen=new Headers;xhrReset(){this.req={},this.resp={},this.xhrOpenRestArgs=[],this.xhrSetRequestHeadersAfterOpen=new Headers,this.xhrAlreadyReturned=!1}constructor({req:e={}}={}){super({req:e})}}class u extends n{constructor({req:e={}}={}){super({req:e})}}class l{nativeFetch=window.fetch;nativeFetchPrototype=this.nativeFetch.prototype;hooks=[];fetchInstanceAttr=["status","statusText","ok","headers","redirected"];fetchInstanceAttrHandler=this.fetchInstanceAttr.reduce((e,t)=>(e[t]=function(e,r){return r[s].resp[t]},e),{});fetchMethods=["json","formData","blob","arrayBuffer","text"];fetchMethodsHandler=this.fetchMethods.reduce((e,t)=>(e[t]=function(e,r){return async function(...e){return r[s].resp[t]}},e),{});constructor(){}inject(){window.fetch=this._generateProxyFetch()}uninject(){window.fetch=this.nativeFetch}getAttrHandler(e,t){return this.fetchInstanceAttr.includes(t)?this.fetchInstanceAttrHandler[t](this,e):this.fetchMethodsHandler[t]?this.fetchMethodsHandler[t](this,e):null}normalizeRequest(e){let t="",r=null,s=null,n=null;return"string"==typeof e||e instanceof URL?t=a(e):(t=a(e.url),r=e.method??null,s=e.headers??null,n=e.body??null),{url:t,method:r,headers:s,data:n}}resolveRequest(e,t,r){const s="string"==typeof e&&t.url!==e||e instanceof URL&&t.url!==e.href||e instanceof Request&&t.url!==e.url;if("string"==typeof e)return s?t.url:e;if(e instanceof URL)return s?new URL(t.url):e;if(e instanceof Request){if(s||"request"===r.method&&t.method!==e.method||"request"===r.headers&&t.headers!==e.headers||"request"===r.data&&t.data!==e.body){const n="request"===r.method&&t.method!==e.method,o="request"===r.headers&&t.headers!==e.headers,a="request"===r.data&&t.data!==e.body;return new Request(s?t.url:e,{...n&&{method:t.method},...o&&{headers:t.headers},...a&&{body:t.data}})}return e}return e}resolveOptions({options:e,newRequest:t,sourceMap:r}){const{method:s,headers:n,body:o,...a}=e||{},c={...a};if(("options"===r.method||"default"===r.method&&t.method!==(e?.method??"GET"))&&(c.method=t.method),"options"===r.headers)c.headers=t.headers;else if("default"===r.headers){let e=!0;if(t.headers instanceof Headers){let r=0;t.headers.forEach(()=>{r++}),e=0===r}else t.headers&&(e=0===Object.keys(t.headers).length);e||(c.headers=t.headers)}return("options"===r.data||"default"===r.data&&t.data!==(e?.body??null))&&(c.body=t.data),t.data instanceof ReadableStream&&(c.duplex="half"),c}resolveHeaders(e){return e instanceof Headers?e:new Headers(e)}_generateProxyFetch(){const e=this;async function t(t,n={}){const o=e.normalizeRequest(t),a=e.nativeFetch,h=new u,i={method:"default",headers:"default",data:"default"};t instanceof Request&&(i.method="request",i.headers="request",null!==t.body&&(i.data="request")),void 0!==n.method&&(i.method="options"),void 0!==n.headers&&(i.headers="options"),void 0!==n.body&&(i.data="options");const d={type:r,url:o.url,method:n.method??o.method??"GET",headers:e.resolveHeaders(n.headers??o.headers??new Headers),data:n.body??o.data??null,response:()=>{}};let l=d;try{l=await h.execute(d,e.hooks)}catch(e){console.warn("[AjaxInterceptor] Error in fetch request hooks:",e)}h.req=l;const p=await a(e.resolveRequest(t,l,i),e.resolveOptions({options:n,newRequest:l,sourceMap:i})),f=p.headers.get("content-type")||"",y=f.includes("text/event-stream")||f.includes("application/stream+json")||f.includes("application/x-ndjson")||f.includes("application/jsonl")||f.includes("application/json-seq");let x=p;if(y&&p.body){h.resp={status:p.status,statusText:p.statusText,ok:p.ok,headers:p.headers,finalUrl:p.url,redirected:p.redirected};try{await h.req.response(h.resp)}catch(e){console.warn("[AjaxInterceptor] Error in fetch stream response callback:",e)}let e=0;const{readable:t,writable:r}=new TransformStream({async transform(t,r){try{const s=(new TextDecoder).decode(t,{stream:!0});let n=s;if(h.req.onStreamChunk){const r={text:s,raw:t,index:e++,timestamp:Date.now()},o=await h.req.onStreamChunk(r);"string"==typeof o&&(n=o)}const o=new TextEncoder;r.enqueue(o.encode(n))}catch(e){r.enqueue(t)}}});p.body.pipeTo(r),x=new Response(t,{status:p.status,statusText:p.statusText,headers:p.headers})}else if(!y){const[e,t,r,s,n]=await Promise.allSettled([p.clone().json(),p.clone().text(),p.clone().arrayBuffer(),p.clone().blob(),p.clone().formData()]).then(e=>e.map(e=>"fulfilled"===e.status?e.value:null));h.resp={status:p.status,statusText:p.statusText,ok:p.ok,headers:p.headers,finalUrl:p.url,redirected:p.redirected,json:e,text:t,arrayBuffer:r,blob:s,formData:n};try{await h.req.response(h.resp)}catch(e){console.warn("[AjaxInterceptor] Error in fetch response callback:",e)}}x[s]=h;return new Proxy(x,{get(t,r){const s=e.getAttrHandler(t,r);return s||c(t,r)},set:(e,t,r)=>Reflect.set(e,t,r)})}return h({source:this.nativeFetch,target:t,prototype:this.nativeFetchPrototype}),t}}class p{xhrInterceptor;fetchInterceptor;static#e;static#t=Symbol("AjaxInterceptor");static getInstance(e={}){return p.#e||(p.#e=new p(p.#t,e)),p.#e}constructor(e,t={}){if(e!==p.#t)throw new Error("AjaxInterceptor is a singleton");this.xhrInterceptor=new i,this.fetchInterceptor=new l}toggleInject(e,s){switch(e){case t:this.xhrInterceptor[s]();break;case r:this.fetchInterceptor[s]();break;default:this.xhrInterceptor[s](),this.fetchInterceptor[s]()}}inject(e){if("undefined"==typeof window)throw new Error("AjaxInterceptor requires a browser environment");window.XMLHttpRequest||console.warn("XMLHttpRequest is not supported in this environment"),window.fetch||console.warn("Fetch API is not supported in this environment"),this.toggleInject(e,"inject")}uninject(e){this.toggleInject(e,"uninject")}hook(e,s){switch(s){case t:this.xhrInterceptor.hooks.push(e);break;case r:this.fetchInterceptor.hooks.push(e);break;default:this.xhrInterceptor.hooks.push(e),this.fetchInterceptor.hooks.push(e)}}unhook(e,s){const n=t=>{if(!e)return void(t.length=0);const r=t.indexOf(e);-1!==r&&t.splice(r,1)};switch(s){case t:n(this.xhrInterceptor.hooks);break;case r:n(this.fetchInterceptor.hooks);break;default:n(this.xhrInterceptor.hooks),n(this.fetchInterceptor.hooks)}}}return e.AjaxInterceptor=p,e.default=p,Object.defineProperty(e,"__esModule",{value:!0}),e}({});
|
|
1
|
+
var AjaxHooker = function(exports) {
|
|
2
|
+
"use strict";
|
|
3
|
+
const AJAX_TYPE = {
|
|
4
|
+
XHR: "xhr",
|
|
5
|
+
FETCH: "fetch"
|
|
6
|
+
};
|
|
7
|
+
const CYCLE_SCHEDULER = Symbol("CycleScheduler");
|
|
8
|
+
class CycleScheduler {
|
|
9
|
+
req={};
|
|
10
|
+
resp={};
|
|
11
|
+
constructor({req: req = {}} = {}) {
|
|
12
|
+
this.req = req;
|
|
13
|
+
}
|
|
14
|
+
async execute(request, fnList) {
|
|
15
|
+
let result = request;
|
|
16
|
+
for (const fn of fnList) {
|
|
17
|
+
const newResult = await fn(result);
|
|
18
|
+
if (newResult) result = newResult;
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
Object.getOwnPropertyDescriptor.bind(Object);
|
|
24
|
+
const getType = Object.prototype.toString.call.bind(Object.prototype.toString);
|
|
25
|
+
const resolveUrl = (url = "") => new URL(url, window.location.origin).toString();
|
|
26
|
+
const getProxyValue = (target, prop) => {
|
|
27
|
+
const value = Reflect.get(target, prop);
|
|
28
|
+
if (typeof value !== "function") return value;
|
|
29
|
+
return function(...args) {
|
|
30
|
+
return Reflect.apply(value, target, args);
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const copyNativePropsAndPrototype = ({source: source, target: target, prototype: prototype}) => {
|
|
34
|
+
Object.keys(source).forEach(key => {
|
|
35
|
+
target[key] = source[key];
|
|
36
|
+
});
|
|
37
|
+
target.prototype = prototype;
|
|
38
|
+
};
|
|
39
|
+
class XhrInterceptor {
|
|
40
|
+
nativeXhr=window.XMLHttpRequest;
|
|
41
|
+
nativeXhrPrototype=window.XMLHttpRequest.prototype;
|
|
42
|
+
hooks=[];
|
|
43
|
+
xhrResponseEvents=[ "readystatechange", "load", "loadend" ];
|
|
44
|
+
xhrInstanceAttr=[ "response", "responseText", "responseXML", "status", "statusText" ];
|
|
45
|
+
xhrInstanceAttrHandler=this.xhrInstanceAttr.reduce((acc, attr) => {
|
|
46
|
+
acc[attr] = function(target) {
|
|
47
|
+
const hooker = target[CYCLE_SCHEDULER];
|
|
48
|
+
return hooker.xhrAlreadyReturned ? hooker.resp[attr] : target[attr];
|
|
49
|
+
};
|
|
50
|
+
return acc;
|
|
51
|
+
}, {});
|
|
52
|
+
xhrMethodsHandler={
|
|
53
|
+
open: function(self, target) {
|
|
54
|
+
return function(...args) {
|
|
55
|
+
const hooker = target[CYCLE_SCHEDULER];
|
|
56
|
+
hooker.xhrReset();
|
|
57
|
+
hooker.req = {
|
|
58
|
+
type: AJAX_TYPE.XHR,
|
|
59
|
+
method: args[0] || "GET",
|
|
60
|
+
url: resolveUrl(args[1]),
|
|
61
|
+
headers: new Headers,
|
|
62
|
+
data: null,
|
|
63
|
+
response: () => {}
|
|
64
|
+
};
|
|
65
|
+
hooker.xhrOpenRestArgs = args.slice(2);
|
|
66
|
+
self.nativeXhrPrototype.open.apply(target, [ hooker.req.method, hooker.req.url, ...hooker.xhrOpenRestArgs || [] ]);
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
send: function(self, target) {
|
|
70
|
+
return async function(body) {
|
|
71
|
+
const hooker = target[CYCLE_SCHEDULER];
|
|
72
|
+
hooker.req.data = body ?? null;
|
|
73
|
+
hooker.req.responseType = target.responseType;
|
|
74
|
+
hooker.req.withCredentials = target.withCredentials;
|
|
75
|
+
hooker.req.timeout = target.timeout;
|
|
76
|
+
hooker.req.headers = new Headers(hooker.xhrSetRequestHeadersAfterOpen);
|
|
77
|
+
const oldRequest = hooker.req;
|
|
78
|
+
let newRequest = {
|
|
79
|
+
...hooker.req,
|
|
80
|
+
headers: new Headers(hooker.req.headers)
|
|
81
|
+
};
|
|
82
|
+
try {
|
|
83
|
+
newRequest = await hooker.execute(newRequest, self.hooks);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.warn("[AjaxInterceptor] Error in xhr request hooks:", error);
|
|
86
|
+
}
|
|
87
|
+
hooker.req = newRequest;
|
|
88
|
+
const needReopen = oldRequest.method !== newRequest.method || oldRequest.url !== newRequest.url;
|
|
89
|
+
const headersChanged = !self.headersEqual(oldRequest.headers, newRequest.headers);
|
|
90
|
+
const shouldReopen = needReopen || headersChanged;
|
|
91
|
+
if (shouldReopen) self.nativeXhrPrototype.open.apply(target, [ hooker.req.method, hooker.req.url, ...hooker.xhrOpenRestArgs || [] ]);
|
|
92
|
+
const xhrProps = [ "responseType", "withCredentials", "timeout" ];
|
|
93
|
+
for (const prop of xhrProps) if (newRequest[prop] !== oldRequest[prop]) target[prop] = newRequest[prop];
|
|
94
|
+
if (shouldReopen) hooker.req.headers.forEach((val, key) => {
|
|
95
|
+
target.setRequestHeader(key, val);
|
|
96
|
+
});
|
|
97
|
+
self.nativeXhrPrototype.send.apply(target, [ hooker.req.data ]);
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
setRequestHeader: function(self, target) {
|
|
101
|
+
return function(name, value) {
|
|
102
|
+
const hooker = target[CYCLE_SCHEDULER];
|
|
103
|
+
self.nativeXhrPrototype.setRequestHeader.apply(target, [ name, value ]);
|
|
104
|
+
hooker.xhrSetRequestHeadersAfterOpen.append(name, value);
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
addEventListener: function(self, target, receiver) {
|
|
108
|
+
return function(type, listener, ...args) {
|
|
109
|
+
const isResponseEvent = self.xhrResponseEvents.includes(type);
|
|
110
|
+
const hooker = target[CYCLE_SCHEDULER];
|
|
111
|
+
const capture = self.getCaptureOption(args[0]);
|
|
112
|
+
const newListener = async function(...args) {
|
|
113
|
+
if (isResponseEvent && target.readyState === 4) await self.responseProcessor(target);
|
|
114
|
+
if (typeof listener === "function") {
|
|
115
|
+
Reflect.apply(listener, receiver, args);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const [event] = args;
|
|
119
|
+
listener.handleEvent?.(event);
|
|
120
|
+
};
|
|
121
|
+
hooker.saveWrappedEventListener(type, capture, listener, newListener);
|
|
122
|
+
target.addEventListener(type, newListener, ...args);
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
removeEventListener: function(self, target) {
|
|
126
|
+
return function(type, listener, options) {
|
|
127
|
+
if (!listener) return self.nativeXhrPrototype.removeEventListener.apply(target, [ type, listener, options ]);
|
|
128
|
+
const hooker = target[CYCLE_SCHEDULER];
|
|
129
|
+
const capture = self.getCaptureOption(options);
|
|
130
|
+
const wrappedListener = hooker.getWrappedEventListener(type, capture, listener) || listener;
|
|
131
|
+
self.nativeXhrPrototype.removeEventListener.apply(target, [ type, wrappedListener, options ]);
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
constructor() {}
|
|
136
|
+
inject() {
|
|
137
|
+
window.XMLHttpRequest = this._generateProxyXMLHttpRequest();
|
|
138
|
+
}
|
|
139
|
+
uninject() {
|
|
140
|
+
window.XMLHttpRequest = this.nativeXhr;
|
|
141
|
+
}
|
|
142
|
+
parseHeaders(obj) {
|
|
143
|
+
const headers = {};
|
|
144
|
+
if (!obj) return headers;
|
|
145
|
+
const mergeHeader = (key, value) => {
|
|
146
|
+
const lkey = key.toLowerCase();
|
|
147
|
+
headers[lkey] = lkey in headers ? `${headers[lkey]}, ${value}` : value;
|
|
148
|
+
};
|
|
149
|
+
const type = getType(obj);
|
|
150
|
+
if (type === "[object String]") {
|
|
151
|
+
const str = obj;
|
|
152
|
+
for (const line of str.trim().split(/[\r\n]+/)) {
|
|
153
|
+
const colonIndex = line.indexOf(":");
|
|
154
|
+
if (colonIndex === -1) continue;
|
|
155
|
+
const header = line.slice(0, colonIndex).trim();
|
|
156
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
157
|
+
if (!header) continue;
|
|
158
|
+
mergeHeader(header, value);
|
|
159
|
+
}
|
|
160
|
+
} else if (type === "[object Headers]") {
|
|
161
|
+
const headersObj = obj;
|
|
162
|
+
headersObj.forEach((val, key) => {
|
|
163
|
+
mergeHeader(key, val);
|
|
164
|
+
});
|
|
165
|
+
} else if (type === "[object Object]") {
|
|
166
|
+
const record = obj;
|
|
167
|
+
for (const [key, val] of Object.entries(record)) if (val != null) mergeHeader(key, String(val));
|
|
168
|
+
}
|
|
169
|
+
return headers;
|
|
170
|
+
}
|
|
171
|
+
async responseProcessor(target) {
|
|
172
|
+
const hooker = target[CYCLE_SCHEDULER];
|
|
173
|
+
if (hooker.xhrAlreadyReturned) return;
|
|
174
|
+
hooker.xhrAlreadyReturned = true;
|
|
175
|
+
let responseText;
|
|
176
|
+
if (target.responseType === "" || target.responseType === "text") try {
|
|
177
|
+
responseText = target.responseText;
|
|
178
|
+
} catch (_error) {}
|
|
179
|
+
let responseXML;
|
|
180
|
+
try {
|
|
181
|
+
responseXML = target.responseXML;
|
|
182
|
+
} catch (_error) {}
|
|
183
|
+
hooker.resp = {
|
|
184
|
+
status: target.status,
|
|
185
|
+
statusText: target.statusText,
|
|
186
|
+
response: target.response,
|
|
187
|
+
responseText: responseText,
|
|
188
|
+
responseXML: responseXML,
|
|
189
|
+
headers: new Headers(this.parseHeaders(target.getAllResponseHeaders())),
|
|
190
|
+
finalUrl: target.responseURL || ""
|
|
191
|
+
};
|
|
192
|
+
try {
|
|
193
|
+
await hooker.req.response(hooker.resp);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.warn("[AjaxInterceptor] Error in xhr response callback:", error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
headersEqual(a, b) {
|
|
199
|
+
if (a === b) return true;
|
|
200
|
+
const toSortedString = h => {
|
|
201
|
+
const arr = [];
|
|
202
|
+
h.forEach((v, k) => arr.push(`${k}: ${v}`));
|
|
203
|
+
return arr.sort().toString();
|
|
204
|
+
};
|
|
205
|
+
return toSortedString(a) === toSortedString(b);
|
|
206
|
+
}
|
|
207
|
+
getCaptureOption(options) {
|
|
208
|
+
if (typeof options === "boolean") return options;
|
|
209
|
+
return !!options?.capture;
|
|
210
|
+
}
|
|
211
|
+
getAttrHandler(target, attr, receiver) {
|
|
212
|
+
if (this.xhrInstanceAttr.includes(attr)) return this.xhrInstanceAttrHandler[attr](target);
|
|
213
|
+
if (this.xhrMethodsHandler[attr]) return this.xhrMethodsHandler[attr](this, target, receiver);
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
_generateProxyXMLHttpRequest() {
|
|
217
|
+
const self = this;
|
|
218
|
+
function proxyXhr() {
|
|
219
|
+
const xhr = new self.nativeXhr;
|
|
220
|
+
xhr[CYCLE_SCHEDULER] = new XhrCycleScheduler;
|
|
221
|
+
const proxyXhr = new Proxy(xhr, {
|
|
222
|
+
get(target, prop, receiver) {
|
|
223
|
+
return self.getAttrHandler(target, prop, receiver) ?? getProxyValue(target, prop);
|
|
224
|
+
},
|
|
225
|
+
set(target, prop, value, receiver) {
|
|
226
|
+
if (typeof value === "function" && prop.startsWith("on")) {
|
|
227
|
+
const isResponseEvent = self.xhrResponseEvents.includes(prop.replace(/^on/, ""));
|
|
228
|
+
const fn = async function(...args) {
|
|
229
|
+
if (isResponseEvent && target.readyState === 4) await self.responseProcessor(target);
|
|
230
|
+
Reflect.apply(value, receiver, args);
|
|
231
|
+
};
|
|
232
|
+
return Reflect.set(target, prop, fn);
|
|
233
|
+
}
|
|
234
|
+
return Reflect.set(target, prop, value);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
return proxyXhr;
|
|
238
|
+
}
|
|
239
|
+
copyNativePropsAndPrototype({
|
|
240
|
+
source: self.nativeXhr,
|
|
241
|
+
target: proxyXhr,
|
|
242
|
+
prototype: this.nativeXhrPrototype
|
|
243
|
+
});
|
|
244
|
+
return proxyXhr;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
class XhrCycleScheduler extends CycleScheduler {
|
|
248
|
+
xhrAlreadyReturned=false;
|
|
249
|
+
xhrOpenRestArgs=[];
|
|
250
|
+
xhrSetRequestHeadersAfterOpen=new Headers;
|
|
251
|
+
xhrWrappedEventListeners=new Map;
|
|
252
|
+
getListenerBucket(type) {
|
|
253
|
+
if (!this.xhrWrappedEventListeners.has(type)) this.xhrWrappedEventListeners.set(type, {
|
|
254
|
+
captureTrue: new WeakMap,
|
|
255
|
+
captureFalse: new WeakMap
|
|
256
|
+
});
|
|
257
|
+
return this.xhrWrappedEventListeners.get(type);
|
|
258
|
+
}
|
|
259
|
+
saveWrappedEventListener(type, capture, original, wrapped) {
|
|
260
|
+
const bucket = this.getListenerBucket(type);
|
|
261
|
+
(capture ? bucket.captureTrue : bucket.captureFalse).set(original, wrapped);
|
|
262
|
+
}
|
|
263
|
+
getWrappedEventListener(type, capture, original) {
|
|
264
|
+
const bucket = this.xhrWrappedEventListeners.get(type);
|
|
265
|
+
if (!bucket) return null;
|
|
266
|
+
return (capture ? bucket.captureTrue : bucket.captureFalse).get(original) ?? null;
|
|
267
|
+
}
|
|
268
|
+
xhrReset() {
|
|
269
|
+
this.req = {};
|
|
270
|
+
this.resp = {};
|
|
271
|
+
this.xhrOpenRestArgs = [];
|
|
272
|
+
this.xhrSetRequestHeadersAfterOpen = new Headers;
|
|
273
|
+
this.xhrAlreadyReturned = false;
|
|
274
|
+
this.xhrWrappedEventListeners.clear();
|
|
275
|
+
}
|
|
276
|
+
constructor({req: req = {}} = {}) {
|
|
277
|
+
super({
|
|
278
|
+
req: req
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
class FetchCycleScheduler extends CycleScheduler {
|
|
283
|
+
constructor({req: req = {}} = {}) {
|
|
284
|
+
super({
|
|
285
|
+
req: req
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
class FetchInterceptor {
|
|
290
|
+
nativeFetch=window.fetch;
|
|
291
|
+
nativeFetchPrototype=this.nativeFetch.prototype;
|
|
292
|
+
hooks=[];
|
|
293
|
+
fetchInstanceAttr=[ "status", "statusText", "ok", "headers", "redirected" ];
|
|
294
|
+
fetchInstanceAttrHandler=this.fetchInstanceAttr.reduce((acc, attr) => {
|
|
295
|
+
acc[attr] = function(self, target) {
|
|
296
|
+
const hooker = target[CYCLE_SCHEDULER];
|
|
297
|
+
return hooker.resp[attr];
|
|
298
|
+
};
|
|
299
|
+
return acc;
|
|
300
|
+
}, {});
|
|
301
|
+
fetchMethods=[ "json", "formData", "blob", "arrayBuffer", "text" ];
|
|
302
|
+
fetchMethodsHandler=this.fetchMethods.reduce((acc, methodName) => {
|
|
303
|
+
acc[methodName] = function(self, target) {
|
|
304
|
+
return async function(...args) {
|
|
305
|
+
const hooker = target[CYCLE_SCHEDULER];
|
|
306
|
+
return hooker.resp[methodName];
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
return acc;
|
|
310
|
+
}, {});
|
|
311
|
+
constructor() {}
|
|
312
|
+
inject() {
|
|
313
|
+
window.fetch = this._generateProxyFetch();
|
|
314
|
+
}
|
|
315
|
+
uninject() {
|
|
316
|
+
window.fetch = this.nativeFetch;
|
|
317
|
+
}
|
|
318
|
+
getAttrHandler(target, attr) {
|
|
319
|
+
if (this.fetchInstanceAttr.includes(attr)) return this.fetchInstanceAttrHandler[attr](this, target);
|
|
320
|
+
if (this.fetchMethodsHandler[attr]) return this.fetchMethodsHandler[attr](this, target);
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
normalizeRequest(req) {
|
|
324
|
+
let url = "";
|
|
325
|
+
let method = null;
|
|
326
|
+
let headers = null;
|
|
327
|
+
let data = null;
|
|
328
|
+
if (typeof req === "string") url = resolveUrl(req); else if (req instanceof URL) url = resolveUrl(req); else {
|
|
329
|
+
url = resolveUrl(req.url);
|
|
330
|
+
method = req.method ?? null;
|
|
331
|
+
headers = req.headers ?? null;
|
|
332
|
+
data = req.body ?? null;
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
url: url,
|
|
336
|
+
method: method,
|
|
337
|
+
headers: headers,
|
|
338
|
+
data: data
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
resolveRequest(req, newRequest, sourceMap) {
|
|
342
|
+
const urlChanged = typeof req === "string" && newRequest.url !== req || req instanceof URL && newRequest.url !== req.href || req instanceof Request && newRequest.url !== req.url;
|
|
343
|
+
if (typeof req === "string") return urlChanged ? newRequest.url : req;
|
|
344
|
+
if (req instanceof URL) return urlChanged ? new URL(newRequest.url) : req;
|
|
345
|
+
if (req instanceof Request) {
|
|
346
|
+
const needsNewRequest = urlChanged || sourceMap.method === "request" && newRequest.method !== req.method || sourceMap.headers === "request" && newRequest.headers !== req.headers || sourceMap.data === "request" && newRequest.data !== req.body;
|
|
347
|
+
if (needsNewRequest) {
|
|
348
|
+
const methodChanged = sourceMap.method === "request" && newRequest.method !== req.method;
|
|
349
|
+
const headersChanged = sourceMap.headers === "request" && newRequest.headers !== req.headers;
|
|
350
|
+
const bodyChanged = sourceMap.data === "request" && newRequest.data !== req.body;
|
|
351
|
+
return new Request(urlChanged ? newRequest.url : req, {
|
|
352
|
+
...methodChanged && {
|
|
353
|
+
method: newRequest.method
|
|
354
|
+
},
|
|
355
|
+
...headersChanged && {
|
|
356
|
+
headers: newRequest.headers
|
|
357
|
+
},
|
|
358
|
+
...bodyChanged && {
|
|
359
|
+
body: newRequest.data
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return req;
|
|
364
|
+
}
|
|
365
|
+
return req;
|
|
366
|
+
}
|
|
367
|
+
resolveOptions({options: options, newRequest: newRequest, sourceMap: sourceMap}) {
|
|
368
|
+
const {method: _, headers: __, body: ___, ...rest} = options || {};
|
|
369
|
+
const resolved = {
|
|
370
|
+
...rest
|
|
371
|
+
};
|
|
372
|
+
if (sourceMap.method === "options" || sourceMap.method === "default" && newRequest.method !== (options?.method ?? "GET")) resolved.method = newRequest.method;
|
|
373
|
+
if (sourceMap.headers === "options") resolved.headers = newRequest.headers; else if (sourceMap.headers === "default") {
|
|
374
|
+
let isHeadersEmpty = true;
|
|
375
|
+
if (newRequest.headers instanceof Headers) {
|
|
376
|
+
let count = 0;
|
|
377
|
+
newRequest.headers.forEach(() => {
|
|
378
|
+
count++;
|
|
379
|
+
});
|
|
380
|
+
isHeadersEmpty = count === 0;
|
|
381
|
+
} else if (newRequest.headers) isHeadersEmpty = Object.keys(newRequest.headers).length === 0;
|
|
382
|
+
if (!isHeadersEmpty) resolved.headers = newRequest.headers;
|
|
383
|
+
}
|
|
384
|
+
if (sourceMap.data === "options" || sourceMap.data === "default" && newRequest.data !== (options?.body ?? null)) resolved.body = newRequest.data;
|
|
385
|
+
if (newRequest.data instanceof ReadableStream) resolved.duplex = "half";
|
|
386
|
+
return resolved;
|
|
387
|
+
}
|
|
388
|
+
resolveHeaders(headers) {
|
|
389
|
+
if (headers instanceof Headers) return headers;
|
|
390
|
+
return new Headers(headers);
|
|
391
|
+
}
|
|
392
|
+
_generateProxyFetch() {
|
|
393
|
+
const self = this;
|
|
394
|
+
async function proxyFetch(req, options = {}) {
|
|
395
|
+
const request = self.normalizeRequest(req);
|
|
396
|
+
const winFetch = self.nativeFetch;
|
|
397
|
+
const hooker = new FetchCycleScheduler;
|
|
398
|
+
const sourceMap = {
|
|
399
|
+
method: "default",
|
|
400
|
+
headers: "default",
|
|
401
|
+
data: "default"
|
|
402
|
+
};
|
|
403
|
+
if (req instanceof Request) {
|
|
404
|
+
sourceMap.method = "request";
|
|
405
|
+
sourceMap.headers = "request";
|
|
406
|
+
if (req.body !== null) sourceMap.data = "request";
|
|
407
|
+
}
|
|
408
|
+
if (options.method !== void 0) sourceMap.method = "options";
|
|
409
|
+
if (options.headers !== void 0) sourceMap.headers = "options";
|
|
410
|
+
if (options.body !== void 0) sourceMap.data = "options";
|
|
411
|
+
const originalRequest = {
|
|
412
|
+
type: AJAX_TYPE.FETCH,
|
|
413
|
+
url: request.url,
|
|
414
|
+
method: options.method ?? request.method ?? "GET",
|
|
415
|
+
headers: self.resolveHeaders(options.headers ?? request.headers ?? new Headers),
|
|
416
|
+
data: options.body ?? request.data ?? null,
|
|
417
|
+
response: () => {}
|
|
418
|
+
};
|
|
419
|
+
let newRequest = originalRequest;
|
|
420
|
+
try {
|
|
421
|
+
newRequest = await hooker.execute(originalRequest, self.hooks);
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.warn("[AjaxInterceptor] Error in fetch request hooks:", error);
|
|
424
|
+
}
|
|
425
|
+
hooker.req = newRequest;
|
|
426
|
+
const fh = await winFetch(self.resolveRequest(req, newRequest, sourceMap), self.resolveOptions({
|
|
427
|
+
options: options,
|
|
428
|
+
newRequest: newRequest,
|
|
429
|
+
sourceMap: sourceMap
|
|
430
|
+
}));
|
|
431
|
+
const contentType = fh.headers.get("content-type") || "";
|
|
432
|
+
const isStreamResponse = contentType.includes("text/event-stream") || contentType.includes("application/stream+json") || contentType.includes("application/x-ndjson") || contentType.includes("application/jsonl") || contentType.includes("application/json-seq");
|
|
433
|
+
let interceptedResponse = fh;
|
|
434
|
+
if (isStreamResponse && fh.body) {
|
|
435
|
+
hooker.resp = {
|
|
436
|
+
status: fh.status,
|
|
437
|
+
statusText: fh.statusText,
|
|
438
|
+
ok: fh.ok,
|
|
439
|
+
headers: fh.headers,
|
|
440
|
+
finalUrl: fh.url,
|
|
441
|
+
redirected: fh.redirected
|
|
442
|
+
};
|
|
443
|
+
try {
|
|
444
|
+
await hooker.req.response(hooker.resp);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.warn("[AjaxInterceptor] Error in fetch stream response callback:", error);
|
|
447
|
+
}
|
|
448
|
+
let chunkIndex = 0;
|
|
449
|
+
const {readable: readable, writable: writable} = new TransformStream({
|
|
450
|
+
async transform(chunk, controller) {
|
|
451
|
+
try {
|
|
452
|
+
const decoder = new TextDecoder;
|
|
453
|
+
const text = decoder.decode(chunk, {
|
|
454
|
+
stream: true
|
|
455
|
+
});
|
|
456
|
+
let modifiedText = text;
|
|
457
|
+
if (hooker.req.onStreamChunk) {
|
|
458
|
+
const streamChunk = {
|
|
459
|
+
text: text,
|
|
460
|
+
raw: chunk,
|
|
461
|
+
index: chunkIndex++,
|
|
462
|
+
timestamp: Date.now()
|
|
463
|
+
};
|
|
464
|
+
const result = await hooker.req.onStreamChunk(streamChunk);
|
|
465
|
+
if (typeof result === "string") modifiedText = result;
|
|
466
|
+
}
|
|
467
|
+
const encoder = new TextEncoder;
|
|
468
|
+
controller.enqueue(encoder.encode(modifiedText));
|
|
469
|
+
} catch (error) {
|
|
470
|
+
controller.enqueue(chunk);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
fh.body.pipeTo(writable);
|
|
475
|
+
interceptedResponse = new Response(readable, {
|
|
476
|
+
status: fh.status,
|
|
477
|
+
statusText: fh.statusText,
|
|
478
|
+
headers: fh.headers
|
|
479
|
+
});
|
|
480
|
+
} else if (!isStreamResponse) {
|
|
481
|
+
const [json, text, arrayBuffer, blob, formData] = await Promise.allSettled([ fh.clone().json(), fh.clone().text(), fh.clone().arrayBuffer(), fh.clone().blob(), fh.clone().formData() ]).then(results => results.map(result => result.status === "fulfilled" ? result.value : null));
|
|
482
|
+
hooker.resp = {
|
|
483
|
+
status: fh.status,
|
|
484
|
+
statusText: fh.statusText,
|
|
485
|
+
ok: fh.ok,
|
|
486
|
+
headers: fh.headers,
|
|
487
|
+
finalUrl: fh.url,
|
|
488
|
+
redirected: fh.redirected,
|
|
489
|
+
json: json,
|
|
490
|
+
text: text,
|
|
491
|
+
arrayBuffer: arrayBuffer,
|
|
492
|
+
blob: blob,
|
|
493
|
+
formData: formData
|
|
494
|
+
};
|
|
495
|
+
try {
|
|
496
|
+
await hooker.req.response(hooker.resp);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
console.warn("[AjaxInterceptor] Error in fetch response callback:", error);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
interceptedResponse[CYCLE_SCHEDULER] = hooker;
|
|
502
|
+
const proxyFh = new Proxy(interceptedResponse, {
|
|
503
|
+
get(target, prop) {
|
|
504
|
+
const attrHandler = self.getAttrHandler(target, prop);
|
|
505
|
+
if (attrHandler) return attrHandler;
|
|
506
|
+
return getProxyValue(target, prop);
|
|
507
|
+
},
|
|
508
|
+
set(target, prop, value) {
|
|
509
|
+
return Reflect.set(target, prop, value);
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
return proxyFh;
|
|
513
|
+
}
|
|
514
|
+
copyNativePropsAndPrototype({
|
|
515
|
+
source: this.nativeFetch,
|
|
516
|
+
target: proxyFetch,
|
|
517
|
+
prototype: this.nativeFetchPrototype
|
|
518
|
+
});
|
|
519
|
+
return proxyFetch;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
class AjaxInterceptor {
|
|
523
|
+
xhrInterceptor;
|
|
524
|
+
fetchInterceptor;
|
|
525
|
+
static #instance;
|
|
526
|
+
static #token=Symbol("AjaxInterceptor");
|
|
527
|
+
static getInstance(options = {}) {
|
|
528
|
+
if (!AjaxInterceptor.#instance) AjaxInterceptor.#instance = new AjaxInterceptor(AjaxInterceptor.#token, options);
|
|
529
|
+
return AjaxInterceptor.#instance;
|
|
530
|
+
}
|
|
531
|
+
constructor(token, options = {}) {
|
|
532
|
+
if (token !== AjaxInterceptor.#token) throw new Error("AjaxInterceptor is a singleton");
|
|
533
|
+
this.xhrInterceptor = new XhrInterceptor;
|
|
534
|
+
this.fetchInterceptor = new FetchInterceptor;
|
|
535
|
+
}
|
|
536
|
+
toggleInject(type, action) {
|
|
537
|
+
switch (type) {
|
|
538
|
+
case AJAX_TYPE.XHR:
|
|
539
|
+
this.xhrInterceptor[action]();
|
|
540
|
+
break;
|
|
541
|
+
|
|
542
|
+
case AJAX_TYPE.FETCH:
|
|
543
|
+
this.fetchInterceptor[action]();
|
|
544
|
+
break;
|
|
545
|
+
|
|
546
|
+
default:
|
|
547
|
+
this.xhrInterceptor[action]();
|
|
548
|
+
this.fetchInterceptor[action]();
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
inject(type) {
|
|
553
|
+
if (typeof window === "undefined") throw new Error("AjaxInterceptor requires a browser environment");
|
|
554
|
+
if (!window.XMLHttpRequest) console.warn("XMLHttpRequest is not supported in this environment");
|
|
555
|
+
if (!window.fetch) console.warn("Fetch API is not supported in this environment");
|
|
556
|
+
this.toggleInject(type, "inject");
|
|
557
|
+
}
|
|
558
|
+
uninject(type) {
|
|
559
|
+
this.toggleInject(type, "uninject");
|
|
560
|
+
}
|
|
561
|
+
hook(fn, type) {
|
|
562
|
+
switch (type) {
|
|
563
|
+
case AJAX_TYPE.XHR:
|
|
564
|
+
this.xhrInterceptor.hooks.push(fn);
|
|
565
|
+
break;
|
|
566
|
+
|
|
567
|
+
case AJAX_TYPE.FETCH:
|
|
568
|
+
this.fetchInterceptor.hooks.push(fn);
|
|
569
|
+
break;
|
|
570
|
+
|
|
571
|
+
default:
|
|
572
|
+
this.xhrInterceptor.hooks.push(fn);
|
|
573
|
+
this.fetchInterceptor.hooks.push(fn);
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
unhook(fn, type) {
|
|
578
|
+
const removeFrom = hooks => {
|
|
579
|
+
if (!fn) {
|
|
580
|
+
hooks.length = 0;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const index = hooks.indexOf(fn);
|
|
584
|
+
if (index !== -1) hooks.splice(index, 1);
|
|
585
|
+
};
|
|
586
|
+
switch (type) {
|
|
587
|
+
case AJAX_TYPE.XHR:
|
|
588
|
+
removeFrom(this.xhrInterceptor.hooks);
|
|
589
|
+
break;
|
|
590
|
+
|
|
591
|
+
case AJAX_TYPE.FETCH:
|
|
592
|
+
removeFrom(this.fetchInterceptor.hooks);
|
|
593
|
+
break;
|
|
594
|
+
|
|
595
|
+
default:
|
|
596
|
+
removeFrom(this.xhrInterceptor.hooks);
|
|
597
|
+
removeFrom(this.fetchInterceptor.hooks);
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
exports.AjaxInterceptor = AjaxInterceptor;
|
|
603
|
+
exports.default = AjaxInterceptor;
|
|
604
|
+
Object.defineProperty(exports, "__esModule", {
|
|
605
|
+
value: true
|
|
606
|
+
});
|
|
607
|
+
return exports;
|
|
608
|
+
}({});
|
|
609
|
+
|
|
610
|
+
AjaxHooker = Object.assign(AjaxHooker.default || AjaxHooker, AjaxHooker);
|
|
@@ -4,13 +4,17 @@ import { FetchInterceptor } from './fetch';
|
|
|
4
4
|
declare class AjaxInterceptor {
|
|
5
5
|
#private;
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* 1.x compatibility field.
|
|
8
|
+
* Direct access to internal instances is still available in 1.x,
|
|
9
|
+
* and is planned to become private in 2.x.
|
|
10
|
+
* Prefer `hook` / `unhook` / `inject` / `uninject`.
|
|
9
11
|
*/
|
|
10
12
|
xhrInterceptor: XhrInterceptor;
|
|
11
13
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
+
* 1.x compatibility field.
|
|
15
|
+
* Direct access to internal instances is still available in 1.x,
|
|
16
|
+
* and is planned to become private in 2.x.
|
|
17
|
+
* Prefer `hook` / `unhook` / `inject` / `uninject`.
|
|
14
18
|
*/
|
|
15
19
|
fetchInterceptor: FetchInterceptor;
|
|
16
20
|
static getInstance(options?: AjaxInterceptorCreateInstanceOptions): AjaxInterceptor;
|