@d1g1tal/transportr 1.3.2 → 1.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@d1g1tal/transportr",
3
- "version": "1.3.2",
3
+ "version": "1.4.0",
4
4
  "description": "JavaScript wrapper for the Fetch API",
5
5
  "type": "module",
6
6
  "exports": {
@@ -45,11 +45,11 @@
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.4"
53
53
  },
54
54
  "dependencies": {
55
55
  "@d1g1tal/chrysalis": "^1.2.3",
@@ -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,16 @@
1
1
  import SetMultiMap from '@d1g1tal/collections/set-multi-map.js';
2
2
  import Subscribr from '@d1g1tal/subscribr';
3
+ import AbortSignal from './abort-signal.js';
3
4
  import HttpError from './http-error.js';
4
5
  import HttpMediaType from './http-media-type.js';
5
6
  import HttpRequestHeader from './http-request-headers.js';
6
7
  import HttpRequestMethod from './http-request-methods.js';
7
8
  import HttpResponseHeader from './http-response-headers.js';
8
- import ResponseStatus from './response-status.js';
9
9
  import ParameterMap from './parameter-map.js';
10
- import AbortSignal from './abort-signal.js';
10
+ import ResponseStatus from './response-status.js';
11
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
+ import { _objectMerge, _type } from '@d1g1tal/chrysalis';
13
+ import { RequestEvents, abortEvent, abortSignalProxyHandler, endsWithSlashRegEx, eventResponseStatuses, internalServerError, mediaTypes, requestBodyMethods } from './constants.js';
14
14
 
15
15
  /**
16
16
  * @template T extends ResponseBody
@@ -272,26 +272,6 @@ export default class Transportr {
272
272
  window: null
273
273
  });
274
274
 
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
275
  /**
296
276
  * Returns a {@link EventRegistration} used for subscribing to global events.
297
277
  *
@@ -597,12 +577,12 @@ export default class Transportr {
597
577
  * @async
598
578
  * @param {string} [path] The path to the endpoint you want to call.
599
579
  * @param {RequestOptions} [userOptions] The options passed to the public function to use for the request.
600
- * @param {RequestOptions} [options] The options for the request.
580
+ * @param {RequestOptions} [options={}] The options for the request.
601
581
  * @param {ResponseHandler<ResponseBody>} [responseHandler] A function that will be called with the response object.
602
582
  * @returns {Promise<ResponseBody>} The result of the #request method.
603
583
  */
604
- async #get(path, userOptions, options, responseHandler) {
605
- return this.#request(path, userOptions, { ...options, method: HttpRequestMethod.GET }, responseHandler);
584
+ async #get(path, userOptions, options = {}, responseHandler) {
585
+ return this.#request(path, userOptions, Object.assign(options, { method: HttpRequestMethod.GET }), responseHandler);
606
586
  }
607
587
 
608
588
  /**
@@ -621,38 +601,30 @@ export default class Transportr {
621
601
  async #request(path, userOptions = {}, options = {}, responseHandler) {
622
602
  if (_type(path) == Object) { [ path, userOptions ] = [ undefined, path ] }
623
603
 
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 });
604
+ options = this.#processRequestOptions(userOptions, options);
631
605
 
606
+ let response;
632
607
  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
608
  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) }));
609
+ Transportr.#activeRequests.add(options.signal);
610
+ // Proxy the options and trap for the `signal` to be accessed to start the timeout timer
611
+ response = await fetch(url, new Proxy(options, abortSignalProxyHandler));
643
612
 
644
- if (!responseHandler && response.status != 204 && response.headers.has(HttpResponseHeader.CONTENT_TYPE)) {
613
+ if (!responseHandler && response.status != 204) {
645
614
  responseHandler = this.#getResponseHandler(response.headers.get(HttpResponseHeader.CONTENT_TYPE));
646
615
  }
647
616
 
648
- result = await responseHandler?.(response) ?? response;
617
+ const result = await responseHandler?.(response) ?? response;
649
618
 
650
619
  if (!response.ok) {
651
620
  return Promise.reject(this.#handleError(url, { status: Transportr.#generateResponseStatusFromError('ResponseError', response), entity: result }));
652
621
  }
622
+
653
623
  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) }));
624
+
625
+ return result;
626
+ } catch (cause) {
627
+ return Promise.reject(this.#handleError(url, { cause, status: Transportr.#generateResponseStatusFromError(cause.name, response) }));
656
628
  } finally {
657
629
  options.signal.clearTimeout();
658
630
  if (!options.signal.aborted) {
@@ -665,8 +637,6 @@ export default class Transportr {
665
637
  }
666
638
  }
667
639
  }
668
-
669
- return result;
670
640
  }
671
641
 
672
642
  /**
@@ -742,23 +712,13 @@ export default class Transportr {
742
712
  requestOptions.body = undefined;
743
713
  }
744
714
 
745
- return requestOptions;
746
- }
715
+ requestOptions.signal = new AbortSignal(requestOptions.signal)
716
+ .onAbort((event) => this.#publish({ name: RequestEvents.ABORTED, event, global: requestOptions.global }))
717
+ .onTimeout((event) => this.#publish({ name: RequestEvents.TIMEOUT, event, global: requestOptions.global }));
747
718
 
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
- };
719
+ this.#publish({ name: RequestEvents.CONFIGURED, data: requestOptions, global: requestOptions.global });
720
+
721
+ return requestOptions;
762
722
  }
763
723
 
764
724
  /**
@@ -809,9 +769,9 @@ export default class Transportr {
809
769
  */
810
770
  static #generateResponseStatusFromError(errorName, response) {
811
771
  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');
772
+ case 'AbortError': return eventResponseStatuses[RequestEvents.ABORTED];
773
+ case 'TimeoutError': return eventResponseStatuses[RequestEvents.TIMEOUT];
774
+ default: return response ? new ResponseStatus(response.status, response.statusText) : internalServerError;
815
775
  }
816
776
  }
817
777