@selkirk-systems/fetch 1.0.11 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Fetch.js +112 -2
- package/dist/constants/FetchConstants.js +3 -0
- package/dist/index.js +1 -1
- package/dist/middleware/FetchErrorHandler.js +1 -1
- package/lib/Fetch.js +163 -2
- package/lib/constants/FetchConstants.js +3 -0
- package/lib/index.js +1 -1
- package/lib/middleware/FetchErrorHandler.js +1 -1
- package/package.json +2 -2
- package/lib/constants/ErrorConstants.js +0 -1
package/dist/Fetch.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
//Inspired by https://www.bennadel.com/blog/4180-canceling-api-requests-using-fetch-and-abortcontroller-in-javascript.htm
|
|
2
2
|
|
|
3
3
|
import Download from './Download';
|
|
4
|
+
import { DELETE_FROM_CACHE, UPDATE_CACHE } from './constants/FetchConstants';
|
|
4
5
|
import FetchErrorHandler from './middleware/FetchErrorHandler';
|
|
6
|
+
import { dispatch, serializeData } from "@selkirk-systems/state-management";
|
|
5
7
|
|
|
6
8
|
// Regular expression patterns for testing content-type response headers.
|
|
7
9
|
const RE_CONTENT_TYPE_JSON = /^application\/(x-)?json/i;
|
|
@@ -12,6 +14,10 @@ const CONTENT_TYPE_DOWNLOADS = {
|
|
|
12
14
|
'application/pdf': true,
|
|
13
15
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': true
|
|
14
16
|
};
|
|
17
|
+
|
|
18
|
+
//30 minutes
|
|
19
|
+
const CACHED_EXPIRY_TIMESTAMP = 30 * 60000;
|
|
20
|
+
|
|
15
21
|
//We store the original promise.catch so we can override it in some
|
|
16
22
|
//scenarios when we want to swallow errors vs bubble them up.
|
|
17
23
|
const ORIGINAL_CATCH_FN = Promise.prototype.catch;
|
|
@@ -44,6 +50,7 @@ export function OnOKResponse(fn) {
|
|
|
44
50
|
return fn([network, isAbort]);
|
|
45
51
|
};
|
|
46
52
|
}
|
|
53
|
+
|
|
47
54
|
/**
|
|
48
55
|
* Make the fetch request with the given configuration options.
|
|
49
56
|
*
|
|
@@ -59,7 +66,7 @@ export function OnOKResponse(fn) {
|
|
|
59
66
|
* - status.text
|
|
60
67
|
* - status.isAbort
|
|
61
68
|
*/
|
|
62
|
-
|
|
69
|
+
function Fetch(url, options = {}) {
|
|
63
70
|
const config = {
|
|
64
71
|
downloadFileName: null,
|
|
65
72
|
contentType: "application/json",
|
|
@@ -403,4 +410,107 @@ function normalizeTransportError(transportError, request, config) {
|
|
|
403
410
|
isAbort: isAbort
|
|
404
411
|
}
|
|
405
412
|
};
|
|
406
|
-
}
|
|
413
|
+
}
|
|
414
|
+
const responseObjectJson = {
|
|
415
|
+
status: 200,
|
|
416
|
+
headers: {
|
|
417
|
+
'Content-Type': 'application/json'
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
export const getCacheByName = async name => {
|
|
421
|
+
try {
|
|
422
|
+
return caches.open(name);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
export const putJsonInCache = (cache, url, json) => {
|
|
428
|
+
const response = new Response(JSON.stringify(json), responseObjectJson);
|
|
429
|
+
return cache.put(url, response);
|
|
430
|
+
};
|
|
431
|
+
let _cache = null;
|
|
432
|
+
const DATA_METHODS = {
|
|
433
|
+
"GET": null,
|
|
434
|
+
"PATCH": null,
|
|
435
|
+
"POST": null,
|
|
436
|
+
"PUT": null
|
|
437
|
+
};
|
|
438
|
+
const fetch = (url, options = {}) => {
|
|
439
|
+
function cacheResponse([response, isAbort]) {
|
|
440
|
+
const status = response.status.code;
|
|
441
|
+
const headers = response.request.headers;
|
|
442
|
+
const method = response.request.method;
|
|
443
|
+
if (status >= 200 && status < 400 && headers.get('content-type') === "application/json") {
|
|
444
|
+
if (DATA_METHODS.hasOwnProperty(method) || method === "GET") {
|
|
445
|
+
const data = serializeData(response.data);
|
|
446
|
+
if (data.page && !data.items || data.items && data.items.length === 0) {
|
|
447
|
+
deleteFromCache(_cache, url, response);
|
|
448
|
+
return [response, isAbort];
|
|
449
|
+
}
|
|
450
|
+
const finalOptions = {
|
|
451
|
+
...responseObjectJson
|
|
452
|
+
};
|
|
453
|
+
finalOptions.headers['Time-Cached'] = new Date().getTime();
|
|
454
|
+
const responseObj = new Response(JSON.stringify(response), finalOptions);
|
|
455
|
+
_cache.put(url, responseObj);
|
|
456
|
+
dispatch(UPDATE_CACHE, {
|
|
457
|
+
url: url,
|
|
458
|
+
response: response
|
|
459
|
+
});
|
|
460
|
+
return [response, isAbort];
|
|
461
|
+
}
|
|
462
|
+
if (method === "DELETE") {
|
|
463
|
+
deleteFromCache(_cache, url, response);
|
|
464
|
+
return [response, isAbort];
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return [response, isAbort];
|
|
468
|
+
}
|
|
469
|
+
function cacheMatch() {
|
|
470
|
+
//HANDLE: Data updates, always return fresh data
|
|
471
|
+
if (options.method && DATA_METHODS.hasOwnProperty(options.method)) {
|
|
472
|
+
return Fetch(url, options).then(cacheResponse);
|
|
473
|
+
}
|
|
474
|
+
return _cache.match(url).then(response => {
|
|
475
|
+
if (response) {
|
|
476
|
+
const timeCached = response.headers.get('Time-Cached');
|
|
477
|
+
if (expiredCache(timeCached)) {
|
|
478
|
+
_cache.delete(url);
|
|
479
|
+
return Fetch(url, options).then(cacheResponse);
|
|
480
|
+
}
|
|
481
|
+
Fetch(url, options).then(cacheResponse);
|
|
482
|
+
return unwrapResponseData(response).then(obj => {
|
|
483
|
+
return Promise.resolve([{
|
|
484
|
+
request: null,
|
|
485
|
+
response: response,
|
|
486
|
+
data: obj.data,
|
|
487
|
+
status: {
|
|
488
|
+
code: response.status,
|
|
489
|
+
text: response.statusText,
|
|
490
|
+
isAbort: false
|
|
491
|
+
}
|
|
492
|
+
}, false]);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
return Fetch(url, options).then(cacheResponse);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (_cache) {
|
|
499
|
+
return cacheMatch();
|
|
500
|
+
}
|
|
501
|
+
return caches.open('fetch').then(cache => {
|
|
502
|
+
_cache = cache;
|
|
503
|
+
}).then(cacheMatch);
|
|
504
|
+
};
|
|
505
|
+
function deleteFromCache(_cache, url, response) {
|
|
506
|
+
_cache.delete(url);
|
|
507
|
+
dispatch(DELETE_FROM_CACHE, {
|
|
508
|
+
url: url,
|
|
509
|
+
response: response
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
function expiredCache(timeCached) {
|
|
513
|
+
const now = new Date().getTime();
|
|
514
|
+
return Math.abs(now - timeCached) >= CACHED_EXPIRY_TIMESTAMP;
|
|
515
|
+
}
|
|
516
|
+
export default fetch;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dispatch } from "@selkirk-systems/state-management";
|
|
2
|
-
import { ADD_ERROR } from "../constants/
|
|
2
|
+
import { ADD_ERROR } from "../constants/FetchConstants";
|
|
3
3
|
const handler = err => response => options => next => {
|
|
4
4
|
if (err) {
|
|
5
5
|
dispatch(ADD_ERROR, err);
|
package/lib/Fetch.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
//Inspired by https://www.bennadel.com/blog/4180-canceling-api-requests-using-fetch-and-abortcontroller-in-javascript.htm
|
|
2
2
|
|
|
3
3
|
import Download from './Download';
|
|
4
|
+
import { DELETE_FROM_CACHE, UPDATE_CACHE } from './constants/FetchConstants';
|
|
4
5
|
import FetchErrorHandler from './middleware/FetchErrorHandler';
|
|
6
|
+
import { dispatch, serializeData } from "@selkirk-systems/state-management";
|
|
5
7
|
|
|
6
8
|
// Regular expression patterns for testing content-type response headers.
|
|
7
9
|
const RE_CONTENT_TYPE_JSON = /^application\/(x-)?json/i;
|
|
@@ -14,6 +16,11 @@ const CONTENT_TYPE_DOWNLOADS = {
|
|
|
14
16
|
'application/pdf': true,
|
|
15
17
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': true
|
|
16
18
|
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
//30 minutes
|
|
22
|
+
const CACHED_EXPIRY_TIMESTAMP = 30 * 60000;
|
|
23
|
+
|
|
17
24
|
//We store the original promise.catch so we can override it in some
|
|
18
25
|
//scenarios when we want to swallow errors vs bubble them up.
|
|
19
26
|
const ORIGINAL_CATCH_FN = Promise.prototype.catch;
|
|
@@ -51,6 +58,8 @@ export function OnOKResponse( fn ) {
|
|
|
51
58
|
return fn( [network, isAbort] );
|
|
52
59
|
}
|
|
53
60
|
}
|
|
61
|
+
|
|
62
|
+
|
|
54
63
|
/**
|
|
55
64
|
* Make the fetch request with the given configuration options.
|
|
56
65
|
*
|
|
@@ -66,7 +75,7 @@ export function OnOKResponse( fn ) {
|
|
|
66
75
|
* - status.text
|
|
67
76
|
* - status.isAbort
|
|
68
77
|
*/
|
|
69
|
-
|
|
78
|
+
function Fetch( url, options = {} ) {
|
|
70
79
|
|
|
71
80
|
const config = {
|
|
72
81
|
downloadFileName: null,
|
|
@@ -529,4 +538,156 @@ function normalizeTransportError( transportError, request, config ) {
|
|
|
529
538
|
}
|
|
530
539
|
} );
|
|
531
540
|
|
|
532
|
-
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const responseObjectJson = {
|
|
544
|
+
status: 200,
|
|
545
|
+
headers: {
|
|
546
|
+
'Content-Type': 'application/json'
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export const getCacheByName = async ( name ) => {
|
|
551
|
+
try {
|
|
552
|
+
return caches.open( name );
|
|
553
|
+
} catch ( err ) {
|
|
554
|
+
throw ( err );
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
export const putJsonInCache = ( cache, url, json ) => {
|
|
560
|
+
|
|
561
|
+
const response = new Response( JSON.stringify( json ), responseObjectJson );
|
|
562
|
+
|
|
563
|
+
return cache.put( url, response )
|
|
564
|
+
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
let _cache = null;
|
|
568
|
+
const DATA_METHODS = {
|
|
569
|
+
"GET": null,
|
|
570
|
+
"PATCH": null,
|
|
571
|
+
"POST": null,
|
|
572
|
+
"PUT": null
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const fetch = ( url, options = {} ) => {
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
function cacheResponse( [response, isAbort] ) {
|
|
579
|
+
|
|
580
|
+
const status = response.status.code;
|
|
581
|
+
const headers = response.request.headers;
|
|
582
|
+
const method = response.request.method;
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
if ( status >= 200 && status < 400 && headers.get( 'content-type' ) === "application/json" ) {
|
|
586
|
+
|
|
587
|
+
if ( DATA_METHODS.hasOwnProperty( method ) || method === "GET" ) {
|
|
588
|
+
|
|
589
|
+
const data = serializeData( response.data );
|
|
590
|
+
|
|
591
|
+
if ( data.page && !data.items || data.items && data.items.length === 0 ) {
|
|
592
|
+
|
|
593
|
+
deleteFromCache( _cache, url, response );
|
|
594
|
+
|
|
595
|
+
return [response, isAbort];
|
|
596
|
+
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const finalOptions = { ...responseObjectJson };
|
|
600
|
+
finalOptions.headers['Time-Cached'] = new Date().getTime();
|
|
601
|
+
|
|
602
|
+
const responseObj = new Response( JSON.stringify( response ), finalOptions );
|
|
603
|
+
_cache.put( url, responseObj );
|
|
604
|
+
|
|
605
|
+
dispatch( UPDATE_CACHE, { url: url, response: response } );
|
|
606
|
+
|
|
607
|
+
return [response, isAbort];
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if ( method === "DELETE" ) {
|
|
611
|
+
|
|
612
|
+
deleteFromCache( _cache, url, response );
|
|
613
|
+
return [response, isAbort];
|
|
614
|
+
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return [response, isAbort];
|
|
621
|
+
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function cacheMatch() {
|
|
625
|
+
|
|
626
|
+
//HANDLE: Data updates, always return fresh data
|
|
627
|
+
if ( options.method && DATA_METHODS.hasOwnProperty( options.method ) ) {
|
|
628
|
+
|
|
629
|
+
return Fetch( url, options ).then( cacheResponse );
|
|
630
|
+
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
return _cache.match( url ).then( ( response ) => {
|
|
635
|
+
|
|
636
|
+
if ( response ) {
|
|
637
|
+
|
|
638
|
+
const timeCached = response.headers.get( 'Time-Cached' );
|
|
639
|
+
|
|
640
|
+
if ( expiredCache( timeCached ) ) {
|
|
641
|
+
|
|
642
|
+
_cache.delete( url );
|
|
643
|
+
return Fetch( url, options ).then( cacheResponse );
|
|
644
|
+
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
Fetch( url, options ).then( cacheResponse )
|
|
648
|
+
|
|
649
|
+
return unwrapResponseData( response ).then( ( obj ) => {
|
|
650
|
+
return Promise.resolve( [{
|
|
651
|
+
request: null,
|
|
652
|
+
response: response,
|
|
653
|
+
data: obj.data,
|
|
654
|
+
status: {
|
|
655
|
+
code: response.status,
|
|
656
|
+
text: response.statusText,
|
|
657
|
+
isAbort: false
|
|
658
|
+
},
|
|
659
|
+
}, false] )
|
|
660
|
+
} )
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return Fetch( url, options ).then( cacheResponse );
|
|
666
|
+
} )
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if ( _cache ) {
|
|
670
|
+
|
|
671
|
+
return cacheMatch()
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return caches.open( 'fetch' ).then( ( cache ) => {
|
|
675
|
+
_cache = cache;
|
|
676
|
+
} ).then( cacheMatch )
|
|
677
|
+
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function deleteFromCache( _cache, url, response ) {
|
|
681
|
+
|
|
682
|
+
_cache.delete( url );
|
|
683
|
+
dispatch( DELETE_FROM_CACHE, { url: url, response: response } );
|
|
684
|
+
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function expiredCache( timeCached ) {
|
|
688
|
+
const now = new Date().getTime();
|
|
689
|
+
|
|
690
|
+
return Math.abs( now - timeCached ) >= CACHED_EXPIRY_TIMESTAMP;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export default fetch;
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { dispatch } from "@selkirk-systems/state-management";
|
|
2
|
-
import { ADD_ERROR } from "../constants/
|
|
2
|
+
import { ADD_ERROR } from "../constants/FetchConstants";
|
|
3
3
|
|
|
4
4
|
const handler = ( err ) => ( response ) => ( options ) => ( next ) => {
|
|
5
5
|
if ( err ) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selkirk-systems/fetch",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Abortable fetch library",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"author": "Marcos Bernal <mbernal@selkirksystems.com>",
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"@selkirk-systems/state-management": ">=1.0.0"
|
|
38
38
|
},
|
|
39
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "4f7bf2bed7794dfe4651752033317e80386bb782"
|
|
40
40
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const ADD_ERROR = "ADD_ERROR";
|