lissa 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +715 -0
- package/lib/core/defaults.js +22 -0
- package/lib/core/errors.js +13 -0
- package/lib/core/lissa.js +182 -0
- package/lib/core/request.js +488 -0
- package/lib/exports.js +3 -0
- package/lib/index.d.ts +510 -0
- package/lib/index.js +28 -0
- package/lib/plugins/dedupe.js +45 -0
- package/lib/plugins/index.js +2 -0
- package/lib/plugins/retry.js +65 -0
- package/lib/utils/OpenPromise.js +88 -0
- package/lib/utils/helper.js +195 -0
- package/package.json +72 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const basic = (headers = {}, params = {}) => ({
|
|
2
|
+
headers: new Headers(headers),
|
|
3
|
+
params,
|
|
4
|
+
});
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
'adapter': 'fetch',
|
|
8
|
+
'method': 'get',
|
|
9
|
+
'headers': new Headers({
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
}),
|
|
12
|
+
'params': {},
|
|
13
|
+
'paramsSerializer': 'simple',
|
|
14
|
+
'urlBuilder': 'simple',
|
|
15
|
+
'responseType': 'json',
|
|
16
|
+
|
|
17
|
+
'get': basic(),
|
|
18
|
+
'post': basic(),
|
|
19
|
+
'put': basic(),
|
|
20
|
+
'patch': basic(),
|
|
21
|
+
'delete': basic(),
|
|
22
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class ConnectionError extends Error {
|
|
2
|
+
constructor(...args) {
|
|
3
|
+
super('ConnectionError', ...args);
|
|
4
|
+
this.name = 'ConnectionError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ResponseError extends Error {
|
|
9
|
+
constructor(...args) {
|
|
10
|
+
super('ResponseError', ...args);
|
|
11
|
+
this.name = 'ResponseError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import LissaRequest from './request.js';
|
|
2
|
+
import { deepMerge, normalizeOptions } from '../utils/helper.js';
|
|
3
|
+
|
|
4
|
+
const type = Symbol('type');
|
|
5
|
+
|
|
6
|
+
export default class Lissa {
|
|
7
|
+
#options;
|
|
8
|
+
#hooks;
|
|
9
|
+
|
|
10
|
+
static create(...args) {
|
|
11
|
+
if (args[0] && typeof args[0] !== 'string') args.unshift(null);
|
|
12
|
+
const [baseURL = null, options = {}] = args;
|
|
13
|
+
|
|
14
|
+
if (baseURL) options.baseURL = baseURL;
|
|
15
|
+
|
|
16
|
+
return new Lissa(options);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
this.#options = normalizeOptions(options);
|
|
21
|
+
this.#hooks = new Set();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get options() {
|
|
25
|
+
return this.#options;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get beforeRequestHooks() {
|
|
29
|
+
return this.#hooks.values().filter(l => l[type] === 'beforeRequest');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get beforeFetchHooks() {
|
|
33
|
+
return this.#hooks.values().filter(l => l[type] === 'beforeFetch');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get responseHooks() {
|
|
37
|
+
return this.#hooks.values().filter(l => l[type] === 'onResponse');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get errorHooks() {
|
|
41
|
+
return this.#hooks.values().filter(l => l[type] === 'onError');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
use(plugin) {
|
|
45
|
+
plugin(this);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
beforeRequest(hook) {
|
|
50
|
+
hook[type] = 'beforeRequest';
|
|
51
|
+
this.#hooks.add(hook);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeFetch(hook) {
|
|
56
|
+
hook[type] = 'beforeFetch';
|
|
57
|
+
this.#hooks.add(hook);
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
onResponse(hook) {
|
|
62
|
+
hook[type] = 'onResponse';
|
|
63
|
+
this.#hooks.add(hook);
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
onError(hook) {
|
|
68
|
+
hook[type] = 'onError';
|
|
69
|
+
this.#hooks.add(hook);
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
extend(options) {
|
|
74
|
+
options = normalizeOptions(options);
|
|
75
|
+
options = deepMerge(this.#options, options);
|
|
76
|
+
|
|
77
|
+
const newInstance = new Lissa(options);
|
|
78
|
+
newInstance.#hooks = new Set(this.#hooks);
|
|
79
|
+
return newInstance;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
authenticate(username, password) {
|
|
83
|
+
return this.extend({
|
|
84
|
+
authenticate: { username, password },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get(url, options = {}) {
|
|
89
|
+
return LissaRequest.init(this, {
|
|
90
|
+
...options,
|
|
91
|
+
method: 'get',
|
|
92
|
+
url,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
post(url, body, options = {}) {
|
|
97
|
+
return LissaRequest.init(this, {
|
|
98
|
+
...options,
|
|
99
|
+
method: 'post',
|
|
100
|
+
url,
|
|
101
|
+
body,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
put(url, body, options = {}) {
|
|
106
|
+
return LissaRequest.init(this, {
|
|
107
|
+
...options,
|
|
108
|
+
method: 'put',
|
|
109
|
+
url,
|
|
110
|
+
body,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
patch(url, body, options = {}) {
|
|
115
|
+
return LissaRequest.init(this, {
|
|
116
|
+
...options,
|
|
117
|
+
method: 'patch',
|
|
118
|
+
url,
|
|
119
|
+
body,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
delete(url, options = {}) {
|
|
124
|
+
return LissaRequest.init(this, {
|
|
125
|
+
...options,
|
|
126
|
+
method: 'delete',
|
|
127
|
+
url,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
request(options = {}) {
|
|
132
|
+
return LissaRequest.init(this, { ...options });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
upload(file, ...args) {
|
|
136
|
+
const url
|
|
137
|
+
= args.find(arg => typeof arg === 'string') || '';
|
|
138
|
+
|
|
139
|
+
const onProgress
|
|
140
|
+
= args.find(arg => typeof arg === 'function') || null;
|
|
141
|
+
|
|
142
|
+
const options
|
|
143
|
+
= args.find(arg => typeof arg === 'object') || {};
|
|
144
|
+
|
|
145
|
+
if (url) options.url = url;
|
|
146
|
+
if (onProgress) options.onUploadProgress = onProgress;
|
|
147
|
+
|
|
148
|
+
// Upload with progress tracking to an http/1.1 server is unfortunately not
|
|
149
|
+
// support yet with fetch in a browser so we set adapter to xhr for now.
|
|
150
|
+
// Upload with process tracking to an http/2 server is experimental. Feel
|
|
151
|
+
// free to test an http/2 upload by setting adapter explicitly to "fetch"
|
|
152
|
+
if (onProgress && !options.adapter && typeof window !== 'undefined') {
|
|
153
|
+
options.adapter = 'xhr';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return LissaRequest.init(this, {
|
|
157
|
+
method: 'post',
|
|
158
|
+
...options,
|
|
159
|
+
body: file,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
download(...args) {
|
|
164
|
+
const url
|
|
165
|
+
= args.find(arg => typeof arg === 'string') || '';
|
|
166
|
+
|
|
167
|
+
const onProgress
|
|
168
|
+
= args.find(arg => typeof arg === 'function') || null;
|
|
169
|
+
|
|
170
|
+
const options
|
|
171
|
+
= args.find(arg => typeof arg === 'object') || {};
|
|
172
|
+
|
|
173
|
+
if (url) options.url = url;
|
|
174
|
+
if (onProgress) options.onDownloadProgress = onProgress;
|
|
175
|
+
|
|
176
|
+
return LissaRequest.init(this, {
|
|
177
|
+
method: 'get',
|
|
178
|
+
responseType: 'file',
|
|
179
|
+
...options,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import defaults from './defaults.js';
|
|
2
|
+
import { ConnectionError, ResponseError } from './errors.js';
|
|
3
|
+
import OpenPromise from '../utils/OpenPromise.js';
|
|
4
|
+
import { resolveCause, deepMerge, stringifyParams, stringToBase64, throttle } from '../utils/helper.js';
|
|
5
|
+
|
|
6
|
+
export default class LissaRequest extends OpenPromise {
|
|
7
|
+
|
|
8
|
+
static init(lissa, options) {
|
|
9
|
+
const request = new LissaRequest();
|
|
10
|
+
request.#init(lissa, options);
|
|
11
|
+
|
|
12
|
+
const keepStack = new Error();
|
|
13
|
+
keepStack.name = 'Code';
|
|
14
|
+
|
|
15
|
+
setTimeout(async () => {
|
|
16
|
+
try {
|
|
17
|
+
const response = await request.#execute();
|
|
18
|
+
request.resolve(response);
|
|
19
|
+
}
|
|
20
|
+
catch (err) {
|
|
21
|
+
request.reject(resolveCause(err, keepStack));
|
|
22
|
+
}
|
|
23
|
+
}, 1);
|
|
24
|
+
|
|
25
|
+
return request;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#lissa;
|
|
29
|
+
#options;
|
|
30
|
+
#locked;
|
|
31
|
+
|
|
32
|
+
#init(lissa, options) {
|
|
33
|
+
this.#lissa = lissa;
|
|
34
|
+
|
|
35
|
+
if (options.headers) options.headers = new Headers(options.headers);
|
|
36
|
+
this.#options = options;
|
|
37
|
+
|
|
38
|
+
this.#locked = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get options() {
|
|
42
|
+
return this.#options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
baseURL(baseURL) {
|
|
46
|
+
return this.#addOptions({ baseURL });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
url(url) {
|
|
50
|
+
return this.#addOptions({ url });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
method(method) {
|
|
54
|
+
return this.#addOptions({ method });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
headers(headers) {
|
|
58
|
+
return this.#addOptions({ headers });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
authenticate(username, password) {
|
|
62
|
+
return this.#addOptions({
|
|
63
|
+
authenticate: { username, password },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
params(params) {
|
|
68
|
+
return this.#addOptions({ params });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
body(body) {
|
|
72
|
+
return this.#addOptions({ body });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
timeout(timeout) {
|
|
76
|
+
return this.#addOptions({ timeout });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
signal(signal) {
|
|
80
|
+
return this.#addOptions({ signal });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
responseType(responseType) {
|
|
84
|
+
return this.#addOptions({ responseType });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onUploadProgress(onProgress) {
|
|
88
|
+
return this.#addOptions({ onUploadProgress: onProgress });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onDownloadProgress(onProgress) {
|
|
92
|
+
return this.#addOptions({ onDownloadProgress: onProgress });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#addOptions(options) {
|
|
96
|
+
if (this.#locked) {
|
|
97
|
+
console.warn(new Error('Request options cannot be changed anymore after execution start!'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (options.headers) options.headers = new Headers(options.headers);
|
|
102
|
+
this.#options = deepMerge(this.#options, options);
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async #execute() {
|
|
107
|
+
this.#locked = true;
|
|
108
|
+
const lissa = this.#lissa;
|
|
109
|
+
|
|
110
|
+
// Build options
|
|
111
|
+
|
|
112
|
+
const method = (this.#options.method || lissa.options.method || defaults.method).toLowerCase();
|
|
113
|
+
|
|
114
|
+
let {
|
|
115
|
+
get: _get, // omit
|
|
116
|
+
post: _post, // omit
|
|
117
|
+
put: _put, // omit
|
|
118
|
+
patch: _patch, // omit
|
|
119
|
+
delete: _del, // omit
|
|
120
|
+
...options
|
|
121
|
+
} = deepMerge(
|
|
122
|
+
defaults,
|
|
123
|
+
defaults[method],
|
|
124
|
+
lissa.options,
|
|
125
|
+
lissa.options[method],
|
|
126
|
+
this.#options,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
for (const hook of lissa.beforeRequestHooks) {
|
|
130
|
+
options = await hook.call(this, options) || options;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const $options = {
|
|
134
|
+
...options,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// Build fetch request
|
|
138
|
+
|
|
139
|
+
if (options.authenticate) {
|
|
140
|
+
const { username, password } = options.authenticate;
|
|
141
|
+
delete options.authenticate;
|
|
142
|
+
|
|
143
|
+
const auth = stringToBase64(`${username}:${password}`);
|
|
144
|
+
options.headers.set('Authorization', `Basic ${auth}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (options.params) {
|
|
148
|
+
const params = stringifyParams(options.params, options.paramsSerializer);
|
|
149
|
+
delete options.paramsSerializer;
|
|
150
|
+
|
|
151
|
+
options.params = params ? `?${params}` : '';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (options.body != null) {
|
|
155
|
+
if (options.body instanceof Blob && options.body.type && !this.#options.headers?.has('Content-Type')) {
|
|
156
|
+
options.headers.delete('Content-Type');
|
|
157
|
+
}
|
|
158
|
+
else if (options.body instanceof FormData) {
|
|
159
|
+
options.headers.delete('Content-Type');
|
|
160
|
+
}
|
|
161
|
+
else if (options.body instanceof URLSearchParams) {
|
|
162
|
+
options.headers.set('Content-Type', 'application/x-www-form-urlencoded');
|
|
163
|
+
}
|
|
164
|
+
else if (options.body.constructor === Object) {
|
|
165
|
+
options.body = JSON.stringify(options.body);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
options.headers.delete('Content-Type');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (options.timeout) {
|
|
173
|
+
if (options.signal) {
|
|
174
|
+
options.signal = AbortSignal.any([
|
|
175
|
+
options.signal,
|
|
176
|
+
AbortSignal.timeout(options.timeout),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
options.signal = AbortSignal.timeout(options.timeout);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
delete options.timeout;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let baseURL, url, params, urlBuilder, adapter;
|
|
187
|
+
({ baseURL, url, params, urlBuilder, adapter, ...options } = options);
|
|
188
|
+
|
|
189
|
+
const fetchUrl = ((
|
|
190
|
+
baseURL,
|
|
191
|
+
url,
|
|
192
|
+
urlBuilder,
|
|
193
|
+
) => {
|
|
194
|
+
if (typeof urlBuilder === 'function') return urlBuilder(url, baseURL);
|
|
195
|
+
if (urlBuilder === 'extended') return new URL(url, baseURL || undefined);
|
|
196
|
+
return `${baseURL || ''}${url}`;
|
|
197
|
+
})(
|
|
198
|
+
baseURL,
|
|
199
|
+
`${url || ''}${params || ''}`,
|
|
200
|
+
urlBuilder,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
options.method = method.toUpperCase();
|
|
204
|
+
|
|
205
|
+
// Actual fetch
|
|
206
|
+
let returnValue = adapter === 'xhr'
|
|
207
|
+
? await this.#executeXhr({ url: new URL(fetchUrl), ...options })
|
|
208
|
+
: await this.#executeFetch({ url: new URL(fetchUrl), ...options });
|
|
209
|
+
|
|
210
|
+
returnValue.options = $options;
|
|
211
|
+
|
|
212
|
+
// finish
|
|
213
|
+
|
|
214
|
+
const hooks = returnValue instanceof Error
|
|
215
|
+
? lissa.errorHooks
|
|
216
|
+
: lissa.responseHooks;
|
|
217
|
+
|
|
218
|
+
for (const hook of hooks) {
|
|
219
|
+
const newReturn = await hook.call(this, returnValue);
|
|
220
|
+
|
|
221
|
+
if (newReturn !== undefined) {
|
|
222
|
+
returnValue = newReturn;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (returnValue instanceof Error) throw returnValue;
|
|
228
|
+
return returnValue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async #executeFetch({ url, responseType, onUploadProgress, onDownloadProgress, ...options }) {
|
|
232
|
+
let request = { url, options };
|
|
233
|
+
|
|
234
|
+
for (const hook of this.#lissa.beforeFetchHooks) {
|
|
235
|
+
request = await hook.call(this, request) || request;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let fetchRequest = new Request(request.url, request.options);
|
|
239
|
+
|
|
240
|
+
if (onUploadProgress && fetchRequest.body && request.options.body) {
|
|
241
|
+
|
|
242
|
+
// To track the upload progress we can only do it with Streams and track
|
|
243
|
+
// the byte flow. This only actually track bytes read not bytes send
|
|
244
|
+
// and is overall not a good solution.
|
|
245
|
+
|
|
246
|
+
// In Browsers upload a ReadableStream to an http/1.1 server is
|
|
247
|
+
// also not support yet with fetch. Upload a ReadableStream to an
|
|
248
|
+
// http/2 server is experimental due to the required option duplex which
|
|
249
|
+
// is experimental.
|
|
250
|
+
|
|
251
|
+
// The funny part is fetchRequest.body is already a ReadableStream no
|
|
252
|
+
// matter the http version and fetchRequest.duplex is already 'half'.
|
|
253
|
+
// And setting it again to a ReadableStream requires duplex to be set
|
|
254
|
+
// to 'half' again and now uploading to http/1.1 is not working anymore :D
|
|
255
|
+
|
|
256
|
+
// Pls can we get actual progress events for fetch :)
|
|
257
|
+
|
|
258
|
+
const { body } = request.options;
|
|
259
|
+
const total = body.byteLength || body.size || body.length;
|
|
260
|
+
const trackProgressBridge = createTrackProgressBridge(total, onUploadProgress);
|
|
261
|
+
|
|
262
|
+
fetchRequest = new Request(fetchRequest, {
|
|
263
|
+
duplex: 'half',
|
|
264
|
+
body: fetchRequest.body.pipeThrough(trackProgressBridge),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let returnValue;
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const response = await fetch(fetchRequest).catch((error) => {
|
|
272
|
+
if (error.name === 'TypeError') {
|
|
273
|
+
throw new ConnectionError({ cause: error });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
throw error;
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// Explicit zero content length allows for skipping body parsing
|
|
280
|
+
const hasContent = response.headers.get('Content-Length') !== '0' && !!response.body;
|
|
281
|
+
|
|
282
|
+
let data = hasContent ? response.body : null;
|
|
283
|
+
|
|
284
|
+
if (data && onDownloadProgress) {
|
|
285
|
+
const total = +response.headers.get('Content-Length');
|
|
286
|
+
const trackProgressBridge = createTrackProgressBridge(total, onDownloadProgress);
|
|
287
|
+
data = data.pipeThrough(trackProgressBridge);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (data) {
|
|
291
|
+
switch (responseType) {
|
|
292
|
+
case 'text':
|
|
293
|
+
data = await streamToText(data);
|
|
294
|
+
break;
|
|
295
|
+
case 'json':
|
|
296
|
+
data = await streamToText(data);
|
|
297
|
+
if (!data) break;
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
data = JSON.parse(data);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
data = null;
|
|
304
|
+
console.error(error);
|
|
305
|
+
}
|
|
306
|
+
break;
|
|
307
|
+
case 'file':
|
|
308
|
+
data = await streamToFile(response.headers, data);
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
returnValue = response.ok ? {} : new ResponseError();
|
|
314
|
+
returnValue.response = response;
|
|
315
|
+
returnValue.headers = response.headers;
|
|
316
|
+
returnValue.status = response.status;
|
|
317
|
+
returnValue.data = hasContent && data || null;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
// error should be one of ConnectionError, AbortError or TimeoutError
|
|
321
|
+
returnValue = error;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
returnValue.request = request;
|
|
325
|
+
|
|
326
|
+
return returnValue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async #executeXhr({ url, ...options }) {
|
|
330
|
+
let request = { url, options };
|
|
331
|
+
|
|
332
|
+
for (const hook of this.#lissa.beforeFetchHooks) {
|
|
333
|
+
request = await hook.call(this, request) || request;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
({ url, options } = request);
|
|
337
|
+
|
|
338
|
+
if (options.signal?.aborted) return options.signal.reason || new DOMException('This operation was aborted', 'AbortError');
|
|
339
|
+
|
|
340
|
+
const xhr = new XMLHttpRequest();
|
|
341
|
+
xhr.open(options.method, url);
|
|
342
|
+
|
|
343
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
344
|
+
xhr.addEventListener('loadend', () => {
|
|
345
|
+
const { status } = xhr;
|
|
346
|
+
|
|
347
|
+
const headers = new Headers();
|
|
348
|
+
xhr.getAllResponseHeaders()?.trim().split(/[\r\n]+/).forEach((line) => {
|
|
349
|
+
const parts = line.split(': ');
|
|
350
|
+
const name = parts.shift();
|
|
351
|
+
const value = parts.join(': ');
|
|
352
|
+
if (name) {
|
|
353
|
+
headers.append(name, value);
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
resolve({
|
|
358
|
+
ok: status === 0 || (status >= 200 && status < 400),
|
|
359
|
+
status,
|
|
360
|
+
headers,
|
|
361
|
+
body: xhr.response,
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
xhr.addEventListener('abort', () => {
|
|
366
|
+
reject(options.signal?.reason || new DOMException('This operation was aborted', 'AbortError'));
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
xhr.addEventListener('error', () => {
|
|
370
|
+
reject(new ConnectionError());
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
options.headers.forEach((value, name) => xhr.setRequestHeader(name, value));
|
|
375
|
+
|
|
376
|
+
if (options.credentials === 'include') xhr.withCredentials = true;
|
|
377
|
+
|
|
378
|
+
if (options.signal) options.signal.addEventListener('abort', () => xhr.abort());
|
|
379
|
+
|
|
380
|
+
xhr.responseType = options.responseType === 'raw' || options.responseType === 'file' ? 'blob' : options.responseType;
|
|
381
|
+
|
|
382
|
+
if (options.onUploadProgress) {
|
|
383
|
+
xhr.upload.addEventListener('progress', (evt) => {
|
|
384
|
+
options.onUploadProgress(evt.loaded, evt.total);
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (options.onDownloadProgress) {
|
|
389
|
+
xhr.addEventListener('progress', (evt) => {
|
|
390
|
+
options.onDownloadProgress(evt.loaded, evt.total);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let returnValue;
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
xhr.send(options.body);
|
|
398
|
+
|
|
399
|
+
const response = await responsePromise;
|
|
400
|
+
|
|
401
|
+
returnValue = response.ok ? {} : new ResponseError();
|
|
402
|
+
returnValue.response = xhr;
|
|
403
|
+
returnValue.headers = response.headers;
|
|
404
|
+
returnValue.status = response.status;
|
|
405
|
+
returnValue.data = response.body;
|
|
406
|
+
|
|
407
|
+
if (options.responseType === 'file') {
|
|
408
|
+
const type = response.headers.get('Content-Type');
|
|
409
|
+
const filename = getFilenameFromContentDisposition(response.headers.get('Content-Disposition'));
|
|
410
|
+
|
|
411
|
+
returnValue.data = new File([response.body], filename, {
|
|
412
|
+
'type': type,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
// error should be one of ConnectionError, AbortError or TimeoutError
|
|
418
|
+
returnValue = error;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
returnValue.request = request;
|
|
422
|
+
|
|
423
|
+
return returnValue;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function createTrackProgressBridge(total, onProgress = () => {}) {
|
|
428
|
+
const throttledOnProgress = throttle(onProgress);
|
|
429
|
+
|
|
430
|
+
let bytes = 0;
|
|
431
|
+
return new TransformStream({
|
|
432
|
+
transform(chunk, controller) {
|
|
433
|
+
controller.enqueue(chunk);
|
|
434
|
+
bytes += chunk.byteLength;
|
|
435
|
+
throttledOnProgress(bytes, total);
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function streamToFile(headers, stream) {
|
|
441
|
+
const type = headers.get('Content-Type');
|
|
442
|
+
const filename = getFilenameFromContentDisposition(headers.get('Content-Disposition'));
|
|
443
|
+
|
|
444
|
+
const chunks = await streamToBuffer(stream);
|
|
445
|
+
|
|
446
|
+
return new File(chunks, filename, {
|
|
447
|
+
'type': type,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function streamToText(stream) {
|
|
452
|
+
stream = stream.pipeThrough(new TextDecoderStream());
|
|
453
|
+
const chunks = await streamToBuffer(stream);
|
|
454
|
+
|
|
455
|
+
return chunks.length ? chunks.join('') : null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function streamToBuffer(stream) {
|
|
459
|
+
const reader = stream.getReader();
|
|
460
|
+
const chunks = [];
|
|
461
|
+
while (true) {
|
|
462
|
+
const { done, value } = await reader.read();
|
|
463
|
+
if (done) break;
|
|
464
|
+
chunks.push(value);
|
|
465
|
+
}
|
|
466
|
+
return chunks;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function getFilenameFromContentDisposition(header) {
|
|
470
|
+
if (!header) return null;
|
|
471
|
+
|
|
472
|
+
const parts = header.split(';').slice(1);
|
|
473
|
+
|
|
474
|
+
for (const part of parts) {
|
|
475
|
+
const [key, value] = part.split('=').map(s => s.trim());
|
|
476
|
+
|
|
477
|
+
if (key.toLowerCase() === 'filename' && value) {
|
|
478
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
479
|
+
const inner = value.slice(1, -1);
|
|
480
|
+
return inner.replace(/\\(.)/g, '$1');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return value;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return null;
|
|
488
|
+
}
|
package/lib/exports.js
ADDED