@selkirk-systems/fetch 1.0.0 → 1.0.3
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/README.md +17 -0
- package/dist/Download.js +4 -8
- package/dist/FetchWrapper.js +389 -0
- package/dist/constants/ErrorConstants.js +1 -0
- package/dist/index.js +3 -0
- package/dist/middleware/FetchErrorHandler.js +37 -0
- package/dist/middleware/FetchLogger.js +29 -0
- package/lib/Download.js +175 -0
- package/lib/FetchWrapper.js +515 -0
- package/lib/index.js +3 -0
- package/{middleware → lib/middleware}/FetchErrorHandler.js +1 -1
- package/package.json +40 -19
- package/.babelrc +0 -6
- package/dist/Fetch.js +0 -463
- package/index.js +0 -2
- package/middleware/FetchValidationHandler.js +0 -22
- package/tests/Fetch.test.js +0 -114
- package/tests/files/test.pdf +0 -0
- package/tests/mocks/handlers.js +0 -75
- package/tests/mocks/server.js +0 -7
- /package/{constants → lib/constants}/ErrorConstants.js +0 -0
- /package/{middleware → lib/middleware}/FetchLogger.js +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# `Fetch`
|
|
2
|
+
|
|
3
|
+
Abortable Fetch Library
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
import { Fetch } from "@selkirk-systems/fetch";
|
|
9
|
+
|
|
10
|
+
export const fetchAgreementsByFiscalYear = ( year ) => {
|
|
11
|
+
|
|
12
|
+
dispatch( FETCH_AGREEMENTS );
|
|
13
|
+
|
|
14
|
+
return Fetch( AGREEMENTS_LOOKUP_BY_FISCAL_YEAR_URL( { fiscalYear: year } ) ).then( respondWith( FETCHED_AGREEMENTS ) );
|
|
15
|
+
|
|
16
|
+
}
|
|
17
|
+
```
|
package/dist/Download.js
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
|
|
4
|
-
/* eslint-disable no-undef */
|
|
5
|
-
/* eslint-disable no-restricted-globals */
|
|
1
|
+
/* eslint-disable */
|
|
6
2
|
|
|
7
3
|
//download.js v4.21, by dandavis; 2008-2018. [MIT] see http://danml.com/download.html for tests/usage
|
|
8
4
|
// v1 landed a FF+Chrome compatible way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
|
|
@@ -17,7 +13,7 @@ function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" =
|
|
|
17
13
|
if (typeof define === 'function' && define.amd) {
|
|
18
14
|
// AMD. Register as an anonymous module.
|
|
19
15
|
define([], factory);
|
|
20
|
-
} else if (
|
|
16
|
+
} else if (typeof exports === 'object') {
|
|
21
17
|
// Node. Does not work with strict CommonJS, but
|
|
22
18
|
// only CommonJS-like environments that support module.exports,
|
|
23
19
|
// like Node.
|
|
@@ -26,7 +22,7 @@ function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" =
|
|
|
26
22
|
// Browser globals (root is window)
|
|
27
23
|
root.download = factory();
|
|
28
24
|
}
|
|
29
|
-
})(
|
|
25
|
+
})(this, function () {
|
|
30
26
|
return function download(data, strFileName, strMimeType) {
|
|
31
27
|
var self = window,
|
|
32
28
|
// this script is only for browsers anyway...
|
|
@@ -36,7 +32,7 @@ function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" =
|
|
|
36
32
|
payload = data,
|
|
37
33
|
url = !strFileName && !strMimeType && payload,
|
|
38
34
|
anchor = document.createElement("a"),
|
|
39
|
-
toString = function
|
|
35
|
+
toString = function (a) {
|
|
40
36
|
return String(a);
|
|
41
37
|
},
|
|
42
38
|
myBlob = self.Blob || self.MozBlob || self.WebKitBlob || toString,
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
//Inspired by https://www.bennadel.com/blog/4180-canceling-api-requests-using-fetch-and-abortcontroller-in-javascript.htm
|
|
2
|
+
|
|
3
|
+
import Download from './Download';
|
|
4
|
+
import FetchErrorHandler from './middleware/FetchErrorHandler';
|
|
5
|
+
|
|
6
|
+
// Regular expression patterns for testing content-type response headers.
|
|
7
|
+
const RE_CONTENT_TYPE_JSON = /^application\/(x-)?json/i;
|
|
8
|
+
const RE_CONTENT_TYPE_TEXT = /"^text\/"/i;
|
|
9
|
+
const RE_QUERY_STRING = /\/.+\?/;
|
|
10
|
+
const UNEXPECTED_ERROR_MESSAGE = "An unexpected error occurred while processing your request.";
|
|
11
|
+
const CONTENT_TYPE_DOWNLOADS = {
|
|
12
|
+
'application/pdf': true,
|
|
13
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': true
|
|
14
|
+
};
|
|
15
|
+
//We store the original promise.catch so we can override it in some
|
|
16
|
+
//scenarios when we want to swallow errors vs bubble them up.
|
|
17
|
+
const ORIGINAL_CATCH_FN = Promise.prototype.catch;
|
|
18
|
+
|
|
19
|
+
//Auto applied middleware that dispatches all errors so any UI's can respond.
|
|
20
|
+
let _middlewares = [FetchErrorHandler];
|
|
21
|
+
|
|
22
|
+
//Cache of request AbortController signals for auto request aborting.
|
|
23
|
+
let _requests = {};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* PUBLIC: Apply custom middleware to act on any fetch responses and errors.
|
|
27
|
+
*
|
|
28
|
+
* NOTE: Middleware can handle errors and swallow them by passing back a new error object.
|
|
29
|
+
*
|
|
30
|
+
* @param {array} middleware
|
|
31
|
+
*/
|
|
32
|
+
export function applyMiddleware(middleware = []) {
|
|
33
|
+
_middlewares = middleware;
|
|
34
|
+
_middlewares.push(FetchErrorHandler);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Make the fetch request with the given configuration options.
|
|
39
|
+
*
|
|
40
|
+
* GUARANTEE: All errors produced by this method will have consistent structure, even
|
|
41
|
+
* if they are low-level networking errors. At a minimum, every Promise rejection will
|
|
42
|
+
* have the following properties:
|
|
43
|
+
*
|
|
44
|
+
* TODO: Add support for multi-form uploads (images, files etc)
|
|
45
|
+
*
|
|
46
|
+
* - data.type
|
|
47
|
+
* - data.message
|
|
48
|
+
* - status.code
|
|
49
|
+
* - status.text
|
|
50
|
+
* - status.isAbort
|
|
51
|
+
*/
|
|
52
|
+
export default function Fetch(url, options = {}) {
|
|
53
|
+
const config = {
|
|
54
|
+
downloadFileName: null,
|
|
55
|
+
contentType: "application/json",
|
|
56
|
+
headers: {
|
|
57
|
+
accept: "*/*"
|
|
58
|
+
},
|
|
59
|
+
credentials: "same-origin",
|
|
60
|
+
url: url || "",
|
|
61
|
+
urlTemplateData: {},
|
|
62
|
+
method: "GET",
|
|
63
|
+
params: {},
|
|
64
|
+
form: null,
|
|
65
|
+
json: null,
|
|
66
|
+
body: null,
|
|
67
|
+
signal: new AbortController(),
|
|
68
|
+
...options,
|
|
69
|
+
_hasCatch: false,
|
|
70
|
+
_promiseChain: null,
|
|
71
|
+
_userSignal: Boolean(options.signal)
|
|
72
|
+
};
|
|
73
|
+
let finalHeaders, finalMethod, finalUrl, finalBody, finalSignal, request;
|
|
74
|
+
try {
|
|
75
|
+
finalHeaders = buildHeaders(config.headers);
|
|
76
|
+
finalMethod = config.method;
|
|
77
|
+
finalUrl = buildURL(config.url, config.urlTemplateData, config.params);
|
|
78
|
+
finalBody = config.body;
|
|
79
|
+
finalSignal = config.signal;
|
|
80
|
+
|
|
81
|
+
// Bail out early if url contains url params,
|
|
82
|
+
//these should be set via the options.params object.
|
|
83
|
+
if (!config.url.href && RE_QUERY_STRING.test(config.url)) {
|
|
84
|
+
return Promise.reject(normalizeError({
|
|
85
|
+
type: "INVALID_URL",
|
|
86
|
+
message: `${config.url} contains query parameters: please use options.params object for this purpose.`
|
|
87
|
+
}, {}, {}, config));
|
|
88
|
+
}
|
|
89
|
+
if (CONTENT_TYPE_DOWNLOADS[config.contentType] && finalMethod === "GET") {
|
|
90
|
+
finalHeaders.credentials = "same-origin";
|
|
91
|
+
finalHeaders.accept = "*/*";
|
|
92
|
+
}
|
|
93
|
+
if (config.form) {
|
|
94
|
+
// For form data posts, we want the browser to build the Content-
|
|
95
|
+
// Type for us so that it puts in both the "multipart/form-data" plus the
|
|
96
|
+
// correct, auto-generated field delimiter.
|
|
97
|
+
delete finalHeaders["content-type"];
|
|
98
|
+
finalMethod = "POST";
|
|
99
|
+
finalBody = buildFormData(config.form);
|
|
100
|
+
} else if (config.json) {
|
|
101
|
+
finalHeaders["content-type"] = config.contentType || "application/x-json";
|
|
102
|
+
finalBody = JSON.stringify(config.json);
|
|
103
|
+
} else if (config.body) {
|
|
104
|
+
finalHeaders["content-type"] = config.contentType || "application/octet-stream";
|
|
105
|
+
} else {
|
|
106
|
+
finalHeaders["content-type"] = config.contentType;
|
|
107
|
+
}
|
|
108
|
+
request = new window.Request(finalUrl, {
|
|
109
|
+
headers: finalHeaders,
|
|
110
|
+
method: finalMethod,
|
|
111
|
+
body: finalBody,
|
|
112
|
+
signal: finalSignal.signal
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
//Check if a pending request is in-flight, if so and it is the exact same url abort it.
|
|
116
|
+
if (_requests[finalUrl]) {
|
|
117
|
+
_requests[finalUrl].abort();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//Cache requests abort signal by url
|
|
121
|
+
cacheRequestSignal(finalUrl, finalSignal);
|
|
122
|
+
config._promiseChain = Promise.resolve(window.fetch(request)).then(async response => {
|
|
123
|
+
deleteCachedRequestSignal(finalUrl);
|
|
124
|
+
const data = await unwrapResponseData(response);
|
|
125
|
+
if (response.ok) {
|
|
126
|
+
//Run response through middleware
|
|
127
|
+
const nextResp = _applyMiddleware(null, response, config);
|
|
128
|
+
if (config.downloadFileName) {
|
|
129
|
+
presetBrowserDownloadDialog(data, config);
|
|
130
|
+
}
|
|
131
|
+
return [{
|
|
132
|
+
request: request,
|
|
133
|
+
response: nextResp || response,
|
|
134
|
+
data: data
|
|
135
|
+
}, false];
|
|
136
|
+
}
|
|
137
|
+
return handleError(normalizeError(data, request, response, config), config);
|
|
138
|
+
}).catch(err => {
|
|
139
|
+
deleteCachedRequestSignal(finalUrl);
|
|
140
|
+
const error = isNormalizedError(err) ? err : normalizeTransportError(err);
|
|
141
|
+
return handleError(error, config);
|
|
142
|
+
});
|
|
143
|
+
} catch (err) {
|
|
144
|
+
deleteCachedRequestSignal(finalUrl);
|
|
145
|
+
return handleError(normalizeTransportError(err), config);
|
|
146
|
+
}
|
|
147
|
+
if (config._promiseChain) {
|
|
148
|
+
//If catch is added outside, then assume they want to handle errors and
|
|
149
|
+
//not have them swallowed
|
|
150
|
+
|
|
151
|
+
config._promiseChain.catch = function (...args) {
|
|
152
|
+
config._hasCatch = true;
|
|
153
|
+
return ORIGINAL_CATCH_FN.apply(this, args);
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return config._promiseChain;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Shows the browser download dialog
|
|
161
|
+
* @param {response.blob} blob
|
|
162
|
+
* @param {object} config
|
|
163
|
+
* @returns
|
|
164
|
+
*/
|
|
165
|
+
function presetBrowserDownloadDialog(blob, config) {
|
|
166
|
+
return Download(blob, config.downloadFileName, config.contentType);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Stores a ref to the abort signal
|
|
171
|
+
* @param {string} url
|
|
172
|
+
* @param {AbortController.signal} signal
|
|
173
|
+
*/
|
|
174
|
+
function cacheRequestSignal(url, signal) {
|
|
175
|
+
_requests[url] = signal;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Deletes a req from the cached requests
|
|
180
|
+
* @param {string} url
|
|
181
|
+
*/
|
|
182
|
+
function deleteCachedRequestSignal(url) {
|
|
183
|
+
delete _requests[url];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Checks if the error structure mimics ours and has already been normalized.
|
|
188
|
+
* @param {Object} err
|
|
189
|
+
* @returns
|
|
190
|
+
*/
|
|
191
|
+
function isNormalizedError(err) {
|
|
192
|
+
return err.hasOwnProperty("status") && err.hasOwnProperty("data");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Handles errors by passing them through any middleware and finally throwing them or swallowing them.
|
|
197
|
+
* @param {object} normalizedError
|
|
198
|
+
* @param {object} config
|
|
199
|
+
* @returns
|
|
200
|
+
*/
|
|
201
|
+
function handleError(normalizedError, config) {
|
|
202
|
+
const hasCatch = config._hasCatch;
|
|
203
|
+
const promiseChain = config._promiseChain;
|
|
204
|
+
const nextErr = _applyMiddleware(normalizedError, null, config);
|
|
205
|
+
|
|
206
|
+
//Only swallow errors if they have been handled by middleware AND they have not
|
|
207
|
+
//Added a catch outside
|
|
208
|
+
if (!hasCatch && !nextErr) {
|
|
209
|
+
if (promiseChain && promiseChain.cancel) promiseChain.cancel();
|
|
210
|
+
//In the case of aborted requests etc, we treat them as success and let the initial fetch chain
|
|
211
|
+
//handle them.
|
|
212
|
+
return Promise.resolve([normalizedError, true]);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// The request failed in a critical way; the content of this error will be
|
|
216
|
+
// entirely unpredictable.
|
|
217
|
+
return Promise.reject(nextErr || normalizedError);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Passes any errors, responses through configured middleware
|
|
222
|
+
*
|
|
223
|
+
* @param {ErrorEvent} err the error.
|
|
224
|
+
* @param {Object} response the network response.
|
|
225
|
+
* @param {Object} options the request options.
|
|
226
|
+
* @returns null
|
|
227
|
+
*/
|
|
228
|
+
function _applyMiddleware(err, response, options) {
|
|
229
|
+
let i = -1;
|
|
230
|
+
const next = function (nextErr, nextState) {
|
|
231
|
+
i++;
|
|
232
|
+
const middleware = _middlewares[i];
|
|
233
|
+
if (!middleware) return nextErr || nextState;
|
|
234
|
+
return middleware(nextErr)(nextState)(options)(next);
|
|
235
|
+
};
|
|
236
|
+
return next(err, response);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Unwrap the response payload from the given response based on the reported
|
|
241
|
+
* content-type.
|
|
242
|
+
*/
|
|
243
|
+
async function unwrapResponseData(response) {
|
|
244
|
+
var contentType = response.headers.has("content-type") ? response.headers.get("content-type") : "";
|
|
245
|
+
if (RE_CONTENT_TYPE_JSON.test(contentType)) {
|
|
246
|
+
return response.json();
|
|
247
|
+
} else if (RE_CONTENT_TYPE_TEXT.test(contentType)) {
|
|
248
|
+
return response.text();
|
|
249
|
+
} else {
|
|
250
|
+
return response.blob();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* FormData instance from the given object.
|
|
256
|
+
*
|
|
257
|
+
* NOTE: At this time, only simple values (ie, no files) are supported.
|
|
258
|
+
*/
|
|
259
|
+
function buildFormData(form) {
|
|
260
|
+
var formData = new FormData();
|
|
261
|
+
Object.entries(form).forEach(([key, value]) => {
|
|
262
|
+
formData.append(key, value);
|
|
263
|
+
});
|
|
264
|
+
return formData;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Supports url or url with template identifiers,creates a url with optional query parameters.
|
|
269
|
+
* @param {string} url
|
|
270
|
+
* @param {object} templateData
|
|
271
|
+
* @param {object} params
|
|
272
|
+
* @returns
|
|
273
|
+
*/
|
|
274
|
+
function buildURL(url, templateData, params) {
|
|
275
|
+
if (url.href) return url;
|
|
276
|
+
const formattedUrl = buildURLTemplate(url, templateData);
|
|
277
|
+
const finalUrl = new URL(formattedUrl, `${window.location.origin}${window.baseUrl || ""}`);
|
|
278
|
+
const searchParams = new URLSearchParams();
|
|
279
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
for (let i = 0; i < value.length; i++) {
|
|
282
|
+
const e = value[i];
|
|
283
|
+
searchParams.append(key, e);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
searchParams.append(key, value);
|
|
288
|
+
});
|
|
289
|
+
finalUrl.search = searchParams;
|
|
290
|
+
return finalUrl;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Builds a urls tring using template identifiers.
|
|
295
|
+
* @param {string} url
|
|
296
|
+
* @param {object} templateData
|
|
297
|
+
*/
|
|
298
|
+
function buildURLTemplate(url, templateData) {
|
|
299
|
+
Object.entries(templateData).forEach(([key, value]) => {
|
|
300
|
+
url = url.replace(new RegExp(`{${key}}`, "ig"), value.toString());
|
|
301
|
+
});
|
|
302
|
+
return url;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Transform the collection of HTTP headers into a like collection wherein the names
|
|
307
|
+
* of the headers have been lower-cased. This way, if we need to manipulate the
|
|
308
|
+
* collection prior to transport, we'll know what key-casing to use.
|
|
309
|
+
*/
|
|
310
|
+
function buildHeaders(headers) {
|
|
311
|
+
var lowercaseHeaders = {};
|
|
312
|
+
Object.entries(headers).forEach(([key, value]) => {
|
|
313
|
+
lowercaseHeaders[key.toLowerCase()] = value;
|
|
314
|
+
});
|
|
315
|
+
return lowercaseHeaders;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* At a minimum, we want every error to have the following properties:
|
|
320
|
+
*
|
|
321
|
+
* - data.type
|
|
322
|
+
* - data.message
|
|
323
|
+
* - status.code
|
|
324
|
+
* - status.text
|
|
325
|
+
* - status.isAbort
|
|
326
|
+
*
|
|
327
|
+
* These are the keys that the calling context will depend on; and, are the minimum
|
|
328
|
+
* keys that the server is expected to return when it throws domain errors.
|
|
329
|
+
*/
|
|
330
|
+
function normalizeError(data, request, response, config) {
|
|
331
|
+
var error = {
|
|
332
|
+
data: {
|
|
333
|
+
type: "ServerError",
|
|
334
|
+
message: UNEXPECTED_ERROR_MESSAGE
|
|
335
|
+
},
|
|
336
|
+
status: {
|
|
337
|
+
code: response.status,
|
|
338
|
+
text: response.statusText,
|
|
339
|
+
isAbort: false
|
|
340
|
+
},
|
|
341
|
+
// The following data is being provided to make debugging AJAX errors easier.
|
|
342
|
+
request: request,
|
|
343
|
+
response: response,
|
|
344
|
+
config: config || {}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
// If the error data is an Object (which it should be if the server responded
|
|
348
|
+
// with a domain-based error), then it should have "type" and "message"
|
|
349
|
+
// properties within it. That said, just because this isn't a transport error, it
|
|
350
|
+
// doesn't mean that this error is actually being returned by our application.
|
|
351
|
+
if (typeof data?.type === "string" && typeof data?.message === "string") {
|
|
352
|
+
Object.assign(error.data, data);
|
|
353
|
+
|
|
354
|
+
// If the error data has any other shape, it means that an unexpected error
|
|
355
|
+
// occurred on the server (or somewhere in transit). Let's pass that raw error
|
|
356
|
+
// through as the rootCause, using the default error structure.
|
|
357
|
+
} else {
|
|
358
|
+
error.data.rootCause = data;
|
|
359
|
+
}
|
|
360
|
+
return error;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* If our request never makes it to the server (or the round-trip is interrupted
|
|
365
|
+
* somehow), we still want the error response to have a consistent structure with the
|
|
366
|
+
* application errors returned by the server. At a minimum, we want every error to
|
|
367
|
+
* have the following properties:
|
|
368
|
+
*
|
|
369
|
+
* - data.type
|
|
370
|
+
* - data.message
|
|
371
|
+
* - status.code
|
|
372
|
+
* - status.text
|
|
373
|
+
* - status.isAbort
|
|
374
|
+
*/
|
|
375
|
+
function normalizeTransportError(transportError) {
|
|
376
|
+
const isAbort = transportError.name === "AbortError";
|
|
377
|
+
return {
|
|
378
|
+
data: {
|
|
379
|
+
type: "TransportError",
|
|
380
|
+
message: isAbort ? "Network Request Aborted" : UNEXPECTED_ERROR_MESSAGE,
|
|
381
|
+
rootCause: transportError
|
|
382
|
+
},
|
|
383
|
+
status: {
|
|
384
|
+
code: 0,
|
|
385
|
+
text: "Unknown",
|
|
386
|
+
isAbort: isAbort
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const ADD_ERROR = "ADD_ERROR";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { dispatch } from "@selkirk-systems/state-management";
|
|
2
|
+
import { ADD_ERROR } from "../constants/ErrorConstants";
|
|
3
|
+
const handler = err => response => options => next => {
|
|
4
|
+
if (err) {
|
|
5
|
+
dispatch(ADD_ERROR, err);
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
return next(null, response);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// NOTE: event name is all lower case as per DOM convention
|
|
12
|
+
window.addEventListener("unhandledrejection", function (e) {
|
|
13
|
+
// NOTE: e.preventDefault() must be manually called to prevent the default
|
|
14
|
+
// action which is currently to log the stack trace to console.warn
|
|
15
|
+
e.preventDefault();
|
|
16
|
+
// NOTE: parameters are properties of the event detail property
|
|
17
|
+
var reason = e.reason || e.detail.reason;
|
|
18
|
+
//var promise = e.detail.promise;
|
|
19
|
+
// See Promise.onPossiblyUnhandledRejection for parameter documentation
|
|
20
|
+
console.groupEnd();
|
|
21
|
+
console.groupEnd();
|
|
22
|
+
console.groupEnd();
|
|
23
|
+
throw reason;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// NOTE: event name is all lower case as per DOM convention
|
|
27
|
+
window.addEventListener("rejectionhandled", function (e) {
|
|
28
|
+
// NOTE: e.preventDefault() must be manually called prevent the default
|
|
29
|
+
// action which is currently unset (but might be set to something in the future)
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
// NOTE: parameters are properties of the event detail property
|
|
32
|
+
var promise = e.reason || e.detail.promise;
|
|
33
|
+
// See Promise.onUnhandledRejectionHandled for parameter documentation
|
|
34
|
+
console.groupEnd();
|
|
35
|
+
console.log("REJECTION HANDLED", promise);
|
|
36
|
+
});
|
|
37
|
+
export default handler;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
function getStatusCode(err) {
|
|
2
|
+
return err && err.response && err.response.statusCode ? err.response.statusCode : 500;
|
|
3
|
+
}
|
|
4
|
+
const FetchLogger = err => response => options => next => {
|
|
5
|
+
//Only log outputs for 'development' environment
|
|
6
|
+
if (process.env.NODE_ENV !== "development") return next(err, response);
|
|
7
|
+
const headerCSS = ['color:gray;font-weight:lighter'];
|
|
8
|
+
const bodyCSS = 'color:gray;font-weight:lighter';
|
|
9
|
+
const successCSS = 'color: green; font-weight:bold';
|
|
10
|
+
const errorCSS = 'color:red;font-weight:bold';
|
|
11
|
+
const responseCSS = 'color:#87A7DB;font-weight:bold';
|
|
12
|
+
const paramsCSS = 'color:orange;font-weight:bold';
|
|
13
|
+
if (err) {
|
|
14
|
+
headerCSS.push(errorCSS);
|
|
15
|
+
headerCSS.push(bodyCSS);
|
|
16
|
+
console.groupCollapsed(`%cfetch %cFAIL (${getStatusCode(err)}) %c${options.url}`, ...headerCSS);
|
|
17
|
+
console.log("error", err.response);
|
|
18
|
+
} else {
|
|
19
|
+
headerCSS.push(successCSS);
|
|
20
|
+
headerCSS.push(bodyCSS);
|
|
21
|
+
console.groupCollapsed(`%cfetch %cSUCCESS %c${options.url}`, ...headerCSS);
|
|
22
|
+
console.log("%cerror", errorCSS, err);
|
|
23
|
+
}
|
|
24
|
+
console.log("%cresponse", responseCSS, response);
|
|
25
|
+
console.log("%cparams", paramsCSS, options);
|
|
26
|
+
console.groupEnd();
|
|
27
|
+
return next(err, response);
|
|
28
|
+
};
|
|
29
|
+
export default FetchLogger;
|
package/lib/Download.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
//download.js v4.21, by dandavis; 2008-2018. [MIT] see http://danml.com/download.html for tests/usage
|
|
4
|
+
// v1 landed a FF+Chrome compatible way of downloading strings to local un-named files, upgraded to use a hidden frame and optional mime
|
|
5
|
+
// v2 added named files via a[download], msSaveBlob, IE (10+) support, and window.URL support for larger+faster saves than dataURLs
|
|
6
|
+
// v3 added dataURL and Blob Input, bind-toggle arity, and legacy dataURL fallback was improved with force-download mime and base64 support. 3.1 improved safari handling.
|
|
7
|
+
// v4 adds AMD/UMD, commonJS, and plain browser support
|
|
8
|
+
// v4.1 adds url download capability via solo URL argument (same domain/CORS only)
|
|
9
|
+
// v4.2 adds semantic variable names, long (over 2MB) dataURL support, and hidden by default temp anchors
|
|
10
|
+
// https://github.com/rndme/download
|
|
11
|
+
|
|
12
|
+
( function ( root, factory ) {
|
|
13
|
+
if ( typeof define === 'function' && define.amd ) {
|
|
14
|
+
// AMD. Register as an anonymous module.
|
|
15
|
+
define( [], factory );
|
|
16
|
+
} else if ( typeof exports === 'object' ) {
|
|
17
|
+
// Node. Does not work with strict CommonJS, but
|
|
18
|
+
// only CommonJS-like environments that support module.exports,
|
|
19
|
+
// like Node.
|
|
20
|
+
module.exports = factory();
|
|
21
|
+
} else {
|
|
22
|
+
// Browser globals (root is window)
|
|
23
|
+
root.download = factory();
|
|
24
|
+
}
|
|
25
|
+
}( this, function () {
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
return function download( data, strFileName, strMimeType ) {
|
|
29
|
+
|
|
30
|
+
var self = window, // this script is only for browsers anyway...
|
|
31
|
+
defaultMime = "application/octet-stream", // this default mime also triggers iframe downloads
|
|
32
|
+
mimeType = strMimeType || defaultMime,
|
|
33
|
+
payload = data,
|
|
34
|
+
url = !strFileName && !strMimeType && payload,
|
|
35
|
+
anchor = document.createElement( "a" ),
|
|
36
|
+
toString = function ( a ) { return String( a ); },
|
|
37
|
+
myBlob = ( self.Blob || self.MozBlob || self.WebKitBlob || toString ),
|
|
38
|
+
fileName = strFileName || "download",
|
|
39
|
+
blob,
|
|
40
|
+
reader;
|
|
41
|
+
myBlob = myBlob.call ? myBlob.bind( self ) : Blob;
|
|
42
|
+
|
|
43
|
+
if ( String( this ) === "true" ) { //reverse arguments, allowing download.bind(true, "text/xml", "export.xml") to act as a callback
|
|
44
|
+
payload = [payload, mimeType];
|
|
45
|
+
mimeType = payload[0];
|
|
46
|
+
payload = payload[1];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if ( url && url.length < 2048 ) { // if no filename and no mime, assume a url was passed as the only argument
|
|
51
|
+
fileName = url.split( "/" ).pop().split( "?" )[0];
|
|
52
|
+
anchor.href = url; // assign href prop to temp anchor
|
|
53
|
+
if ( anchor.href.indexOf( url ) !== -1 ) { // if the browser determines that it's a potentially valid url path:
|
|
54
|
+
var ajax = new XMLHttpRequest();
|
|
55
|
+
ajax.open( "GET", url, true );
|
|
56
|
+
ajax.responseType = 'blob';
|
|
57
|
+
ajax.onload = function ( e ) {
|
|
58
|
+
download( e.target.response, fileName, defaultMime );
|
|
59
|
+
};
|
|
60
|
+
setTimeout( function () { ajax.send(); }, 0 ); // allows setting custom ajax headers using the return:
|
|
61
|
+
return ajax;
|
|
62
|
+
} // end if valid url?
|
|
63
|
+
} // end if url?
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
//go ahead and download dataURLs right away
|
|
67
|
+
if ( /^data:([\w+-]+\/[\w+.-]+)?[,;]/.test( payload ) ) {
|
|
68
|
+
|
|
69
|
+
if ( payload.length > ( 1024 * 1024 * 1.999 ) && myBlob !== toString ) {
|
|
70
|
+
payload = dataUrlToBlob( payload );
|
|
71
|
+
mimeType = payload.type || defaultMime;
|
|
72
|
+
} else {
|
|
73
|
+
return navigator.msSaveBlob ? // IE10 can't do a[download], only Blobs:
|
|
74
|
+
navigator.msSaveBlob( dataUrlToBlob( payload ), fileName ) :
|
|
75
|
+
saver( payload ); // everyone else can save dataURLs un-processed
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
} else {//not data url, is it a string with special needs?
|
|
79
|
+
if ( /([\x80-\xff])/.test( payload ) ) {
|
|
80
|
+
var i = 0, tempUiArr = new Uint8Array( payload.length ), mx = tempUiArr.length;
|
|
81
|
+
for ( i; i < mx; ++i ) tempUiArr[i] = payload.charCodeAt( i );
|
|
82
|
+
payload = new myBlob( [tempUiArr], { type: mimeType } );
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
blob = payload instanceof myBlob ?
|
|
86
|
+
payload :
|
|
87
|
+
new myBlob( [payload], { type: mimeType } );
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
function dataUrlToBlob( strUrl ) {
|
|
91
|
+
var parts = strUrl.split( /[:;,]/ ),
|
|
92
|
+
type = parts[1],
|
|
93
|
+
indexDecoder = strUrl.indexOf( "charset" ) > 0 ? 3 : 2,
|
|
94
|
+
decoder = parts[indexDecoder] == "base64" ? atob : decodeURIComponent,
|
|
95
|
+
binData = decoder( parts.pop() ),
|
|
96
|
+
mx = binData.length,
|
|
97
|
+
i = 0,
|
|
98
|
+
uiArr = new Uint8Array( mx );
|
|
99
|
+
|
|
100
|
+
for ( i; i < mx; ++i ) uiArr[i] = binData.charCodeAt( i );
|
|
101
|
+
|
|
102
|
+
return new myBlob( [uiArr], { type: type } );
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function saver( url, winMode ) {
|
|
106
|
+
|
|
107
|
+
if ( 'download' in anchor ) { //html5 A[download]
|
|
108
|
+
anchor.href = url;
|
|
109
|
+
anchor.setAttribute( "download", fileName );
|
|
110
|
+
anchor.className = "download-js-link";
|
|
111
|
+
anchor.innerHTML = "downloading...";
|
|
112
|
+
anchor.style.display = "none";
|
|
113
|
+
anchor.addEventListener( 'click', function ( e ) {
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
this.removeEventListener( 'click', arguments.callee );
|
|
116
|
+
} );
|
|
117
|
+
document.body.appendChild( anchor );
|
|
118
|
+
setTimeout( function () {
|
|
119
|
+
anchor.click();
|
|
120
|
+
document.body.removeChild( anchor );
|
|
121
|
+
if ( winMode === true ) { setTimeout( function () { self.URL.revokeObjectURL( anchor.href ); }, 250 ); }
|
|
122
|
+
}, 66 );
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// handle non-a[download] safari as best we can:
|
|
127
|
+
if ( /(Version)\/(\d+)\.(\d+)(?:\.(\d+))?.*Safari\//.test( navigator.userAgent ) ) {
|
|
128
|
+
if ( /^data:/.test( url ) ) url = "data:" + url.replace( /^data:([\w\/\-\+]+)/, defaultMime );
|
|
129
|
+
if ( !window.open( url ) ) { // popup blocked, offer direct download:
|
|
130
|
+
if ( confirm( "Displaying New Document\n\nUse Save As... to download, then click back to return to this page." ) ) { location.href = url; }
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//do iframe dataURL download (old ch+FF):
|
|
136
|
+
var f = document.createElement( "iframe" );
|
|
137
|
+
document.body.appendChild( f );
|
|
138
|
+
|
|
139
|
+
if ( !winMode && /^data:/.test( url ) ) { // force a mime that will download:
|
|
140
|
+
url = "data:" + url.replace( /^data:([\w\/\-\+]+)/, defaultMime );
|
|
141
|
+
}
|
|
142
|
+
f.src = url;
|
|
143
|
+
setTimeout( function () { document.body.removeChild( f ); }, 333 );
|
|
144
|
+
|
|
145
|
+
}//end saver
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if ( navigator.msSaveBlob ) { // IE10+ : (has Blob, but not a[download] or URL)
|
|
151
|
+
return navigator.msSaveBlob( blob, fileName );
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if ( self.URL ) { // simple fast and modern way using Blob and URL:
|
|
155
|
+
saver( self.URL.createObjectURL( blob ), true );
|
|
156
|
+
} else {
|
|
157
|
+
// handle non-Blob()+non-URL browsers:
|
|
158
|
+
if ( typeof blob === "string" || blob.constructor === toString ) {
|
|
159
|
+
try {
|
|
160
|
+
return saver( "data:" + mimeType + ";base64," + self.btoa( blob ) );
|
|
161
|
+
} catch ( y ) {
|
|
162
|
+
return saver( "data:" + mimeType + "," + encodeURIComponent( blob ) );
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Blob but not URL support:
|
|
167
|
+
reader = new FileReader();
|
|
168
|
+
reader.onload = function ( e ) {
|
|
169
|
+
saver( this.result );
|
|
170
|
+
};
|
|
171
|
+
reader.readAsDataURL( blob );
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}; /* end download() */
|
|
175
|
+
} ) );
|