@d1g1tal/transportr 1.3.2 → 1.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d1g1tal/transportr",
3
- "version": "1.3.2",
3
+ "version": "1.4.1",
4
4
  "description": "JavaScript wrapper for the Fetch API",
5
5
  "type": "module",
6
6
  "exports": {
@@ -45,15 +45,14 @@
45
45
  "@skypack/package-check": "^0.2.2",
46
46
  "@xmldom/xmldom": "^0.8.10",
47
47
  "esbuild": "^0.19.3",
48
- "eslint": "^8.49.0",
48
+ "eslint": "^8.50.0",
49
49
  "eslint-plugin-compat": "^4.2.0",
50
50
  "eslint-plugin-jsdoc": "^46.8.2",
51
51
  "jest": "^29.7.0",
52
- "rimraf": "^5.0.1"
52
+ "rimraf": "^5.0.5"
53
53
  },
54
54
  "dependencies": {
55
55
  "@d1g1tal/chrysalis": "^1.2.3",
56
- "@d1g1tal/collections": "^0.2.3",
57
56
  "@d1g1tal/media-type": "^4.1.0",
58
57
  "@d1g1tal/subscribr": "^3.0.1"
59
58
  },
@@ -1,7 +1,5 @@
1
1
  import { SignalEvents, abortEvent } from './constants.js';
2
2
 
3
- const NativeAbortSignal = globalThis.AbortSignal;
4
-
5
3
  /**
6
4
  * @typedef {function(Event):void} EventListener
7
5
  * @param {Event} event The event object
@@ -22,27 +20,6 @@ export default class AbortSignal extends EventTarget {
22
20
  signal?.addEventListener(SignalEvents.ABORT, (event) => this.#abort(event));
23
21
  }
24
22
 
25
- /**
26
- * Returns an {@link AbortSignal} instance that is already set as aborted.
27
- *
28
- * @static
29
- * @returns {AbortSignal} The abort signal
30
- */
31
- static abort() {
32
- return NativeAbortSignal.abort();
33
- }
34
-
35
- /**
36
- * Returns an AbortSignal instance that will automatically abort after a specified time.
37
- *
38
- * @static
39
- * @param {number} time The time in milliseconds to wait before aborting
40
- * @returns {AbortSignal} The abort signal
41
- */
42
- static timeout(time) {
43
- return NativeAbortSignal.timeout(time);
44
- }
45
-
46
23
  /**
47
24
  * The aborted property is a Boolean that indicates whether the request has been aborted (true) or not (false).
48
25
  *
@@ -61,29 +38,22 @@ export default class AbortSignal extends EventTarget {
61
38
  return this.#abortController.signal.reason;
62
39
  }
63
40
 
64
- /**
65
- * throws the signal's abort reason if the signal has been aborted; otherwise it does nothing.
66
- *
67
- * @returns {void}
68
- */
69
- throwIfAborted() {
70
- this.#abortController.signal.throwIfAborted();
71
- }
72
-
73
41
  /**
74
42
  * Returns an AbortSignal instance that will be aborted when the provided amount of milliseconds have passed.
75
- * A value of -1 (which is the default) means there is no timeout.
43
+ * A value of {@link Infinity} means there is no timeout.
76
44
  * Note: You can't set this property to a value less than 0.
77
45
  *
78
46
  * @param {number} timeout The timeout in milliseconds
79
47
  * @returns {AbortSignal} The abort signal
80
48
  */
81
- withTimeout(timeout) {
49
+ timeout(timeout) {
82
50
  if (timeout < 0) {
83
51
  throw new RangeError('The timeout cannot be negative');
84
52
  }
85
53
 
86
- this.#timeoutId ??= setTimeout(() => this.#abort(AbortSignal.#generateTimeoutEvent(timeout), true), timeout);
54
+ if (timeout != Infinity) {
55
+ this.#timeoutId ??= setTimeout(() => this.#abort(new CustomEvent(SignalEvents.TIMEOUT, { detail: { timeout, cause: new DOMException(`The request timed-out after ${timeout / 1000} seconds`, 'TimeoutError') } }), true), timeout);
56
+ }
87
57
 
88
58
  return this.#abortController.signal;
89
59
  }
@@ -99,38 +69,27 @@ export default class AbortSignal extends EventTarget {
99
69
  }
100
70
 
101
71
  /**
102
- * Adds an event listener for the specified event type.
72
+ * Adds an event listener for the 'abort' event.
103
73
  *
104
- * @override
105
- * @param {string} eventName The name of the event to listen to
106
74
  * @param {EventListener} listener The listener to add
107
- * @returns {void}
75
+ * @returns {AbortSignal} The AbortSignal
108
76
  */
109
- addEventListener(eventName, listener) {
110
- this.#abortController.signal.addEventListener(eventName, listener);
111
- }
77
+ onAbort(listener) {
78
+ this.#abortController.signal.addEventListener(SignalEvents.ABORT, listener);
112
79
 
113
- /**
114
- * Dispatches an event to this EventTarget.
115
- *
116
- * @override
117
- * @param {Event} event The event to dispatch
118
- * @returns {boolean} Whether the event was dispatched or not
119
- */
120
- dispatchEvent(event) {
121
- return this.#abortController.signal.dispatchEvent(event);
80
+ return this;
122
81
  }
123
82
 
124
83
  /**
125
- * Removes an event listener for the specified event type.
84
+ * Adds an event listener for the 'timeout' event.
126
85
  *
127
- * @override
128
- * @param {string} eventName The name of the event to listen to
129
- * @param {EventListener} listener The listener to remove
130
- * @returns {void}
86
+ * @param {EventListener} listener The listener to add
87
+ * @returns {AbortSignal} The AbortSignal
131
88
  */
132
- removeEventListener(eventName, listener) {
133
- this.#abortController.signal.removeEventListener(eventName, listener);
89
+ onTimeout(listener) {
90
+ this.#abortController.signal.addEventListener(SignalEvents.TIMEOUT, listener);
91
+
92
+ return this;
134
93
  }
135
94
 
136
95
  /**
@@ -160,16 +119,4 @@ export default class AbortSignal extends EventTarget {
160
119
  this.#abortController.signal.dispatchEvent(event);
161
120
  }
162
121
  }
163
-
164
- /**
165
- * Generates a timeout event.
166
- *
167
- * @private
168
- * @static
169
- * @param {number} timeout The timeout in milliseconds
170
- * @returns {CustomEvent} The timeout event
171
- */
172
- static #generateTimeoutEvent(timeout) {
173
- return new CustomEvent(SignalEvents.TIMEOUT, { detail: { timeout, cause: new DOMException(`The request timed-out after ${timeout / 1000} seconds`, 'TimeoutError') } });
174
- }
175
122
  }
package/src/constants.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import HttpRequestMethod from './http-request-methods.js';
2
+ import ResponseStatus from './response-status.js';
2
3
  import { MediaType } from '@d1g1tal/media-type';
3
4
  import HttpMediaType from './http-media-type.js';
4
5
 
@@ -47,4 +48,14 @@ const _abortEvent = new CustomEvent(SignalEvents.ABORT, { detail: { cause: new D
47
48
 
48
49
  const requestBodyMethods = [ HttpRequestMethod.POST, HttpRequestMethod.PUT, HttpRequestMethod.PATCH ];
49
50
 
50
- export { defaultCharset, endsWithSlashRegEx, mediaTypes, RequestEvents, SignalEvents, _abortEvent as abortEvent, requestBodyMethods };
51
+ const internalServerError = new ResponseStatus(500, 'Internal Server Error');
52
+
53
+ const eventResponseStatuses = Object.freeze({
54
+ [RequestEvents.ABORTED]: new ResponseStatus(499, 'Aborted'),
55
+ [RequestEvents.TIMEOUT]: new ResponseStatus(504, 'Request Timeout')
56
+ });
57
+
58
+ /** @type {ProxyHandler<import('./transportr.js').RequestOptions>} */
59
+ const abortSignalProxyHandler = { get: (target, property) => property == 'signal' ? target.signal.timeout(target.timeout) : Reflect.get(target, property) };
60
+
61
+ export { defaultCharset, endsWithSlashRegEx, mediaTypes, RequestEvents, SignalEvents, _abortEvent as abortEvent, requestBodyMethods, internalServerError, eventResponseStatuses, abortSignalProxyHandler };
package/src/transportr.js CHANGED
@@ -1,16 +1,15 @@
1
- import SetMultiMap from '@d1g1tal/collections/set-multi-map.js';
2
1
  import Subscribr from '@d1g1tal/subscribr';
2
+ import AbortSignal from './abort-signal.js';
3
3
  import HttpError from './http-error.js';
4
4
  import HttpMediaType from './http-media-type.js';
5
5
  import HttpRequestHeader from './http-request-headers.js';
6
6
  import HttpRequestMethod from './http-request-methods.js';
7
7
  import HttpResponseHeader from './http-response-headers.js';
8
- import ResponseStatus from './response-status.js';
9
8
  import ParameterMap from './parameter-map.js';
10
- import AbortSignal from './abort-signal.js';
9
+ import ResponseStatus from './response-status.js';
11
10
  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';
11
+ import { _objectMerge, _type } from '@d1g1tal/chrysalis';
12
+ import { RequestEvents, abortEvent, abortSignalProxyHandler, endsWithSlashRegEx, eventResponseStatuses, internalServerError, mediaTypes, requestBodyMethods } from './constants.js';
14
13
 
15
14
  /**
16
15
  * @template T extends ResponseBody
@@ -132,8 +131,8 @@ export default class Transportr {
132
131
  static #globalSubscribr = new Subscribr();
133
132
  /** @type {Set<AbortSignal>} */
134
133
  static #activeRequests = new Set();
135
- /** @type {SetMultiMap<ResponseHandler<ResponseBody>, string>} */
136
- static #contentTypeHandlers = new SetMultiMap([
134
+ /** @type {Map<ResponseHandler<ResponseBody>, string>} */
135
+ static #contentTypeHandlers = new Map([
137
136
  [_handleImage, mediaTypes.get(HttpMediaType.PNG).type],
138
137
  [_handleText, mediaTypes.get(HttpMediaType.TEXT).type],
139
138
  [_handleJson, mediaTypes.get(HttpMediaType.JSON).subtype],
@@ -272,26 +271,6 @@ export default class Transportr {
272
271
  window: null
273
272
  });
274
273
 
275
- /**
276
- * @private
277
- * @static
278
- * @type {Map<TransportrEvent, ResponseStatus>}
279
- */
280
- static #eventResponseStatuses = new Map([
281
- [RequestEvents.ABORTED, new ResponseStatus(499, 'Aborted')],
282
- [RequestEvents.TIMEOUT, new ResponseStatus(504, 'Gateway Timeout')]
283
- ]);
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
274
  /**
296
275
  * Returns a {@link EventRegistration} used for subscribing to global events.
297
276
  *
@@ -597,12 +576,12 @@ export default class Transportr {
597
576
  * @async
598
577
  * @param {string} [path] The path to the endpoint you want to call.
599
578
  * @param {RequestOptions} [userOptions] The options passed to the public function to use for the request.
600
- * @param {RequestOptions} [options] The options for the request.
579
+ * @param {RequestOptions} [options={}] The options for the request.
601
580
  * @param {ResponseHandler<ResponseBody>} [responseHandler] A function that will be called with the response object.
602
581
  * @returns {Promise<ResponseBody>} The result of the #request method.
603
582
  */
604
- async #get(path, userOptions, options, responseHandler) {
605
- return this.#request(path, userOptions, { ...options, method: HttpRequestMethod.GET }, responseHandler);
583
+ async #get(path, userOptions, options = {}, responseHandler) {
584
+ return this.#request(path, userOptions, Object.assign(options, { method: HttpRequestMethod.GET }), responseHandler);
606
585
  }
607
586
 
608
587
  /**
@@ -621,38 +600,30 @@ export default class Transportr {
621
600
  async #request(path, userOptions = {}, options = {}, responseHandler) {
622
601
  if (_type(path) == Object) { [ path, userOptions ] = [ undefined, path ] }
623
602
 
624
- try {
625
- options = this.#processRequestOptions(userOptions, options);
626
- } catch (cause) {
627
- return Promise.reject(new HttpError('Unable to process request options', { cause }));
628
- }
629
-
630
- this.#publish({ name: RequestEvents.CONFIGURED, data: options, global: options.global });
603
+ options = this.#processRequestOptions(userOptions, options);
631
604
 
605
+ let response;
632
606
  const url = Transportr.#createUrl(this.#baseUrl, path, options.searchParams);
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 }));
636
-
637
- Transportr.#activeRequests.add(options.signal);
638
-
639
- let response, result;
640
607
  try {
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) }));
608
+ Transportr.#activeRequests.add(options.signal);
609
+ // Proxy the options and trap for the `signal` to be accessed to start the timeout timer
610
+ response = await fetch(url, new Proxy(options, abortSignalProxyHandler));
643
611
 
644
- if (!responseHandler && response.status != 204 && response.headers.has(HttpResponseHeader.CONTENT_TYPE)) {
612
+ if (!responseHandler && response.status != 204) {
645
613
  responseHandler = this.#getResponseHandler(response.headers.get(HttpResponseHeader.CONTENT_TYPE));
646
614
  }
647
615
 
648
- result = await responseHandler?.(response) ?? response;
616
+ const result = await responseHandler?.(response) ?? response;
649
617
 
650
618
  if (!response.ok) {
651
619
  return Promise.reject(this.#handleError(url, { status: Transportr.#generateResponseStatusFromError('ResponseError', response), entity: result }));
652
620
  }
621
+
653
622
  this.#publish({ name: RequestEvents.SUCCESS, data: result, global: options.global });
654
- } catch (error) {
655
- return Promise.reject(this.#handleError(url, { cause: error, status: Transportr.#generateResponseStatusFromError(error.name, response) }));
623
+
624
+ return result;
625
+ } catch (cause) {
626
+ return Promise.reject(this.#handleError(url, { cause, status: Transportr.#generateResponseStatusFromError(cause.name, response) }));
656
627
  } finally {
657
628
  options.signal.clearTimeout();
658
629
  if (!options.signal.aborted) {
@@ -665,8 +636,6 @@ export default class Transportr {
665
636
  }
666
637
  }
667
638
  }
668
-
669
- return result;
670
639
  }
671
640
 
672
641
  /**
@@ -742,23 +711,13 @@ export default class Transportr {
742
711
  requestOptions.body = undefined;
743
712
  }
744
713
 
745
- return requestOptions;
746
- }
714
+ requestOptions.signal = new AbortSignal(requestOptions.signal)
715
+ .onAbort((event) => this.#publish({ name: RequestEvents.ABORTED, event, global: requestOptions.global }))
716
+ .onTimeout((event) => this.#publish({ name: RequestEvents.TIMEOUT, event, global: requestOptions.global }));
747
717
 
748
- /**
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.
751
- *
752
- * @private
753
- * @static
754
- * @param {number} timeout The timeout in milliseconds before the signal is aborted.
755
- * @returns {Proxy<RequestOptions>} A proxy for the options object.
756
- */
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
- };
718
+ this.#publish({ name: RequestEvents.CONFIGURED, data: requestOptions, global: requestOptions.global });
719
+
720
+ return requestOptions;
762
721
  }
763
722
 
764
723
  /**
@@ -809,9 +768,9 @@ export default class Transportr {
809
768
  */
810
769
  static #generateResponseStatusFromError(errorName, response) {
811
770
  switch (errorName) {
812
- case 'AbortError': return Transportr.#eventResponseStatuses.get(RequestEvents.ABORTED);
813
- case 'TimeoutError': return Transportr.#eventResponseStatuses.get(RequestEvents.TIMEOUT);
814
- default: return response ? new ResponseStatus(response.status, response.statusText) : new ResponseStatus(500, 'Internal Server Error');
771
+ case 'AbortError': return eventResponseStatuses[RequestEvents.ABORTED];
772
+ case 'TimeoutError': return eventResponseStatuses[RequestEvents.TIMEOUT];
773
+ default: return response ? new ResponseStatus(response.status, response.statusText) : internalServerError;
815
774
  }
816
775
  }
817
776