@d1g1tal/transportr 1.4.2 → 2.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/src/transportr.js DELETED
@@ -1,838 +0,0 @@
1
- import Subscribr from '@d1g1tal/subscribr';
2
- import AbortSignal from './abort-signal.js';
3
- import HttpError from './http-error.js';
4
- import HttpMediaType from './http-media-type.js';
5
- import HttpRequestHeader from './http-request-headers.js';
6
- import HttpRequestMethod from './http-request-methods.js';
7
- import HttpResponseHeader from './http-response-headers.js';
8
- import ParameterMap from './parameter-map.js';
9
- import ResponseStatus from './response-status.js';
10
- import MediaType from '@d1g1tal/media-type';
11
- import { _objectMerge, _type } from '@d1g1tal/chrysalis';
12
- import { RequestEvents, abortEvent, abortSignalProxyHandler, endsWithSlashRegEx, eventResponseStatuses, internalServerError, mediaTypes, requestBodyMethods } from './constants.js';
13
-
14
- /**
15
- * @template T extends ResponseBody
16
- * @typedef {function(Response): Promise<T>} ResponseHandler<T>
17
- */
18
-
19
- /**
20
- * @typedef {Object} ContextEventHandler
21
- * @property {*} context The context object.
22
- * @property {function(*): void} eventHandler The event handler.
23
- */
24
-
25
- /**
26
- * @typedef {Object} EventRegistration
27
- * @property {string} eventName The name of the event to subscribe to.
28
- * @property {ContextEventHandler} contextEventHandler The context event handler.
29
- */
30
-
31
- /** @typedef {Object.prototype.constructor} Type */
32
- /** @typedef {Object<string, (boolean|string|number|Array)>} JsonObject */
33
- /** @typedef {'configured'|'success'|'error'|'aborted'|'timeout'|'complete'} TransportrEvent */
34
- /** @typedef {Blob|ArrayBuffer|TypedArray|DataView|FormData|URLSearchParams|string|ReadableStream} RequestBody */
35
- /** @typedef {JsonObject|Document|DocumentFragment|Blob|ArrayBuffer|FormData|string|ReadableStream<Uint8Array>} ResponseBody */
36
- /** @typedef {'default'|'force-cache'|'no-cache'|'no-store'|'only-if-cached'|'reload'} RequestCache */
37
- /** @typedef {'include'|'omit'|'same-origin'} RequestCredentials */
38
- /** @typedef {Headers|Object<string, string>} RequestHeaders */
39
- /** @typedef {'GET'|'POST'|'PUT'|'PATCH'|'DELETE'|'HEAD'|'OPTIONS'} RequestMethod */
40
- /** @typedef {'cors'|'navigate'|'no-cors'|'same-origin'} RequestMode */
41
- /** @typedef {'error'|'follow'|'manual'} RequestRedirect */
42
- /** @typedef {URLSearchParams|FormData|Object<string, string>|string} SearchParameters */
43
- /** @typedef {''|'no-referrer'|'no-referrer-when-downgrade'|'origin'|'origin-when-cross-origin'|'same-origin'|'strict-origin'|'strict-origin-when-cross-origin'|'unsafe-url'} ReferrerPolicy */
44
- /** @typedef {Int8Array|Uint8Array|Uint8ClampedArray|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array|BigInt64Array|BigUint64Array} TypedArray */
45
-
46
- /**
47
- * The options for a {@link Request} object or the second parameter of a {@link fetch} request
48
- *
49
- * @typedef {Object} RequestOptions
50
- * @property {RequestBody} body A RequestInit object or null to set request's body.
51
- * @property {RequestCache} cache A string indicating how the request will interact with the browser's cache to set request's cache.
52
- * @property {RequestCredentials} credentials A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials.
53
- * @property {RequestHeaders} headers A Headers object, an object literal, or an array of two-item arrays to set request's headers.
54
- * @property {SearchParameters} searchParams The parameters to be added to the URL for the request.
55
- * @property {string} integrity A cryptographic hash of the resource to be fetched by request. Sets request's integrity.
56
- * @property {boolean} keepalive A boolean to set request's keepalive.
57
- * @property {RequestMethod} method A string to set request's method.
58
- * @property {RequestMode} mode A string to indicate whether the request will use CORS, or will be restricted to same-origin URLs. Sets request's mode.
59
- * @property {RequestRedirect} redirect A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect.
60
- * @property {string} referrer A string whose value is a same-origin URL, "about:client", or the empty string, to set request's referrer.
61
- * @property {ReferrerPolicy} referrerPolicy A referrer policy to set request's referrerPolicy.
62
- * @property {AbortSignal} signal An AbortSignal to set request's signal.
63
- * @property {number} timeout A number representing a timeout in milliseconds for request.
64
- * @property {boolean} global If true, it will trigger the global event handlers. Defaults to true.
65
- * @property {null} window Can only be null. Used to disassociate request from any Window.
66
- */
67
-
68
- /** @type {ResponseHandler<string>} */
69
- const _handleText = async (response) => await response.text();
70
-
71
- /** @type {ResponseHandler<void>} */
72
- const _handleScript = async (response) => {
73
- const objectURL = URL.createObjectURL(await response.blob());
74
-
75
- document.head.removeChild(document.head.appendChild(Object.assign(document.createElement('script'), { src: objectURL, type: HttpMediaType.JAVA_SCRIPT, async: true })));
76
-
77
- URL.revokeObjectURL(objectURL);
78
-
79
- return Promise.resolve();
80
- };
81
-
82
- /** @type {ResponseHandler<void>} */
83
- const _handleCss = async (response) => {
84
- const objectURL = URL.createObjectURL(await response.blob());
85
-
86
- document.head.appendChild(Object.assign(document.createElement('link'), { href: objectURL, type: HttpMediaType.CSS, rel: 'stylesheet' }));
87
-
88
- URL.revokeObjectURL(objectURL);
89
-
90
- return Promise.resolve();
91
- };
92
-
93
- /** @type {ResponseHandler<JsonObject>} */
94
- const _handleJson = async (response) => await response.json();
95
-
96
- /** @type {ResponseHandler<Blob>} */
97
- const _handleBlob = async (response) => await response.blob();
98
-
99
- /** @type {ResponseHandler<string>} */
100
- const _handleImage = async (response) => URL.createObjectURL(await response.blob());
101
-
102
- /** @type {ResponseHandler<ArrayBuffer>} */
103
- const _handleBuffer = async (response) => await response.arrayBuffer();
104
-
105
- /** @type {ResponseHandler<ReadableStream<Uint8Array>>} */
106
- const _handleReadableStream = async (response) => response.body;
107
-
108
- /** @type {ResponseHandler<Document>} */
109
- const _handleXml = async (response) => new DOMParser().parseFromString(await response.text(), HttpMediaType.XML);
110
-
111
- /** @type {ResponseHandler<Document>} */
112
- const _handleHtml = async (response) => new DOMParser().parseFromString(await response.text(), HttpMediaType.HTML);
113
-
114
- /** @type {ResponseHandler<DocumentFragment>} */
115
- const _handleHtmlFragment = async (response) => document.createRange().createContextualFragment(await response.text());
116
-
117
- /**
118
- * A wrapper around the fetch API that makes it easier to make HTTP requests.
119
- *
120
- * @module {Transportr} transportr
121
- * @author D1g1talEntr0py <jason.dimeo@gmail.com>
122
- */
123
- export default class Transportr {
124
- /** @type {URL} */
125
- #baseUrl;
126
- /** @type {RequestOptions} */
127
- #options;
128
- /** @type {Subscribr} */
129
- #subscribr;
130
- /** @type {Subscribr} */
131
- static #globalSubscribr = new Subscribr();
132
- /** @type {Set<AbortSignal>} */
133
- static #activeRequests = new Set();
134
- /** @type {Map<ResponseHandler<ResponseBody>, string>} */
135
- static #contentTypeHandlers = new Map([
136
- [_handleImage, mediaTypes.get(HttpMediaType.PNG).type],
137
- [_handleText, mediaTypes.get(HttpMediaType.TEXT).type],
138
- [_handleJson, mediaTypes.get(HttpMediaType.JSON).subtype],
139
- [_handleHtml, mediaTypes.get(HttpMediaType.HTML).subtype],
140
- [_handleScript, mediaTypes.get(HttpMediaType.JAVA_SCRIPT).subtype],
141
- [_handleCss, mediaTypes.get(HttpMediaType.CSS).subtype],
142
- [_handleXml, mediaTypes.get(HttpMediaType.XML).subtype],
143
- [_handleReadableStream, mediaTypes.get(HttpMediaType.BIN).subtype]
144
- ]);
145
-
146
- /**
147
- * Create a new Transportr instance with the provided location or origin and context path.
148
- *
149
- * @param {URL|string|RequestOptions} [url=location.origin] The URL for {@link fetch} requests.
150
- * @param {RequestOptions} [options={}] The default {@link RequestOptions} for this instance.
151
- */
152
- constructor(url = globalThis.location.origin, options = {}) {
153
- if (_type(url) == Object) { [ url, options ] = [ globalThis.location.origin, url ] }
154
-
155
- this.#baseUrl = Transportr.#getBaseUrl(url);
156
- this.#options = Transportr.#createOptions(options, Transportr.#defaultRequestOptions);
157
- this.#subscribr = new Subscribr();
158
- }
159
-
160
- /**
161
- * @static
162
- * @constant {Object<string, HttpRequestMethod>}
163
- */
164
- static Method = Object.freeze(HttpRequestMethod);
165
-
166
- /**
167
- * @static
168
- * @constant {Object<string, HttpMediaType>}
169
- */
170
- static MediaType = Object.freeze(HttpMediaType);
171
-
172
- /**
173
- * @static
174
- * @see {@link HttpRequestHeader}
175
- * @constant {Object<string, HttpRequestHeader>}
176
- */
177
- static RequestHeader = Object.freeze(HttpRequestHeader);
178
-
179
- /**
180
- * @static
181
- * @constant {Object<string, HttpResponseHeader>}
182
- */
183
- static ResponseHeader = Object.freeze(HttpResponseHeader);
184
-
185
- /**
186
- * @static
187
- * @constant {Object<string, RequestCache>}
188
- */
189
- static CachingPolicy = Object.freeze({
190
- DEFAULT: 'default',
191
- FORCE_CACHE: 'force-cache',
192
- NO_CACHE: 'no-cache',
193
- NO_STORE: 'no-store',
194
- ONLY_IF_CACHED: 'only-if-cached',
195
- RELOAD: 'reload'
196
- });
197
-
198
- /**
199
- * @static
200
- * @constant {Object<string, RequestCredentials>}
201
- */
202
- static CredentialsPolicy = Object.freeze({
203
- INCLUDE: 'include',
204
- OMIT: 'omit',
205
- SAME_ORIGIN: 'same-origin'
206
- });
207
-
208
- /**
209
- * @static
210
- * @constant {Object<string, RequestMode>}
211
- */
212
- static RequestMode = Object.freeze({
213
- CORS: 'cors',
214
- NAVIGATE: 'navigate',
215
- NO_CORS: 'no-cors',
216
- SAME_ORIGIN: 'same-origin'
217
- });
218
-
219
- /**
220
- * @static
221
- * @constant {Object<string, RequestRedirect>}
222
- */
223
- static RedirectPolicy = Object.freeze({
224
- ERROR: 'error',
225
- FOLLOW: 'follow',
226
- MANUAL: 'manual'
227
- });
228
-
229
- /**
230
- * @static
231
- * @constant {Object<string, ReferrerPolicy>}
232
- */
233
- static ReferrerPolicy = Object.freeze({
234
- NO_REFERRER: 'no-referrer',
235
- NO_REFERRER_WHEN_DOWNGRADE: 'no-referrer-when-downgrade',
236
- ORIGIN: 'origin',
237
- ORIGIN_WHEN_CROSS_ORIGIN: 'origin-when-cross-origin',
238
- SAME_ORIGIN: 'same-origin',
239
- STRICT_ORIGIN: 'strict-origin',
240
- STRICT_ORIGIN_WHEN_CROSS_ORIGIN: 'strict-origin-when-cross-origin',
241
- UNSAFE_URL: 'unsafe-url'
242
- });
243
-
244
- /**
245
- * @static
246
- * @constant {Object<string, TransportrEvent>}
247
- */
248
- static Events = RequestEvents;
249
-
250
- /**
251
- * @private
252
- * @static
253
- * @type {RequestOptions}
254
- */
255
- static #defaultRequestOptions = Object.freeze({
256
- body: null,
257
- cache: Transportr.CachingPolicy.NO_STORE,
258
- credentials: Transportr.CredentialsPolicy.SAME_ORIGIN,
259
- headers: { [HttpRequestHeader.CONTENT_TYPE]: `${mediaTypes.get(HttpMediaType.JSON)}`, [HttpRequestHeader.ACCEPT]: `${mediaTypes.get(HttpMediaType.JSON)}` },
260
- searchParams: {},
261
- integrity: undefined,
262
- keepalive: undefined,
263
- method: HttpRequestMethod.GET,
264
- mode: Transportr.RequestMode.CORS,
265
- redirect: Transportr.RedirectPolicy.FOLLOW,
266
- referrer: 'about:client',
267
- referrerPolicy: Transportr.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN,
268
- signal: undefined,
269
- timeout: 30000,
270
- global: true,
271
- window: null
272
- });
273
-
274
- /**
275
- * Returns a {@link EventRegistration} used for subscribing to global events.
276
- *
277
- * @static
278
- * @param {TransportrEvent} event The event to subscribe to.
279
- * @param {function(Event, *): void} handler The event handler.
280
- * @param {*} context The context to bind the handler to.
281
- * @returns {EventRegistration} A new {@link EventRegistration} instance.
282
- */
283
- static register(event, handler, context) {
284
- return Transportr.#globalSubscribr.subscribe(event, handler, context);
285
- }
286
-
287
- /**
288
- * Removes a {@link EventRegistration} from the global event handler.
289
- *
290
- * @static
291
- * @param {EventRegistration} eventRegistration The {@link EventRegistration} to remove.
292
- * @returns {boolean} True if the {@link EventRegistration} was removed, false otherwise.
293
- */
294
- static unregister(eventRegistration) {
295
- return Transportr.#globalSubscribr.unsubscribe(eventRegistration);
296
- }
297
-
298
- /**
299
- * Aborts all active requests.
300
- * This is useful for when the user navigates away from the current page.
301
- * This will also clear the {@link Transportr#activeRequests} set.
302
- *
303
- * @static
304
- * @returns {void}
305
- */
306
- static abortAll() {
307
- for (const abortSignal of this.#activeRequests) {
308
- abortSignal.abort(abortEvent);
309
- }
310
-
311
- // Clear the array after aborting all requests
312
- this.#activeRequests.clear();
313
- }
314
-
315
- /**
316
- * It returns the base {@link URL} for the API.
317
- *
318
- * @returns {URL} The baseUrl property.
319
- */
320
- get baseUrl() {
321
- return this.#baseUrl;
322
- }
323
-
324
- /**
325
- * Registers an event handler with a {@link Transportr} instance.
326
- *
327
- * @param {TransportrEvent} event The name of the event to listen for.
328
- * @param {function(Event, *): void} handler The function to call when the event is triggered.
329
- * @param {*} [context] The context to bind to the handler.
330
- * @returns {EventRegistration} An object that can be used to remove the event handler.
331
- */
332
- register(event, handler, context) {
333
- return this.#subscribr.subscribe(event, handler, context);
334
- }
335
-
336
- /**
337
- * Unregisters an event handler from a {@link Transportr} instance.
338
- *
339
- * @param {EventRegistration} eventRegistration The event registration to remove.
340
- * @returns {void}
341
- */
342
- unregister(eventRegistration) {
343
- this.#subscribr.unsubscribe(eventRegistration);
344
- }
345
-
346
- /**
347
- * This function returns a promise that resolves to the result of a request to the specified path with
348
- * the specified options, where the method is GET.
349
- *
350
- * @async
351
- * @param {string} [path] The path to the resource you want to get.
352
- * @param {RequestOptions} [options] The options for the request.
353
- * @returns {Promise<ResponseBody>} A promise that resolves to the response of the request.
354
- */
355
- async get(path, options) {
356
- return this.#get(path, options);
357
- }
358
-
359
- /**
360
- * This function makes a POST request to the given path with the given body and options.
361
- *
362
- * @async
363
- * @param {string} [path] The path to the endpoint you want to call.
364
- * @param {RequestBody} body The body of the request.
365
- * @param {RequestOptions} [options] The options for the request.
366
- * @returns {Promise<ResponseBody>} A promise that resolves to the response body.
367
- */
368
- async post(path, body = {}, options = {}) {
369
- if (_type(path) != String) { [ path, body, options ] = [ undefined, path, body ] }
370
-
371
- return this.#request(path, Object.assign(options, { body }), { method: HttpRequestMethod.POST });
372
- }
373
-
374
- /**
375
- * This function returns a promise that resolves to the result of a request to the specified path with
376
- * the specified options, where the method is PUT.
377
- *
378
- * @async
379
- * @param {string} [path] The path to the endpoint you want to call.
380
- * @param {RequestOptions} [options] The options for the request.
381
- * @returns {Promise<ResponseBody>} The return value of the #request method.
382
- */
383
- async put(path, options) {
384
- return this.#request(path, options, { method: HttpRequestMethod.PUT });
385
- }
386
-
387
- /**
388
- * It takes a path and options, and returns a request with the method set to PATCH.
389
- *
390
- * @async
391
- * @param {string} [path] The path to the endpoint you want to hit.
392
- * @param {RequestOptions} [options] The options for the request.
393
- * @returns {Promise<ResponseBody>} A promise that resolves to the response of the request.
394
- */
395
- async patch(path, options) {
396
- return this.#request(path, options, { method: HttpRequestMethod.PATCH });
397
- }
398
-
399
- /**
400
- * It takes a path and options, and returns a request with the method set to DELETE.
401
- *
402
- * @async
403
- * @param {string} [path] The path to the resource you want to access.
404
- * @param {RequestOptions} [options] The options for the request.
405
- * @returns {Promise<ResponseBody>} The result of the request.
406
- */
407
- async delete(path, options) {
408
- return this.#request(path, options, { method: HttpRequestMethod.DELETE });
409
- }
410
-
411
- /**
412
- * Returns the response headers of a request to the given path.
413
- *
414
- * @async
415
- * @param {string} [path] The path to the resource you want to access.
416
- * @param {RequestOptions} [options] The options for the request.
417
- * @returns {Promise<ResponseBody>} A promise that resolves to the response object.
418
- */
419
- async head(path, options) {
420
- return this.#request(path, options, { method: HttpRequestMethod.HEAD });
421
- }
422
-
423
- /**
424
- * It returns a promise that resolves to the allowed request methods for the given resource path.
425
- *
426
- * @async
427
- * @param {string} [path] The path to the resource.
428
- * @param {RequestOptions} [options] The options for the request.
429
- * @returns {Promise<string[]>} A promise that resolves to an array of allowed request methods for this resource.
430
- */
431
- async options(path, options) {
432
- const response = await this.#request(path, options, { method: HttpRequestMethod.OPTIONS });
433
-
434
- return response.headers.get('allow').split(',').map((method) => method.trim());
435
- }
436
-
437
- /**
438
- * It takes a path and options, and makes a request to the server.
439
- *
440
- * @async
441
- * @param {string} [path] The path to the endpoint you want to hit.
442
- * @param {RequestOptions} [userOptions] The options for the request.
443
- * @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.
444
- */
445
- async request(path, userOptions) {
446
- return this.#request(path, userOptions, {}, (response) => response);
447
- }
448
-
449
- /**
450
- * It gets a JSON resource from the server.
451
- *
452
- * @async
453
- * @param {string} [path] The path to the resource.
454
- * @param {RequestOptions} [options] The options object to pass to the request.
455
- * @returns {Promise<JsonObject>} A promise that resolves to the response body as a JSON object.
456
- */
457
- async getJson(path, options) {
458
- return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: `${mediaTypes.get(HttpMediaType.JSON)}` } }, _handleJson);
459
- }
460
-
461
- /**
462
- * It gets the XML representation of the resource at the given path.
463
- *
464
- * @async
465
- * @param {string} [path] The path to the resource you want to get.
466
- * @param {RequestOptions} [options] The options for the request.
467
- * @returns {Promise<Document>} The result of the function call to #get.
468
- */
469
- async getXml(path, options) {
470
- return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: `${mediaTypes.get(HttpMediaType.XML)}` } }, _handleXml);
471
- }
472
-
473
- /**
474
- * Get the HTML content of the specified path.
475
- *
476
- * @todo Add way to return portion of the retrieved HTML using a selector. Like jQuery.
477
- * @async
478
- * @param {string} [path] The path to the resource.
479
- * @param {RequestOptions} [options] The options for the request.
480
- * @returns {Promise<Document>} The return value of the function is the return value of the function passed to the `then`
481
- * method of the promise returned by the `#get` method.
482
- */
483
- async getHtml(path, options) {
484
- return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: `${mediaTypes.get(HttpMediaType.HTML)}` } }, _handleHtml);
485
- }
486
-
487
- /**
488
- * It returns a promise that resolves to the HTML fragment at the given path.
489
- *
490
- * @todo Add way to return portion of the retrieved HTML using a selector. Like jQuery.
491
- * @async
492
- * @param {string} [path] The path to the resource.
493
- * @param {RequestOptions} [options] The options for the request.
494
- * @returns {Promise<DocumentFragment>} A promise that resolves to an HTML fragment.
495
- */
496
- async getHtmlFragment(path, options) {
497
- return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: `${mediaTypes.get(HttpMediaType.HTML)}` } }, _handleHtmlFragment);
498
- }
499
-
500
- /**
501
- * It gets a script from the server, and appends the script to the {@link Document} {@link HTMLHeadElement}
502
- * CORS is enabled by default.
503
- *
504
- * @async
505
- * @param {string} [path] The path to the script.
506
- * @param {RequestOptions} [options] The options for the request.
507
- * @returns {Promise<void>} A promise that has been resolved.
508
- */
509
- async getScript(path, options) {
510
- return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: `${mediaTypes.get(HttpMediaType.JAVA_SCRIPT)}` } }, _handleScript);
511
- }
512
-
513
- /**
514
- * Gets a stylesheet from the server, and adds it as a {@link Blob} {@link URL}.
515
- *
516
- * @async
517
- * @param {string} [path] The path to the stylesheet.
518
- * @param {RequestOptions} [options] The options for the request.
519
- * @returns {Promise<void>} A promise that has been resolved.
520
- */
521
- async getStylesheet(path, options) {
522
- return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: `${mediaTypes.get(HttpMediaType.CSS)}` } }, _handleCss);
523
- }
524
-
525
- /**
526
- * It returns a blob from the specified path.
527
- *
528
- * @async
529
- * @param {string} [path] The path to the resource.
530
- * @param {RequestOptions} [options] The options for the request.
531
- * @returns {Promise<Blob>} A promise that resolves to a blob.
532
- */
533
- async getBlob(path, options) {
534
- return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: HttpMediaType.BIN } }, _handleBlob);
535
- }
536
-
537
- /**
538
- * It returns a promise that resolves to an object URL.
539
- *
540
- * @async
541
- * @param {string|RequestOptions} [path] The path to the resource.
542
- * @param {RequestOptions} [options] The options for the request.
543
- * @returns {Promise<string>} A promise that resolves to an object URL.
544
- */
545
- async getImage(path, options) {
546
- return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: 'image/*' } }, _handleImage);
547
- }
548
-
549
- /**
550
- * It gets a buffer from the specified path
551
- *
552
- * @async
553
- * @param {string} [path] The path to the resource.
554
- * @param {RequestOptions} [options] The options for the request.
555
- * @returns {Promise<ArrayBuffer>} A promise that resolves to a buffer.
556
- */
557
- async getBuffer(path, options) {
558
- return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: HttpMediaType.BIN } }, _handleBuffer);
559
- }
560
-
561
- /**
562
- * It returns a readable stream of the response body from the specified path.
563
- *
564
- * @async
565
- * @param {string} [path] The path to the resource.
566
- * @param {RequestOptions} [options] The options for the request.
567
- * @returns {Promise<ReadableStream<Uint8Array>>} A readable stream.
568
- */
569
- async getStream(path, options) {
570
- return this.#get(path, options, { headers: { [HttpRequestHeader.ACCEPT]: HttpMediaType.BIN } }, _handleReadableStream);
571
- }
572
-
573
- /**
574
- * Makes a GET request to the given path, using the given options, and then calls the
575
- * given response handler with the response.
576
- *
577
- * @private
578
- * @async
579
- * @param {string} [path] The path to the endpoint you want to call.
580
- * @param {RequestOptions} [userOptions] The options passed to the public function to use for the request.
581
- * @param {RequestOptions} [options={}] The options for the request.
582
- * @param {ResponseHandler<ResponseBody>} [responseHandler] A function that will be called with the response object.
583
- * @returns {Promise<ResponseBody>} The result of the #request method.
584
- */
585
- async #get(path, userOptions, options = {}, responseHandler) {
586
- return this.#request(path, userOptions, Object.assign(options, { method: HttpRequestMethod.GET }), responseHandler);
587
- }
588
-
589
- /**
590
- * It takes a path, options, and a response handler, and returns a promise that resolves to the
591
- * response entity.
592
- *
593
- * @private
594
- * @async
595
- * @param {string} [path] The path to the resource you want to access.
596
- * @param {RequestOptions} [userOptions={}] The options passed to the public function to use for the request.
597
- * @param {RequestOptions} [options={}] The options to use for the request.
598
- * @param {ResponseHandler<ResponseBody>} [responseHandler] A function that will be called with the response body as a parameter. This
599
- * is useful if you want to do something with the response body before returning it.
600
- * @returns {Promise<ResponseBody>} The response from the API call.
601
- */
602
- async #request(path, userOptions = {}, options = {}, responseHandler) {
603
- if (_type(path) == Object) { [ path, userOptions ] = [ undefined, path ] }
604
-
605
- options = this.#processRequestOptions(userOptions, options);
606
-
607
- let response;
608
- const url = Transportr.#createUrl(this.#baseUrl, path, options.searchParams);
609
- try {
610
- Transportr.#activeRequests.add(options.signal);
611
- // Proxy the options and trap for the `signal` to be accessed to start the timeout timer
612
- response = await fetch(url, new Proxy(options, abortSignalProxyHandler));
613
-
614
- if (!responseHandler && response.status != 204) {
615
- responseHandler = this.#getResponseHandler(response.headers.get(HttpResponseHeader.CONTENT_TYPE));
616
- }
617
-
618
- const result = await responseHandler?.(response) ?? response;
619
-
620
- if (!response.ok) {
621
- return Promise.reject(this.#handleError(url, { status: Transportr.#generateResponseStatusFromError('ResponseError', response), entity: result }));
622
- }
623
-
624
- this.#publish({ name: RequestEvents.SUCCESS, data: result, global: options.global });
625
-
626
- return result;
627
- } catch (cause) {
628
- return Promise.reject(this.#handleError(url, { cause, status: Transportr.#generateResponseStatusFromError(cause.name, response) }));
629
- } finally {
630
- options.signal.clearTimeout();
631
- if (!options.signal.aborted) {
632
- this.#publish({ name: RequestEvents.COMPLETE, data: response, global: options.global });
633
-
634
- Transportr.#activeRequests.delete(options.signal);
635
-
636
- if (Transportr.#activeRequests.size == 0) {
637
- this.#publish({ name: RequestEvents.ALL_COMPLETE, global: options.global });
638
- }
639
- }
640
- }
641
- }
642
-
643
- /**
644
- * Creates the options for a {@link Transportr} instance.
645
- *
646
- * @private
647
- * @static
648
- * @param {RequestOptions} userOptions The {@link RequestOptions} to convert.
649
- * @param {RequestOptions} options The default {@link RequestOptions}.
650
- * @returns {RequestOptions} The converted {@link RequestOptions}.
651
- */
652
- static #createOptions({ body, headers: userHeaders, searchParams: userSearchParams, ...userOptions }, { headers, searchParams, ...options }) {
653
- return _objectMerge(options, userOptions, {
654
- body: [FormData, URLSearchParams, Object].includes(_type(body)) ? new ParameterMap(body) : body,
655
- headers: Transportr.#mergeOptions(new Headers(), userHeaders, headers),
656
- searchParams: Transportr.#mergeOptions(new URLSearchParams(), userSearchParams, searchParams)
657
- });
658
- }
659
-
660
- /**
661
- * Merge the user options and request options into the target.
662
- *
663
- * @private
664
- * @static
665
- * @param {Headers|URLSearchParams|FormData} target The target to merge the options into.
666
- * @param {Headers|URLSearchParams|FormData|Object} userOption The user options to merge into the target.
667
- * @param {Headers|URLSearchParams|FormData|Object} requestOption The request options to merge into the target.
668
- * @returns {Headers|URLSearchParams} The target.
669
- */
670
- static #mergeOptions(target, userOption = {}, requestOption = {}) {
671
- for (const option of [userOption, requestOption]) {
672
- for (const [name, value] of option.entries?.() ?? Object.entries(option)) { target.set(name, value) }
673
- }
674
-
675
- return target;
676
- }
677
-
678
- /**
679
- * Merges the user options and request options with the instance options into a new object that is used for the request.
680
- *
681
- * @private
682
- * @param {RequestOptions} userOptions The user options to merge into the request options.
683
- * @param {RequestOptions} options The request options to merge into the user options.
684
- * @returns {RequestOptions} The merged options.
685
- */
686
- #processRequestOptions({ body: userBody, headers: userHeaders, searchParams: userSearchParams, ...userOptions }, { headers, searchParams, ...options }) {
687
- const requestOptions = _objectMerge(this.#options, userOptions, options, {
688
- headers: Transportr.#mergeOptions(new Headers(this.#options.headers), userHeaders, headers),
689
- searchParams: Transportr.#mergeOptions(new URLSearchParams(this.#options.searchParams), userSearchParams, searchParams)
690
- });
691
-
692
- if (requestBodyMethods.includes(requestOptions.method)) {
693
- if ([ParameterMap, FormData, URLSearchParams, Object].includes(_type(userBody))) {
694
- const contentType = requestOptions.headers.get(HttpRequestHeader.CONTENT_TYPE);
695
- const mediaType = (mediaTypes.get(contentType) ?? MediaType.parse(contentType))?.subtype;
696
- if (mediaType == HttpMediaType.MULTIPART_FORM_DATA) {
697
- requestOptions.body = Transportr.#mergeOptions(new FormData(requestOptions.body), userBody);
698
- } else if (mediaType == HttpMediaType.FORM) {
699
- requestOptions.body = Transportr.#mergeOptions(new URLSearchParams(requestOptions.body), userBody);
700
- } else if (mediaType.includes('json')) {
701
- requestOptions.body = JSON.stringify(Transportr.#mergeOptions(new ParameterMap(requestOptions.body), userBody));
702
- } else {
703
- requestOptions.body = Transportr.#mergeOptions(new ParameterMap(requestOptions.body), userBody);
704
- }
705
- } else {
706
- requestOptions.body = userBody;
707
- }
708
- } else {
709
- requestOptions.headers.delete(HttpRequestHeader.CONTENT_TYPE);
710
- if (requestOptions.body) {
711
- Transportr.#mergeOptions(requestOptions.searchParams, requestOptions.body);
712
- }
713
- requestOptions.body = undefined;
714
- }
715
-
716
- requestOptions.signal = new AbortSignal(requestOptions.signal)
717
- .onAbort((event) => this.#publish({ name: RequestEvents.ABORTED, event, global: requestOptions.global }))
718
- .onTimeout((event) => this.#publish({ name: RequestEvents.TIMEOUT, event, global: requestOptions.global }));
719
-
720
- this.#publish({ name: RequestEvents.CONFIGURED, data: requestOptions, global: requestOptions.global });
721
-
722
- return requestOptions;
723
- }
724
-
725
- /**
726
- * It takes a url or a string, and returns a {@link URL} instance.
727
- * If the url is a string and starts with a slash, then the origin of the current page is used as the base url.
728
- *
729
- * @private
730
- * @static
731
- * @param {URL|string} url The URL to convert to a {@link URL} instance.
732
- * @returns {URL} A {@link URL} instance.
733
- * @throws {TypeError} If the url is not a string or {@link URL} instance.
734
- */
735
- static #getBaseUrl(url) {
736
- switch (_type(url)) {
737
- case URL: return url;
738
- case String: return new URL(url, url.startsWith('/') ? globalThis.location.origin : undefined);
739
- default: throw new TypeError('Invalid URL');
740
- }
741
- }
742
-
743
- /**
744
- * It takes a URL, a path, and a set of search parameters, and returns a new URL with the path and
745
- * search parameters applied.
746
- *
747
- * @private
748
- * @static
749
- * @param {URL} url The URL to use as a base.
750
- * @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.
751
- * @param {URLSearchParams} [searchParams] The optional search parameters to append to the URL.
752
- * @returns {URL} A new URL object with the pathname and origin of the url parameter, and the path parameter appended to the end of the pathname.
753
- */
754
- static #createUrl(url, path, searchParams) {
755
- const requestUrl = path ? new URL(`${url.pathname.replace(endsWithSlashRegEx, '')}${path}`, url.origin) : new URL(url);
756
-
757
- searchParams?.forEach((value, name) => requestUrl.searchParams.append(name, value));
758
-
759
- return requestUrl;
760
- }
761
-
762
- /**
763
- * Generates a ResponseStatus object based on the error name and the response.
764
- *
765
- * @private
766
- * @static
767
- * @param {string} errorName The name of the error.
768
- * @param {Response} response The response object returned by the fetch API.
769
- * @returns {ResponseStatus} The response status object.
770
- */
771
- static #generateResponseStatusFromError(errorName, response) {
772
- switch (errorName) {
773
- case 'AbortError': return eventResponseStatuses[RequestEvents.ABORTED];
774
- case 'TimeoutError': return eventResponseStatuses[RequestEvents.TIMEOUT];
775
- default: return response ? new ResponseStatus(response.status, response.statusText) : internalServerError;
776
- }
777
- }
778
-
779
- /**
780
- * Handles an error by logging it and throwing it.
781
- *
782
- * @private
783
- * @param {URL} url The path to the resource you want to access.
784
- * @param {import('./http-error.js').HttpErrorOptions} options The options for the HttpError.
785
- * @returns {HttpError} The HttpError.
786
- */
787
- #handleError(url, options) {
788
- const error = new HttpError(`An error has occurred with your request to: '${url}'`, options);
789
- this.#publish({ name: RequestEvents.ERROR, data: error });
790
-
791
- return error;
792
- }
793
-
794
- /**
795
- * Publishes an event to the global and instance subscribers.
796
- *
797
- * @private
798
- * @param {Object} options The options for the event.
799
- * @param {string} options.name The name of the event.
800
- * @param {Event} [options.event] The event object.
801
- * @param {*} [options.data] The data to pass to the subscribers.
802
- * @param {boolean} [options.global=true] Whether or not to publish the event to the global subscribers.
803
- * @returns {void}
804
- */
805
- #publish({ name, event = new CustomEvent(name), data, global = true } = {}) {
806
- if (global) { Transportr.#globalSubscribr.publish(name, event, data) }
807
- this.#subscribr.publish(name, event, data);
808
- }
809
-
810
- /**
811
- * Returns a response handler for the given content type.
812
- *
813
- * @private
814
- * @param {string} contentType The content type of the response.
815
- * @returns {ResponseHandler<ResponseBody>} The response handler.
816
- */
817
- #getResponseHandler(contentType) {
818
- const mediaType = MediaType.parse(contentType);
819
-
820
- if (mediaType) {
821
- for (const [responseHandler, contentType] of Transportr.#contentTypeHandlers) {
822
- if (mediaType.matches(contentType)) { return responseHandler }
823
- }
824
- }
825
-
826
- return undefined;
827
- }
828
-
829
- /**
830
- * A String value that is used in the creation of the default string
831
- * description of an object. Called by the built-in method {@link Object.prototype.toString}.
832
- *
833
- * @returns {string} The default string description of this object.
834
- */
835
- get [Symbol.toStringTag]() {
836
- return 'Transportr';
837
- }
838
- }