@d1g1tal/transportr 1.2.2 → 1.3.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/dist/transportr.js +1417 -1077
- package/dist/transportr.min.js +2 -2
- package/dist/transportr.min.js.map +4 -4
- package/package.json +6 -6
- package/src/abort-signal.js +175 -0
- package/src/constants.js +50 -0
- package/src/http-media-type.js +2 -0
- package/src/parameter-map.js +221 -0
- package/src/transportr.js +258 -282
- package/src/signal-controller.js +0 -50
package/src/transportr.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { _objectIsEmpty, _objectMerge, _type } from '@d1g1tal/chrysalis';
|
|
2
1
|
import SetMultiMap from '@d1g1tal/collections/set-multi-map.js';
|
|
3
|
-
import { MediaType } from '@d1g1tal/media-type';
|
|
4
2
|
import Subscribr from '@d1g1tal/subscribr';
|
|
5
3
|
import HttpError from './http-error.js';
|
|
6
4
|
import HttpMediaType from './http-media-type.js';
|
|
@@ -8,25 +6,17 @@ import HttpRequestHeader from './http-request-headers.js';
|
|
|
8
6
|
import HttpRequestMethod from './http-request-methods.js';
|
|
9
7
|
import HttpResponseHeader from './http-response-headers.js';
|
|
10
8
|
import ResponseStatus from './response-status.js';
|
|
11
|
-
import
|
|
9
|
+
import ParameterMap from './parameter-map.js';
|
|
10
|
+
import AbortSignal from './abort-signal.js';
|
|
11
|
+
import { MediaType } from '@d1g1tal/media-type';
|
|
12
|
+
import { _objectMerge, _objectIsEmpty, _type } from '@d1g1tal/chrysalis';
|
|
13
|
+
import { mediaTypes, endsWithSlashRegEx, RequestEvents, SignalEvents, abortEvent, requestBodyMethods } from './constants.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* @template T extends ResponseBody
|
|
15
17
|
* @typedef {function(Response): Promise<T>} ResponseHandler<T>
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
|
-
/**
|
|
19
|
-
* @template T
|
|
20
|
-
* @typedef {function(T): Object<string, *>} TypeConverter<T>
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @typedef {Object} PropertyTypeConverter
|
|
25
|
-
* @property {string} property The name of the property on the {@link RequestOptions} object.
|
|
26
|
-
* @property {Type} type The type of the property on the {@link RequestOptions} object.
|
|
27
|
-
* @property {TypeConverter<FormData|URLSearchParams|Headers>} converter A function that converts the property on the {@link RequestOptions} object to the type T.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
20
|
/**
|
|
31
21
|
* @typedef {Object} ContextEventHandler
|
|
32
22
|
* @property {*} context The context object.
|
|
@@ -76,22 +66,6 @@ import SignalController from './signal-controller.js';
|
|
|
76
66
|
* @property {null} window Can only be null. Used to disassociate request from any Window.
|
|
77
67
|
*/
|
|
78
68
|
|
|
79
|
-
/** @type {RegExp} */
|
|
80
|
-
const endsWithSlashRegEx = /\/$/;
|
|
81
|
-
/** @type {string} */
|
|
82
|
-
const charset = 'utf-8';
|
|
83
|
-
|
|
84
|
-
const _mediaTypes = new Map([
|
|
85
|
-
[HttpMediaType.PNG, new MediaType(HttpMediaType.PNG)],
|
|
86
|
-
[HttpMediaType.TEXT, new MediaType(HttpMediaType.TEXT, { charset })],
|
|
87
|
-
[HttpMediaType.JSON, new MediaType(HttpMediaType.JSON, { charset })],
|
|
88
|
-
[HttpMediaType.HTML, new MediaType(HttpMediaType.HTML, { charset })],
|
|
89
|
-
[HttpMediaType.JAVA_SCRIPT, new MediaType(HttpMediaType.JAVA_SCRIPT, { charset })],
|
|
90
|
-
[HttpMediaType.CSS, new MediaType(HttpMediaType.CSS, { charset })],
|
|
91
|
-
[HttpMediaType.XML, new MediaType(HttpMediaType.XML, { charset })],
|
|
92
|
-
[HttpMediaType.BIN, new MediaType(HttpMediaType.BIN)]
|
|
93
|
-
]);
|
|
94
|
-
|
|
95
69
|
/** @type {ResponseHandler<string>} */
|
|
96
70
|
const _handleText = async (response) => await response.text();
|
|
97
71
|
|
|
@@ -141,9 +115,6 @@ const _handleHtml = async (response) => new DOMParser().parseFromString(await re
|
|
|
141
115
|
/** @type {ResponseHandler<DocumentFragment>} */
|
|
142
116
|
const _handleHtmlFragment = async (response) => document.createRange().createContextualFragment(await response.text());
|
|
143
117
|
|
|
144
|
-
/** @type {TypeConverter<FormData|URLSearchParams>} */
|
|
145
|
-
const _typeConverter = (data) => Object.fromEntries(Array.from(data.keys()).map((key, index, keys, value = data.getAll(key)) => [key, value.length > 1 ? value : value[0]]));
|
|
146
|
-
|
|
147
118
|
/**
|
|
148
119
|
* A wrapper around the fetch API that makes it easier to make HTTP requests.
|
|
149
120
|
*
|
|
@@ -159,32 +130,18 @@ export default class Transportr {
|
|
|
159
130
|
#subscribr;
|
|
160
131
|
/** @type {Subscribr} */
|
|
161
132
|
static #globalSubscribr = new Subscribr();
|
|
162
|
-
/** @type {
|
|
163
|
-
static #activeRequests =
|
|
164
|
-
/**
|
|
165
|
-
* @private
|
|
166
|
-
* @static
|
|
167
|
-
* @type {SetMultiMap<ResponseHandler<ResponseBody>, string>}
|
|
168
|
-
*/
|
|
133
|
+
/** @type {Set<AbortSignal>} */
|
|
134
|
+
static #activeRequests = new Set();
|
|
135
|
+
/** @type {SetMultiMap<ResponseHandler<ResponseBody>, string>} */
|
|
169
136
|
static #contentTypeHandlers = new SetMultiMap([
|
|
170
|
-
[_handleImage,
|
|
171
|
-
[_handleText,
|
|
172
|
-
[_handleJson,
|
|
173
|
-
[_handleHtml,
|
|
174
|
-
[_handleScript,
|
|
175
|
-
[_handleCss,
|
|
176
|
-
[_handleXml,
|
|
177
|
-
[_handleReadableStream,
|
|
178
|
-
]);
|
|
179
|
-
/**
|
|
180
|
-
* @private
|
|
181
|
-
* @static
|
|
182
|
-
* @type {Set<PropertyTypeConverter>}
|
|
183
|
-
*/
|
|
184
|
-
static #propertyTypeConverters = new Set([
|
|
185
|
-
[{ property: 'body', type: FormData, converter: _typeConverter }],
|
|
186
|
-
[{ property: 'searchParams', type: URLSearchParams, converter: _typeConverter }],
|
|
187
|
-
[{ property: 'headers', type: Headers, converter: Object.fromEntries }]
|
|
137
|
+
[_handleImage, mediaTypes.get(HttpMediaType.PNG).type],
|
|
138
|
+
[_handleText, mediaTypes.get(HttpMediaType.TEXT).type],
|
|
139
|
+
[_handleJson, mediaTypes.get(HttpMediaType.JSON).subtype],
|
|
140
|
+
[_handleHtml, mediaTypes.get(HttpMediaType.HTML).subtype],
|
|
141
|
+
[_handleScript, mediaTypes.get(HttpMediaType.JAVA_SCRIPT).subtype],
|
|
142
|
+
[_handleCss, mediaTypes.get(HttpMediaType.CSS).subtype],
|
|
143
|
+
[_handleXml, mediaTypes.get(HttpMediaType.XML).subtype],
|
|
144
|
+
[_handleReadableStream, mediaTypes.get(HttpMediaType.BIN).subtype]
|
|
188
145
|
]);
|
|
189
146
|
|
|
190
147
|
/**
|
|
@@ -193,74 +150,14 @@ export default class Transportr {
|
|
|
193
150
|
* @param {URL|string|RequestOptions} [url=location.origin] The URL for {@link fetch} requests.
|
|
194
151
|
* @param {RequestOptions} [options={}] The default {@link RequestOptions} for this instance.
|
|
195
152
|
*/
|
|
196
|
-
constructor(url = location.origin, options = {}) {
|
|
197
|
-
|
|
198
|
-
if (type == Object) {
|
|
199
|
-
options = url;
|
|
200
|
-
url = location.origin;
|
|
201
|
-
} else if (type != URL) {
|
|
202
|
-
url = url.startsWith('/') ? new URL(url, location.origin) : new URL(url);
|
|
203
|
-
}
|
|
153
|
+
constructor(url = globalThis.location.origin, options = {}) {
|
|
154
|
+
if (_type(url) == Object) { [ url, options ] = [ globalThis.location.origin, url ] }
|
|
204
155
|
|
|
205
|
-
this.#baseUrl = url;
|
|
206
|
-
|
|
207
|
-
this.#options = _objectMerge(Transportr.#defaultRequestOptions, Transportr.#convertRequestOptions(options));
|
|
156
|
+
this.#baseUrl = Transportr.#getBaseUrl(url);
|
|
157
|
+
this.#options = Transportr.#createOptions(options, Transportr.#defaultRequestOptions);
|
|
208
158
|
this.#subscribr = new Subscribr();
|
|
209
159
|
}
|
|
210
160
|
|
|
211
|
-
/**
|
|
212
|
-
* Returns a {@link SignalController} used for aborting requests.
|
|
213
|
-
*
|
|
214
|
-
* @static
|
|
215
|
-
* @param {AbortSignal} [signal] The optional {@link AbortSignal} to used for chaining.
|
|
216
|
-
* @returns {SignalController} A new {@link SignalController} instance.
|
|
217
|
-
*/
|
|
218
|
-
static signalController(signal) {
|
|
219
|
-
return new SignalController(signal);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Returns a {@link EventRegistration} used for subscribing to global events.
|
|
224
|
-
*
|
|
225
|
-
* @static
|
|
226
|
-
* @param {TransportrEvent} event The event to subscribe to.
|
|
227
|
-
* @param {function(Event, *): void} handler The event handler.
|
|
228
|
-
* @param {*} context The context to bind the handler to.
|
|
229
|
-
* @returns {EventRegistration} A new {@link EventRegistration} instance.
|
|
230
|
-
*/
|
|
231
|
-
static register(event, handler, context) {
|
|
232
|
-
return Transportr.#globalSubscribr.subscribe(event, handler, context);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Removes a {@link EventRegistration} from the global event handler.
|
|
237
|
-
*
|
|
238
|
-
* @static
|
|
239
|
-
* @param {EventRegistration} eventRegistration The {@link EventRegistration} to remove.
|
|
240
|
-
* @returns {boolean} True if the {@link EventRegistration} was removed, false otherwise.
|
|
241
|
-
*/
|
|
242
|
-
static unregister(eventRegistration) {
|
|
243
|
-
return Transportr.#globalSubscribr.unsubscribe(eventRegistration);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Aborts all active requests.
|
|
248
|
-
* This is useful for when the user navigates away from the current page.
|
|
249
|
-
* This will also clear the {@link Transportr#activeRequests} array.
|
|
250
|
-
* This is called automatically when the {@link Transportr#abort} method is called.
|
|
251
|
-
*
|
|
252
|
-
* @static
|
|
253
|
-
* @returns {void}
|
|
254
|
-
*/
|
|
255
|
-
static abortAll() {
|
|
256
|
-
for (const signalController of this.#activeRequests) {
|
|
257
|
-
signalController.abort();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Clear the array after aborting all requests
|
|
261
|
-
this.#activeRequests = [];
|
|
262
|
-
}
|
|
263
|
-
|
|
264
161
|
/**
|
|
265
162
|
* @static
|
|
266
163
|
* @constant {Object<string, HttpRequestMethod>}
|
|
@@ -349,15 +246,7 @@ export default class Transportr {
|
|
|
349
246
|
* @static
|
|
350
247
|
* @constant {Object<string, TransportrEvent>}
|
|
351
248
|
*/
|
|
352
|
-
static
|
|
353
|
-
CONFIGURED: 'configured',
|
|
354
|
-
SUCCESS: 'success',
|
|
355
|
-
ERROR: 'error',
|
|
356
|
-
ABORTED: 'aborted',
|
|
357
|
-
TIMEOUT: 'timeout',
|
|
358
|
-
COMPLETE: 'complete',
|
|
359
|
-
ALL_COMPLETE: 'all-complete'
|
|
360
|
-
});
|
|
249
|
+
static RequestEvents = RequestEvents;
|
|
361
250
|
|
|
362
251
|
/**
|
|
363
252
|
* @private
|
|
@@ -368,7 +257,7 @@ export default class Transportr {
|
|
|
368
257
|
body: null,
|
|
369
258
|
cache: Transportr.CachingPolicy.NO_STORE,
|
|
370
259
|
credentials: Transportr.CredentialsPolicy.SAME_ORIGIN,
|
|
371
|
-
headers: { [HttpRequestHeader.CONTENT_TYPE]:
|
|
260
|
+
headers: { [HttpRequestHeader.CONTENT_TYPE]: mediaTypes.get(HttpMediaType.JSON).toString(), [HttpRequestHeader.ACCEPT]: mediaTypes.get(HttpMediaType.JSON).toString() },
|
|
372
261
|
searchParams: {},
|
|
373
262
|
integrity: undefined,
|
|
374
263
|
keepalive: undefined,
|
|
@@ -389,10 +278,61 @@ export default class Transportr {
|
|
|
389
278
|
* @type {Map<TransportrEvent, ResponseStatus>}
|
|
390
279
|
*/
|
|
391
280
|
static #eventResponseStatuses = new Map([
|
|
392
|
-
[
|
|
393
|
-
[
|
|
281
|
+
[RequestEvents.ABORTED, new ResponseStatus(499, 'Aborted')],
|
|
282
|
+
[RequestEvents.TIMEOUT, new ResponseStatus(504, 'Gateway Timeout')]
|
|
394
283
|
]);
|
|
395
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Returns a {@link AbortSignal} used for aborting requests.
|
|
287
|
+
*
|
|
288
|
+
* @static
|
|
289
|
+
* @returns {AbortSignal} A new {@link AbortSignal} instance.
|
|
290
|
+
*/
|
|
291
|
+
static abortSignal() {
|
|
292
|
+
return new AbortSignal();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Returns a {@link EventRegistration} used for subscribing to global events.
|
|
297
|
+
*
|
|
298
|
+
* @static
|
|
299
|
+
* @param {TransportrEvent} event The event to subscribe to.
|
|
300
|
+
* @param {function(Event, *): void} handler The event handler.
|
|
301
|
+
* @param {*} context The context to bind the handler to.
|
|
302
|
+
* @returns {EventRegistration} A new {@link EventRegistration} instance.
|
|
303
|
+
*/
|
|
304
|
+
static register(event, handler, context) {
|
|
305
|
+
return Transportr.#globalSubscribr.subscribe(event, handler, context);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Removes a {@link EventRegistration} from the global event handler.
|
|
310
|
+
*
|
|
311
|
+
* @static
|
|
312
|
+
* @param {EventRegistration} eventRegistration The {@link EventRegistration} to remove.
|
|
313
|
+
* @returns {boolean} True if the {@link EventRegistration} was removed, false otherwise.
|
|
314
|
+
*/
|
|
315
|
+
static unregister(eventRegistration) {
|
|
316
|
+
return Transportr.#globalSubscribr.unsubscribe(eventRegistration);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Aborts all active requests.
|
|
321
|
+
* This is useful for when the user navigates away from the current page.
|
|
322
|
+
* This will also clear the {@link Transportr#activeRequests} set.
|
|
323
|
+
*
|
|
324
|
+
* @static
|
|
325
|
+
* @returns {void}
|
|
326
|
+
*/
|
|
327
|
+
static abortAll() {
|
|
328
|
+
for (const abortSignal of this.#activeRequests) {
|
|
329
|
+
abortSignal.abort(abortEvent);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Clear the array after aborting all requests
|
|
333
|
+
this.#activeRequests.clear();
|
|
334
|
+
}
|
|
335
|
+
|
|
396
336
|
/**
|
|
397
337
|
* It returns the base {@link URL} for the API.
|
|
398
338
|
*
|
|
@@ -500,15 +440,17 @@ export default class Transportr {
|
|
|
500
440
|
}
|
|
501
441
|
|
|
502
442
|
/**
|
|
503
|
-
* It
|
|
443
|
+
* It returns a promise that resolves to the allowed request methods for the given resource path.
|
|
504
444
|
*
|
|
505
445
|
* @async
|
|
506
446
|
* @param {string} [path] The path to the resource.
|
|
507
447
|
* @param {RequestOptions} [options] The options for the request.
|
|
508
|
-
* @returns {Promise<
|
|
448
|
+
* @returns {Promise<string[]>} A promise that resolves to an array of allowed request methods for this resource.
|
|
509
449
|
*/
|
|
510
450
|
async options(path, options) {
|
|
511
|
-
|
|
451
|
+
const response = await this.#request(path, options, { method: HttpRequestMethod.OPTIONS });
|
|
452
|
+
|
|
453
|
+
return response.headers.get('allow').split(',').map((method) => method.trim());
|
|
512
454
|
}
|
|
513
455
|
|
|
514
456
|
/**
|
|
@@ -516,11 +458,11 @@ export default class Transportr {
|
|
|
516
458
|
*
|
|
517
459
|
* @async
|
|
518
460
|
* @param {string} [path] The path to the endpoint you want to hit.
|
|
519
|
-
* @param {RequestOptions} [
|
|
461
|
+
* @param {RequestOptions} [userOptions] The options for the request.
|
|
520
462
|
* @returns {Promise<ResponseBody>} The return value of the function is the return value of the function that is passed to the `then` method of the promise returned by the `fetch` method.
|
|
521
463
|
*/
|
|
522
|
-
async request(path,
|
|
523
|
-
return this.#request(path,
|
|
464
|
+
async request(path, userOptions) {
|
|
465
|
+
return this.#request(path, userOptions, {}, (response) => response);
|
|
524
466
|
}
|
|
525
467
|
|
|
526
468
|
/**
|
|
@@ -532,7 +474,7 @@ export default class Transportr {
|
|
|
532
474
|
* @returns {Promise<JsonObject>} A promise that resolves to the response body as a JSON object.
|
|
533
475
|
*/
|
|
534
476
|
async getJson(path, options) {
|
|
535
|
-
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]:
|
|
477
|
+
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: mediaTypes.get(HttpMediaType.JSON).toString() } }, _handleJson);
|
|
536
478
|
}
|
|
537
479
|
|
|
538
480
|
/**
|
|
@@ -544,7 +486,7 @@ export default class Transportr {
|
|
|
544
486
|
* @returns {Promise<Document>} The result of the function call to #get.
|
|
545
487
|
*/
|
|
546
488
|
async getXml(path, options) {
|
|
547
|
-
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]:
|
|
489
|
+
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: mediaTypes.get(HttpMediaType.XML).toString() } }, _handleXml);
|
|
548
490
|
}
|
|
549
491
|
|
|
550
492
|
/**
|
|
@@ -558,7 +500,7 @@ export default class Transportr {
|
|
|
558
500
|
* method of the promise returned by the `#get` method.
|
|
559
501
|
*/
|
|
560
502
|
async getHtml(path, options) {
|
|
561
|
-
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]:
|
|
503
|
+
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: mediaTypes.get(HttpMediaType.HTML).toString() } }, _handleHtml);
|
|
562
504
|
}
|
|
563
505
|
|
|
564
506
|
/**
|
|
@@ -571,7 +513,7 @@ export default class Transportr {
|
|
|
571
513
|
* @returns {Promise<DocumentFragment>} A promise that resolves to an HTML fragment.
|
|
572
514
|
*/
|
|
573
515
|
async getHtmlFragment(path, options) {
|
|
574
|
-
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]:
|
|
516
|
+
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: mediaTypes.get(HttpMediaType.HTML).toString() } }, _handleHtmlFragment);
|
|
575
517
|
}
|
|
576
518
|
|
|
577
519
|
/**
|
|
@@ -584,7 +526,7 @@ export default class Transportr {
|
|
|
584
526
|
* @returns {Promise<void>} A promise that has been resolved.
|
|
585
527
|
*/
|
|
586
528
|
async getScript(path, options) {
|
|
587
|
-
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]:
|
|
529
|
+
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: mediaTypes.get(HttpMediaType.JAVA_SCRIPT).toString() } }, _handleScript);
|
|
588
530
|
}
|
|
589
531
|
|
|
590
532
|
/**
|
|
@@ -596,7 +538,7 @@ export default class Transportr {
|
|
|
596
538
|
* @returns {Promise<void>} A promise that has been resolved.
|
|
597
539
|
*/
|
|
598
540
|
async getStylesheet(path, options) {
|
|
599
|
-
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]:
|
|
541
|
+
return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: mediaTypes.get(HttpMediaType.CSS).toString() } }, _handleCss);
|
|
600
542
|
}
|
|
601
543
|
|
|
602
544
|
/**
|
|
@@ -660,9 +602,7 @@ export default class Transportr {
|
|
|
660
602
|
* @returns {Promise<ResponseBody>} The result of the #request method.
|
|
661
603
|
*/
|
|
662
604
|
async #get(path, userOptions, options, responseHandler) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return this.#request(path, userOptions, options, responseHandler);
|
|
605
|
+
return this.#request(path, userOptions, { ...options, method: HttpRequestMethod.GET }, responseHandler);
|
|
666
606
|
}
|
|
667
607
|
|
|
668
608
|
/**
|
|
@@ -679,67 +619,49 @@ export default class Transportr {
|
|
|
679
619
|
* @returns {Promise<ResponseBody>} The response from the API call.
|
|
680
620
|
*/
|
|
681
621
|
async #request(path, userOptions = {}, options = {}, responseHandler) {
|
|
682
|
-
if (_type(path) == Object) {
|
|
683
|
-
userOptions = path;
|
|
684
|
-
path = undefined;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
const requestOptions = _objectMerge(this.#options, Transportr.#convertRequestOptions(userOptions), options);
|
|
688
|
-
const url = Transportr.#createUrl(this.#baseUrl, path, requestOptions.searchParams);
|
|
689
|
-
const signalController = new SignalController(requestOptions.signal);
|
|
690
|
-
|
|
691
|
-
Transportr.#activeRequests.push(signalController);
|
|
692
|
-
requestOptions.signal = signalController.signal;
|
|
622
|
+
if (_type(path) == Object) { [ path, userOptions ] = [ undefined, path ] }
|
|
693
623
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return Promise.reject(new HttpError(url, { cause: error })); // reject a promise instead of throwing error
|
|
699
|
-
}
|
|
700
|
-
} else if (requestOptions.method == HttpRequestMethod.GET && requestOptions.headers[HttpRequestHeader.CONTENT_TYPE] != '') {
|
|
701
|
-
delete requestOptions.headers[HttpRequestHeader.CONTENT_TYPE];
|
|
702
|
-
delete requestOptions.body;
|
|
624
|
+
try {
|
|
625
|
+
options = this.#processRequestOptions(userOptions, options);
|
|
626
|
+
} catch (cause) {
|
|
627
|
+
return Promise.reject(new HttpError('Unable to process request options', { cause }));
|
|
703
628
|
}
|
|
704
629
|
|
|
705
|
-
|
|
706
|
-
requestOptions.signal.addEventListener('timeout', (event) => this.#publish(Transportr.Events.TIMEOUT, requestOptions.global, event));
|
|
630
|
+
this.#publish({ name: RequestEvents.CONFIGURED, data: options, global: options.global });
|
|
707
631
|
|
|
708
|
-
this.#
|
|
632
|
+
const url = Transportr.#createUrl(this.#baseUrl, path);
|
|
633
|
+
if (_type(options.signal) != AbortSignal) { options.signal = new AbortSignal(options.signal) }
|
|
634
|
+
options.signal.addEventListener(SignalEvents.ABORT, (event) => this.#publish({ name: RequestEvents.ABORTED, event, global: options.global }));
|
|
635
|
+
options.signal.addEventListener(SignalEvents.TIMEOUT, (event) => this.#publish({ name: RequestEvents.TIMEOUT, event, global: options.global }));
|
|
709
636
|
|
|
710
|
-
|
|
637
|
+
Transportr.#activeRequests.add(options.signal);
|
|
711
638
|
|
|
639
|
+
let response, result;
|
|
712
640
|
try {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
641
|
+
// Proxy the options and trap for the signal to be accessed to start the timeout timer
|
|
642
|
+
response = await fetch(url, new Proxy(options, { get: Transportr.#requestOptionsProxyHandler(options.timeout) }));
|
|
643
|
+
|
|
644
|
+
if (!responseHandler && response.status != 204 && response.headers.has(HttpResponseHeader.CONTENT_TYPE)) {
|
|
645
|
+
responseHandler = this.#getResponseHandler(response.headers.get(HttpResponseHeader.CONTENT_TYPE));
|
|
646
|
+
}
|
|
718
647
|
|
|
719
|
-
|
|
648
|
+
result = await responseHandler?.(response) ?? response;
|
|
720
649
|
|
|
721
650
|
if (!response.ok) {
|
|
722
|
-
|
|
723
|
-
return Promise.reject(this.#handleError(url, { status: Transportr.#generateResponseStatusFromError('ResponseError', response), entity: await this.#processResponse(response, url) }));
|
|
651
|
+
return Promise.reject(this.#handleError(url, { status: Transportr.#generateResponseStatusFromError('ResponseError', response), entity: result }));
|
|
724
652
|
}
|
|
725
|
-
|
|
726
|
-
result = await this.#processResponse(response, url, responseHandler);
|
|
727
|
-
this.#publish(Transportr.Events.SUCCESS, requestOptions.global, result);
|
|
653
|
+
this.#publish({ name: RequestEvents.SUCCESS, data: result, global: options.global });
|
|
728
654
|
} catch (error) {
|
|
729
655
|
return Promise.reject(this.#handleError(url, { cause: error, status: Transportr.#generateResponseStatusFromError(error.name, response) }));
|
|
730
656
|
} finally {
|
|
731
|
-
clearTimeout(
|
|
732
|
-
if (!
|
|
733
|
-
this.#publish(
|
|
734
|
-
|
|
735
|
-
// Remove the completed request's signalController from the array
|
|
736
|
-
const index = Transportr.#activeRequests.indexOf(signalController);
|
|
737
|
-
if (index > -1) {
|
|
738
|
-
Transportr.#activeRequests.splice(index, 1);
|
|
739
|
-
}
|
|
657
|
+
options.signal.clearTimeout();
|
|
658
|
+
if (!options.signal.aborted) {
|
|
659
|
+
this.#publish({ name: RequestEvents.COMPLETE, data: response, global: options.global });
|
|
740
660
|
|
|
741
|
-
|
|
742
|
-
|
|
661
|
+
Transportr.#activeRequests.delete(options.signal);
|
|
662
|
+
|
|
663
|
+
if (Transportr.#activeRequests.size === 0) {
|
|
664
|
+
this.#publish({ name: RequestEvents.ALL_COMPLETE, global: options.global });
|
|
743
665
|
}
|
|
744
666
|
}
|
|
745
667
|
}
|
|
@@ -748,86 +670,112 @@ export default class Transportr {
|
|
|
748
670
|
}
|
|
749
671
|
|
|
750
672
|
/**
|
|
751
|
-
*
|
|
673
|
+
* Creates the options for a {@link Transportr} instance.
|
|
752
674
|
*
|
|
753
675
|
* @private
|
|
754
|
-
* @
|
|
755
|
-
* @param {
|
|
756
|
-
* @
|
|
676
|
+
* @static
|
|
677
|
+
* @param {RequestOptions} userOptions The {@link RequestOptions} to convert.
|
|
678
|
+
* @param {RequestOptions} options The default {@link RequestOptions}.
|
|
679
|
+
* @returns {RequestOptions} The converted {@link RequestOptions}.
|
|
757
680
|
*/
|
|
758
|
-
#
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
681
|
+
static #createOptions({ body, headers: userHeaders, searchParams: userSearchParams, ...userOptions }, { headers, searchParams, ...options }) {
|
|
682
|
+
return _objectMerge(options, userOptions, {
|
|
683
|
+
body: [FormData, URLSearchParams, Object].includes(_type(body)) ? new ParameterMap(body) : body,
|
|
684
|
+
headers: Transportr.#mergeOptions(new Headers(), userHeaders, headers),
|
|
685
|
+
searchParams: Transportr.#mergeOptions(new URLSearchParams(), userSearchParams, searchParams)
|
|
686
|
+
});
|
|
763
687
|
}
|
|
764
688
|
|
|
765
689
|
/**
|
|
766
|
-
*
|
|
690
|
+
* Merge the user options and request options into the target.
|
|
767
691
|
*
|
|
768
692
|
* @private
|
|
769
|
-
* @
|
|
770
|
-
* @param {
|
|
771
|
-
* @param {
|
|
772
|
-
* @param {
|
|
773
|
-
* @returns {
|
|
774
|
-
*/
|
|
775
|
-
#
|
|
776
|
-
|
|
777
|
-
|
|
693
|
+
* @static
|
|
694
|
+
* @param {Headers|URLSearchParams|FormData} target The target to merge the options into.
|
|
695
|
+
* @param {Headers|URLSearchParams|FormData|Object} userOption The user options to merge into the target.
|
|
696
|
+
* @param {Headers|URLSearchParams|FormData|Object} requestOption The request options to merge into the target.
|
|
697
|
+
* @returns {Headers|URLSearchParams} The target.
|
|
698
|
+
*/
|
|
699
|
+
static #mergeOptions(target, userOption = {}, requestOption = {}) {
|
|
700
|
+
for (const option of [userOption, requestOption]) {
|
|
701
|
+
for (const [name, value] of option.entries?.() ?? Object.entries(option)) { target.set(name, value) }
|
|
778
702
|
}
|
|
779
|
-
|
|
703
|
+
|
|
704
|
+
return target;
|
|
780
705
|
}
|
|
781
706
|
|
|
782
707
|
/**
|
|
783
|
-
*
|
|
708
|
+
* Merges the user options and request options with the instance options into a new object that is used for the request.
|
|
784
709
|
*
|
|
785
710
|
* @private
|
|
786
|
-
* @
|
|
787
|
-
* @param {
|
|
788
|
-
* @
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
711
|
+
* @param {RequestOptions} userOptions The user options to merge into the request options.
|
|
712
|
+
* @param {RequestOptions} options The request options to merge into the user options.
|
|
713
|
+
* @returns {RequestOptions} The merged options.
|
|
714
|
+
*/
|
|
715
|
+
#processRequestOptions({ body: userBody, headers: userHeaders, searchParams: userSearchParams, ...userOptions }, { headers, searchParams, ...options }) {
|
|
716
|
+
const requestOptions = _objectMerge(this.#options, userOptions, options, {
|
|
717
|
+
headers: Transportr.#mergeOptions(new Headers(this.#options.headers), userHeaders, headers),
|
|
718
|
+
searchParams: Transportr.#mergeOptions(new URLSearchParams(this.#options.searchParams), userSearchParams, searchParams)
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
if (requestBodyMethods.includes(requestOptions.method)) {
|
|
722
|
+
if ([ParameterMap, FormData, URLSearchParams, Object].includes(_type(userBody))) {
|
|
723
|
+
const contentType = requestOptions.headers.get(HttpRequestHeader.CONTENT_TYPE);
|
|
724
|
+
const mediaType = (mediaTypes.get(contentType) ?? MediaType.parse(contentType))?.subtype;
|
|
725
|
+
if (mediaType == HttpMediaType.MULTIPART_FORM_DATA) {
|
|
726
|
+
requestOptions.body = Transportr.#mergeOptions(new FormData(requestOptions.body), userBody);
|
|
727
|
+
} else if (mediaType == HttpMediaType.FORM) {
|
|
728
|
+
requestOptions.body = Transportr.#mergeOptions(new URLSearchParams(requestOptions.body), userBody);
|
|
729
|
+
} else if (mediaType.includes('json')) {
|
|
730
|
+
requestOptions.body = JSON.stringify(Transportr.#mergeOptions(new ParameterMap(requestOptions.body), userBody));
|
|
731
|
+
} else {
|
|
732
|
+
requestOptions.body = Transportr.#mergeOptions(new ParameterMap(requestOptions.body), userBody);
|
|
733
|
+
}
|
|
734
|
+
} else {
|
|
735
|
+
requestOptions.body = userBody;
|
|
736
|
+
}
|
|
737
|
+
} else {
|
|
738
|
+
requestOptions.headers.delete(HttpRequestHeader.CONTENT_TYPE);
|
|
739
|
+
if (!requestOptions.body?.isEmpty()) {
|
|
740
|
+
Transportr.#mergeOptions(requestOptions.searchParams, requestOptions.body);
|
|
741
|
+
}
|
|
742
|
+
requestOptions.body = undefined;
|
|
796
743
|
}
|
|
744
|
+
|
|
745
|
+
return requestOptions;
|
|
797
746
|
}
|
|
798
747
|
|
|
799
748
|
/**
|
|
800
|
-
*
|
|
801
|
-
*
|
|
749
|
+
* Returns a Proxy of the options object that traps for 'signal' access.
|
|
750
|
+
* If the signal has not been aborted, then it will return a new signal with a timeout.
|
|
802
751
|
*
|
|
803
752
|
* @private
|
|
804
753
|
* @static
|
|
805
|
-
* @
|
|
806
|
-
* @
|
|
807
|
-
* @param {URL} url The path to the resource you want to access. Used for error handling.
|
|
808
|
-
* @param {ResponseHandler<ResponseBody>} [handler] The handler to use for processing the response.
|
|
809
|
-
* @returns {Promise<ResponseBody>} The response is being returned.
|
|
754
|
+
* @param {number} timeout The timeout in milliseconds before the signal is aborted.
|
|
755
|
+
* @returns {Proxy<RequestOptions>} A proxy for the options object.
|
|
810
756
|
*/
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
if (mediaType) {
|
|
818
|
-
for (const [responseHandler, contentTypes] of Transportr.#contentTypeHandlers) {
|
|
819
|
-
if (contentTypes.has(mediaType.type) || contentTypes.has(mediaType.subtype)) {
|
|
820
|
-
handler = responseHandler;
|
|
821
|
-
break;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
757
|
+
static #requestOptionsProxyHandler(timeout) {
|
|
758
|
+
return (target, property) => {
|
|
759
|
+
const value = Reflect.get(target, property);
|
|
760
|
+
return property == 'signal' && !value.aborted ? value.withTimeout(timeout) : value;
|
|
761
|
+
};
|
|
762
|
+
}
|
|
826
763
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
764
|
+
/**
|
|
765
|
+
* It takes a url or a string, and returns a {@link URL} instance.
|
|
766
|
+
* If the url is a string and starts with a slash, then the origin of the current page is used as the base url.
|
|
767
|
+
*
|
|
768
|
+
* @private
|
|
769
|
+
* @static
|
|
770
|
+
* @param {URL|string} url The URL to convert to a {@link URL} instance.
|
|
771
|
+
* @returns {URL} A {@link URL} instance.
|
|
772
|
+
* @throws {TypeError} If the url is not a string or {@link URL} instance.
|
|
773
|
+
*/
|
|
774
|
+
static #getBaseUrl(url) {
|
|
775
|
+
switch (_type(url)) {
|
|
776
|
+
case URL: return url;
|
|
777
|
+
case String: return new URL(url, url.startsWith('/') ? globalThis.location.origin : undefined);
|
|
778
|
+
default: throw new TypeError('Invalid URL');
|
|
831
779
|
}
|
|
832
780
|
}
|
|
833
781
|
|
|
@@ -838,55 +786,83 @@ export default class Transportr {
|
|
|
838
786
|
* @private
|
|
839
787
|
* @static
|
|
840
788
|
* @param {URL} url The URL to use as a base.
|
|
841
|
-
* @param {string} [path] The path to the resource. This
|
|
842
|
-
* @param {Object<string, string>} [searchParams={}] An object containing the query parameters to be added to the URL.
|
|
789
|
+
* @param {string} [path] The optional, relative path to the resource. This MUST be a relative path, otherwise, you should create a new {@link Transportr} instance.
|
|
843
790
|
* @returns {URL} A new URL object with the pathname and origin of the url parameter, and the path parameter
|
|
844
791
|
* appended to the end of the pathname.
|
|
845
792
|
*/
|
|
846
|
-
static #createUrl(url, path
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
793
|
+
static #createUrl(url, path) {
|
|
794
|
+
return path ? new URL(`${url.pathname.replace(endsWithSlashRegEx, '')}${path}`, url.origin) : new URL(url);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Generates a ResponseStatus object based on the error name and the response.
|
|
799
|
+
*
|
|
800
|
+
* @private
|
|
801
|
+
* @static
|
|
802
|
+
* @param {string} errorName The name of the error.
|
|
803
|
+
* @param {Response} response The response object returned by the fetch API.
|
|
804
|
+
* @returns {ResponseStatus} The response status object.
|
|
805
|
+
*/
|
|
806
|
+
static #generateResponseStatusFromError(errorName, response) {
|
|
807
|
+
switch (errorName) {
|
|
808
|
+
case 'AbortError': return Transportr.#eventResponseStatuses.get(RequestEvents.ABORTED);
|
|
809
|
+
case 'TimeoutError': return Transportr.#eventResponseStatuses.get(RequestEvents.TIMEOUT);
|
|
810
|
+
default: return response ? new ResponseStatus(response.status, response.statusText) : new ResponseStatus(500, 'Internal Server Error');
|
|
854
811
|
}
|
|
812
|
+
}
|
|
855
813
|
|
|
856
|
-
|
|
814
|
+
/**
|
|
815
|
+
* Handles an error by logging it and throwing it.
|
|
816
|
+
*
|
|
817
|
+
* @private
|
|
818
|
+
* @param {URL} url The path to the resource you want to access.
|
|
819
|
+
* @param {import('./http-error.js').HttpErrorOptions} options The options for the HttpError.
|
|
820
|
+
* @returns {HttpError} The HttpError.
|
|
821
|
+
*/
|
|
822
|
+
#handleError(url, options) {
|
|
823
|
+
const error = new HttpError(`An error has occurred with your request to: '${url}'`, options);
|
|
824
|
+
this.#publish({ name: RequestEvents.ERROR, data: error });
|
|
857
825
|
|
|
858
|
-
return
|
|
826
|
+
return error;
|
|
859
827
|
}
|
|
860
828
|
|
|
861
829
|
/**
|
|
862
|
-
*
|
|
863
|
-
* needs to be serialized.
|
|
830
|
+
* Publishes an event to the global and instance subscribers.
|
|
864
831
|
*
|
|
865
832
|
* @private
|
|
866
|
-
* @
|
|
867
|
-
* @param {
|
|
868
|
-
* @param {
|
|
869
|
-
* @
|
|
833
|
+
* @param {Object} options The options for the event.
|
|
834
|
+
* @param {string} options.name The name of the event.
|
|
835
|
+
* @param {Event} [options.event] The event object.
|
|
836
|
+
* @param {*} [options.data] The data to pass to the subscribers.
|
|
837
|
+
* @param {boolean} [options.global=true] Whether or not to publish the event to the global subscribers.
|
|
838
|
+
* @returns {void}
|
|
870
839
|
*/
|
|
871
|
-
|
|
872
|
-
|
|
840
|
+
#publish({ name, event = new CustomEvent(name), data, global = true } = {}) {
|
|
841
|
+
if (global) { Transportr.#globalSubscribr.publish(name, event, data) }
|
|
842
|
+
this.#subscribr.publish(name, event, data);
|
|
873
843
|
}
|
|
874
844
|
|
|
875
845
|
/**
|
|
846
|
+
* Returns a response handler for the given content type.
|
|
876
847
|
*
|
|
877
|
-
* @
|
|
878
|
-
* @
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
848
|
+
* @private
|
|
849
|
+
* @param {string} contentType The content type of the response.
|
|
850
|
+
* @returns {ResponseHandler<ResponseBody>} The response handler.
|
|
851
|
+
*/
|
|
852
|
+
#getResponseHandler(contentType) {
|
|
853
|
+
let handler;
|
|
854
|
+
const mediaType = MediaType.parse(contentType);
|
|
855
|
+
|
|
856
|
+
if (mediaType) {
|
|
857
|
+
for (const [responseHandler, contentTypes] of Transportr.#contentTypeHandlers) {
|
|
858
|
+
if (contentTypes.has(mediaType.type) || contentTypes.has(mediaType.subtype)) {
|
|
859
|
+
handler = responseHandler;
|
|
860
|
+
break;
|
|
885
861
|
}
|
|
886
862
|
}
|
|
887
863
|
}
|
|
888
864
|
|
|
889
|
-
return
|
|
865
|
+
return handler;
|
|
890
866
|
}
|
|
891
867
|
|
|
892
868
|
/**
|