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