-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfetch.lib.ts
More file actions
185 lines (152 loc) · 5.87 KB
/
fetch.lib.ts
File metadata and controls
185 lines (152 loc) · 5.87 KB
1
2
3
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
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
/* eslint-disable no-use-before-define */
import { abortSignalAny } from '../abort-signal-any';
import { AborterType } from '../../../modules/aborter/aborter.types';
let executableAborter: AborterType | null = null;
let isAborterCtxProvisionEnabled = false;
const originalFetch = globalThis.fetch;
const OriginalXHR = globalThis.XMLHttpRequest;
/**
* Saves the currently active `Aborter` instance to a module‑level variable.
* When an aborter is provided, the global `fetch` is replaced with an internal
* wrapper that automatically injects the aborter's signal and headers.
* When `null` is passed, the original `fetch` is restored.
*
* This function is used internally by the fetch interception mechanism.
*
* @param aborter - The `Aborter` instance to use for subsequent `fetch` calls,
* or `null` to disable interception.
*
* @example
* const aborter = new Aborter();
* injectAborterContextIntoHttpRequest(aborter);
* // All subsequent `fetch` calls will use the aborter's signal and headers.
*/
export function injectAborterContextIntoHttpRequest(aborter: AborterType | null): void {
if (!isAborterCtxProvisionEnabled) {
return;
}
if (!aborter) {
if (globalThis.fetch !== originalFetch) {
globalThis.fetch = originalFetch;
}
if (globalThis.XMLHttpRequest !== OriginalXHR) {
globalThis.XMLHttpRequest = OriginalXHR;
}
executableAborter = null;
} else {
executableAborter = aborter;
if (globalThis.fetch !== internalFetch) {
globalThis.fetch = internalFetch;
}
if (globalThis.XMLHttpRequest !== ProvisionXMLHttpRequest) {
globalThis.XMLHttpRequest = ProvisionXMLHttpRequest;
}
}
}
/**
* Retrieves abortable request headers from an `Aborter` instance.
*
* If the aborter has a `requestOptions` property containing `headers`, those headers are returned.
* Otherwise, an empty object is returned.
*
* @param {AborterType} aborter - The Aborter instance.
* @returns {Record<string, string>} Headers object (may be empty).
*/
const getAbortableHeaders = (aborter: AborterType): Record<string, string> => {
if (!('requestOptions' in aborter)) {
throw new ReferenceError('The field with the request options for the method "try" was not found!');
}
const { headers } = ('requestOptions' in aborter ? (aborter['requestOptions'] ?? {}) : {}) as Record<'headers', {}>;
return headers;
};
/**
* Proxied `XMLHttpRequest` constructor that automatically injects abort‑related headers
* and aborts the request when the active `Aborter` signals an abort.
*
* @this {XMLHttpRequest}
* @returns {XMLHttpRequest} An `XMLHttpRequest` instance (original or proxied).
*
* @example
* // Setup (once)
* import { injectAborterContextIntoHttpRequest } from './fetch.lib';
* import { Aborter } from './aborter';
*
* const aborter = new Aborter();
* injectAborterContextIntoHttpRequest(aborter);
*
* // Any XHR created after this point will use the aborter
* const xhr = new XMLHttpRequest();
* xhr.open('GET', '/api/data');
* xhr.send();
*
* // Later, abort the request
* aborter.abort();
*/
const ProvisionXMLHttpRequest = function (this: XMLHttpRequest): globalThis.XMLHttpRequest {
if (!executableAborter) {
return new OriginalXHR();
}
const aborter = executableAborter;
// Clear the aborter so that each request uses its own instance
injectAborterContextIntoHttpRequest(null);
const instance = new OriginalXHR();
const originalOpen = instance.open;
instance.open = function (...args: Parameters<typeof originalOpen>) {
originalOpen.apply(instance, args);
const headers = getAbortableHeaders(aborter);
Object.entries(headers).forEach(([name, value]) => {
instance.setRequestHeader(name, value);
});
} as typeof originalOpen;
const unsubscribe = aborter.listeners.addEventListener(
'aborted',
() => {
instance.abort();
},
{ once: true }
);
instance.addEventListener('loadend', unsubscribe, { once: true });
return instance;
} as unknown as typeof XMLHttpRequest;
Object.assign(ProvisionXMLHttpRequest, OriginalXHR);
ProvisionXMLHttpRequest.prototype = OriginalXHR.prototype;
/**
* Internal fetch wrapper that automatically injects an active `Aborter`'s signal
* and request headers into every `fetch` call.
*
* If no `executableAborter` is set, the original `fetch` is called directly.
* Otherwise, the aborter is cleared immediately (to prevent reusing the same
* aborter for subsequent requests).
*
* @param url - The request URL.
* @param init - Optional `fetch` options.
* @returns A `Promise` resolving to the `Response` object.
*/
export function internalFetch(url: RequestInfo | URL, init?: RequestInit): Promise<Response> {
if (!executableAborter) {
return globalThis.fetch(url, init);
}
const aborter = executableAborter;
// Clear the aborter so that each request uses its own instance
injectAborterContextIntoHttpRequest(null);
const headers = getAbortableHeaders(aborter);
return globalThis.fetch(url, {
...init,
signal: abortSignalAny(init?.signal, aborter.signal),
headers: { ...init?.headers, ...headers }
});
}
/**
* Enables or disables automatic provisioning of the active `Aborter` context for `fetch | XMLHttpRequest` calls.
* If enabled, `Aborter.try` calls will override the global `fetch | XMLHttpRequest` only at the time of the call and
* if the user chooses to pass the context automatically.
*
* After the `fetch | XMLHttpRequest` call, the context is immediately restored to the original one.
* The `fetch | XMLHttpRequest` override occurs only in the scope of the `Aborter.try` method.
*
* If disabled, the original `fetch | XMLHttpRequest` is always used, and interception does not occur.
* @param enabled - `true` to enable context provisioning, `false` to disable.
*/
export const setAborterContextProvisionMode = (enabled: boolean): void => {
isAborterCtxProvisionEnabled = enabled;
};