@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 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
- export default function Fetch(url, options = {}) {
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;
@@ -0,0 +1,3 @@
1
+ export const ADD_ERROR = "ADD_ERROR";
2
+ export const UPDATE_CACHE = "UPDATE_CACHE";
3
+ export const DELETE_FROM_CACHE = "DELETE_FROM_CACHE";
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default as Download } from './Download';
2
2
  export { default as fetch, applyMiddleware, OnOKResponse } from './Fetch';
3
- export { ADD_ERROR } from "./constants/ErrorConstants";
3
+ export * from "./constants/FetchConstants";
@@ -1,5 +1,5 @@
1
1
  import { dispatch } from "@selkirk-systems/state-management";
2
- import { ADD_ERROR } from "../constants/ErrorConstants";
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
- export default function Fetch( url, options = {} ) {
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;
@@ -0,0 +1,3 @@
1
+ export const ADD_ERROR = "ADD_ERROR";
2
+ export const UPDATE_CACHE = "UPDATE_CACHE";
3
+ export const DELETE_FROM_CACHE = "DELETE_FROM_CACHE";
package/lib/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { default as Download } from './Download';
2
2
  export { default as fetch, applyMiddleware, OnOKResponse } from './Fetch';
3
- export { ADD_ERROR } from "./constants/ErrorConstants";
3
+ export * from "./constants/FetchConstants";
@@ -1,5 +1,5 @@
1
1
  import { dispatch } from "@selkirk-systems/state-management";
2
- import { ADD_ERROR } from "../constants/ErrorConstants";
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.11",
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": "9bd782d38d100b8be6ca48a23bbf2532a6b8d584"
39
+ "gitHead": "4f7bf2bed7794dfe4651752033317e80386bb782"
40
40
  }
@@ -1 +0,0 @@
1
- export const ADD_ERROR = "ADD_ERROR";