@selkirk-systems/fetch 1.5.2 → 1.8.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 +2 -325
- package/dist/index.js +2 -1
- package/dist/middleware/CacheAPI.js +331 -0
- package/dist/utils/FetchUtils.js +24 -3
- package/lib/Fetch.js +2 -453
- package/lib/index.js +2 -1
- package/lib/middleware/CacheAPI.js +469 -0
- package/lib/utils/FetchUtils.js +31 -2
- package/package.json +2 -2
package/dist/Fetch.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
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';
|
|
5
4
|
import FetchErrorHandler from './middleware/FetchErrorHandler';
|
|
6
|
-
import { dispatch, serializeData } from "@selkirk-systems/state-management";
|
|
7
5
|
|
|
8
6
|
// Regular expression patterns for testing content-type response headers.
|
|
9
7
|
const RE_CONTENT_TYPE_JSON = /^application\/(x-)?json/i;
|
|
@@ -15,9 +13,6 @@ const CONTENT_TYPE_DOWNLOADS = {
|
|
|
15
13
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': true
|
|
16
14
|
};
|
|
17
15
|
|
|
18
|
-
//30 minutes
|
|
19
|
-
const CACHED_EXPIRY_TIMESTAMP = 30 * 60000;
|
|
20
|
-
|
|
21
16
|
//We store the original promise.catch so we can override it in some
|
|
22
17
|
//scenarios when we want to swallow errors vs bubble them up.
|
|
23
18
|
const ORIGINAL_CATCH_FN = Promise.prototype.catch;
|
|
@@ -39,17 +34,6 @@ export function applyMiddleware(middleware = []) {
|
|
|
39
34
|
_middlewares = middleware;
|
|
40
35
|
_middlewares.push(FetchErrorHandler);
|
|
41
36
|
}
|
|
42
|
-
export function OnOKResponse(fn) {
|
|
43
|
-
return ([network, isAbort]) => {
|
|
44
|
-
//Is any status is outside 200 - 299 it is not ok
|
|
45
|
-
if (network.status.code < 200 || network.status.code >= 300 || isAbort) {
|
|
46
|
-
return [network, isAbort];
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
//Run the function since its all ok
|
|
50
|
-
return fn([network, isAbort]);
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
37
|
function isServiceWorker() {
|
|
54
38
|
return self;
|
|
55
39
|
}
|
|
@@ -265,7 +249,7 @@ function _applyMiddleware(err, response, options) {
|
|
|
265
249
|
* Unwrap the response payload from the given response based on the reported
|
|
266
250
|
* content-type.
|
|
267
251
|
*/
|
|
268
|
-
async function unwrapResponseData(response) {
|
|
252
|
+
export async function unwrapResponseData(response) {
|
|
269
253
|
var contentType = response.headers.has("content-type") ? response.headers.get("content-type") : "";
|
|
270
254
|
if (RE_CONTENT_TYPE_JSON.test(contentType)) {
|
|
271
255
|
return response.json();
|
|
@@ -421,311 +405,4 @@ function normalizeTransportError(transportError, request, config) {
|
|
|
421
405
|
}
|
|
422
406
|
};
|
|
423
407
|
}
|
|
424
|
-
|
|
425
|
-
status: 200,
|
|
426
|
-
headers: {
|
|
427
|
-
'Content-Type': 'application/json'
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
export const getCacheByName = async name => {
|
|
431
|
-
try {
|
|
432
|
-
return caches.open(name);
|
|
433
|
-
} catch (err) {
|
|
434
|
-
throw err;
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
export const putJsonInCache = (cache, url, json) => {
|
|
438
|
-
const response = new Response(JSON.stringify(json), responseObjectJson);
|
|
439
|
-
return cache.put(url, response);
|
|
440
|
-
};
|
|
441
|
-
const _caches = {};
|
|
442
|
-
const _updatingCache = {};
|
|
443
|
-
const DATA_METHODS = {
|
|
444
|
-
"GET": null,
|
|
445
|
-
"PATCH": null,
|
|
446
|
-
"POST": null,
|
|
447
|
-
"PUT": null
|
|
448
|
-
};
|
|
449
|
-
const _fetch = (url, options = {}) => {
|
|
450
|
-
const cacheName = getCacheNameFromUrl(url);
|
|
451
|
-
|
|
452
|
-
//HANDLE: Service worker BS, environment is different don't cache if were in a service worker.
|
|
453
|
-
if (!self || !cacheName) {
|
|
454
|
-
return Fetch(url, options);
|
|
455
|
-
}
|
|
456
|
-
let _cache = _caches[cacheName];
|
|
457
|
-
async function cacheResponse([response, isAbort]) {
|
|
458
|
-
const status = response.status.code;
|
|
459
|
-
const headers = response.request.headers;
|
|
460
|
-
const method = response.request.method;
|
|
461
|
-
if (status >= 200 && status < 400 && headers.get('content-type') === "application/json") {
|
|
462
|
-
if (DATA_METHODS.hasOwnProperty(method)) {
|
|
463
|
-
const data = serializeData(response.data);
|
|
464
|
-
|
|
465
|
-
//HANDLE: List/Array like responses
|
|
466
|
-
if (data.page && !data.items || data.items && data.items.length === 0) {
|
|
467
|
-
deleteFromCache(_cache, url, response);
|
|
468
|
-
return [response, isAbort];
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
//HANDLE: Record like response
|
|
472
|
-
if (data.id) {
|
|
473
|
-
let uuid = getUUID(url.toString());
|
|
474
|
-
let matchOptions = {};
|
|
475
|
-
if (method === "POST") {
|
|
476
|
-
uuid = {
|
|
477
|
-
id: data.id,
|
|
478
|
-
shortPath: url.toString()
|
|
479
|
-
};
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* POST new records are hard, what cached list do we put the new record in? what about sort? blag. Don't have a good answer yet
|
|
484
|
-
* For now, new records will be put in the first cached entry that matches at the 0 index, this 'should' be page=0 cached entries
|
|
485
|
-
* and the records should all show up in UI at the top. This is the most common UX for adding records to a list,
|
|
486
|
-
* one day some how figure out a less fragile robust method.
|
|
487
|
-
*
|
|
488
|
-
* Perhaps we should just refetch all the matched cache urls... but this could be huge and slow and network heavy...
|
|
489
|
-
*/
|
|
490
|
-
await cacheFindAllLike({
|
|
491
|
-
cache: _cache,
|
|
492
|
-
url: uuid.shortPath,
|
|
493
|
-
property: "id",
|
|
494
|
-
value: data.id,
|
|
495
|
-
method: method
|
|
496
|
-
}).then(async matches => {
|
|
497
|
-
if (matches.length) {
|
|
498
|
-
const finalOptions = {
|
|
499
|
-
...responseObjectJson
|
|
500
|
-
};
|
|
501
|
-
finalOptions.headers['Time-Cached'] = new Date().getTime();
|
|
502
|
-
await matches.forEach(async match => {
|
|
503
|
-
const items = extractDataArray(match.data);
|
|
504
|
-
|
|
505
|
-
//This is where we put the new record on the top of the cached list, even if a new network request would place it somewhere
|
|
506
|
-
// else based on sort etc.
|
|
507
|
-
if (method === "POST") {
|
|
508
|
-
items.unshift(data);
|
|
509
|
-
} else {
|
|
510
|
-
items[match.matchIndex] = data;
|
|
511
|
-
}
|
|
512
|
-
const responseObj = new Response(JSON.stringify(match.data), finalOptions);
|
|
513
|
-
dispatch(UPDATE_CACHE, {
|
|
514
|
-
url: new URL(match.request.url),
|
|
515
|
-
response: match.response
|
|
516
|
-
});
|
|
517
|
-
return _cache.put(match.request.url, responseObj);
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
return [response, isAbort];
|
|
522
|
-
}
|
|
523
|
-
const finalOptions = {
|
|
524
|
-
...responseObjectJson
|
|
525
|
-
};
|
|
526
|
-
finalOptions.headers['Time-Cached'] = new Date().getTime();
|
|
527
|
-
const responseObj = new Response(JSON.stringify(response.data), finalOptions);
|
|
528
|
-
_cache.put(url, responseObj);
|
|
529
|
-
dispatch(UPDATE_CACHE, {
|
|
530
|
-
url: url,
|
|
531
|
-
response: response
|
|
532
|
-
});
|
|
533
|
-
return [response, isAbort];
|
|
534
|
-
}
|
|
535
|
-
if (method === "DELETE") {
|
|
536
|
-
deleteFromCache(_cache, url, response);
|
|
537
|
-
return [response, isAbort];
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
return [response, isAbort];
|
|
541
|
-
}
|
|
542
|
-
function cacheMatch() {
|
|
543
|
-
//HANDLE: Data updates, always return fresh data
|
|
544
|
-
if (options.skipCache || options.method && DATA_METHODS.hasOwnProperty(options.method)) {
|
|
545
|
-
return Fetch(url, options).then(cacheResponse);
|
|
546
|
-
}
|
|
547
|
-
const uuid = getUUID(url.toString());
|
|
548
|
-
|
|
549
|
-
//HANDLE: individual records, if we are looking for a record check cache for any array or search results with this record
|
|
550
|
-
if (uuid.id) {
|
|
551
|
-
return cacheFindLike({
|
|
552
|
-
cache: _cache,
|
|
553
|
-
url: uuid.shortPath,
|
|
554
|
-
property: "id",
|
|
555
|
-
value: uuid.id
|
|
556
|
-
}).then(match => {
|
|
557
|
-
if (match) {
|
|
558
|
-
//Behind the scenes still fetch the record and update the cache but don't make the user wait for it.
|
|
559
|
-
Fetch(url, options).then(cacheResponse);
|
|
560
|
-
const responseObj = new Response(JSON.stringify(match.match));
|
|
561
|
-
return Promise.resolve([{
|
|
562
|
-
request: null,
|
|
563
|
-
response: responseObj,
|
|
564
|
-
data: match.match,
|
|
565
|
-
status: {
|
|
566
|
-
code: 200,
|
|
567
|
-
text: '',
|
|
568
|
-
isAbort: false
|
|
569
|
-
}
|
|
570
|
-
}, false]);
|
|
571
|
-
}
|
|
572
|
-
return Fetch(url, options).then(cacheResponse);
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
return _cache.match(url).then(response => {
|
|
576
|
-
if (response) {
|
|
577
|
-
const timeCached = response.headers.get('Time-Cached');
|
|
578
|
-
if (expiredCache(timeCached)) {
|
|
579
|
-
_cache.delete(url);
|
|
580
|
-
return Fetch(url, options).then(cacheResponse);
|
|
581
|
-
}
|
|
582
|
-
return unwrapResponseData(response).then(obj => {
|
|
583
|
-
Fetch(url, options).then(cacheResponse);
|
|
584
|
-
return Promise.resolve([{
|
|
585
|
-
request: null,
|
|
586
|
-
response: response,
|
|
587
|
-
data: obj,
|
|
588
|
-
status: {
|
|
589
|
-
code: response.status,
|
|
590
|
-
text: response.statusText,
|
|
591
|
-
isAbort: false
|
|
592
|
-
}
|
|
593
|
-
}, false]);
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
return Fetch(url, options).then(cacheResponse);
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
if (_cache) {
|
|
600
|
-
return cacheMatch();
|
|
601
|
-
}
|
|
602
|
-
return caches.open(cacheName).then(cache => {
|
|
603
|
-
_caches[cacheName] = cache;
|
|
604
|
-
_cache = cache;
|
|
605
|
-
}).then(cacheMatch);
|
|
606
|
-
};
|
|
607
|
-
function deleteFromCache(_cache, url, response) {
|
|
608
|
-
_cache.delete(url);
|
|
609
|
-
dispatch(DELETE_FROM_CACHE, {
|
|
610
|
-
url: url,
|
|
611
|
-
response: response
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
function cacheFindAllLike(options) {
|
|
615
|
-
const finalOptions = {
|
|
616
|
-
...options,
|
|
617
|
-
findAll: true
|
|
618
|
-
};
|
|
619
|
-
return cacheFindLike(finalOptions);
|
|
620
|
-
}
|
|
621
|
-
function cacheFindLike(options) {
|
|
622
|
-
const cache = options.cache;
|
|
623
|
-
const url = options.url;
|
|
624
|
-
const property = options.property;
|
|
625
|
-
const value = options.value;
|
|
626
|
-
const findAll = options.findAll;
|
|
627
|
-
const matchOptions = options.matchOptions;
|
|
628
|
-
const method = options.method || "GET";
|
|
629
|
-
return cache.keys().then(keys => {
|
|
630
|
-
const matchingRequests = [];
|
|
631
|
-
for (let i = 0; i < keys.length; i++) {
|
|
632
|
-
const request = keys[i];
|
|
633
|
-
if (looksLikeResultListUrl(request.url, url)) {
|
|
634
|
-
matchingRequests.push(cache.match(request, matchOptions).then(match => {
|
|
635
|
-
match._request = request;
|
|
636
|
-
return match;
|
|
637
|
-
}));
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
return Promise.all(matchingRequests).then(responses => {
|
|
641
|
-
let count = 0;
|
|
642
|
-
const ret = [];
|
|
643
|
-
const checkCache = async index => {
|
|
644
|
-
if (index >= responses.length) return ret;
|
|
645
|
-
return serializeResponse(responses[index]).then(json => {
|
|
646
|
-
const items = extractDataArray(json);
|
|
647
|
-
const response = responses[count];
|
|
648
|
-
const retObj = {
|
|
649
|
-
request: response._request,
|
|
650
|
-
response: response,
|
|
651
|
-
data: json,
|
|
652
|
-
match: json,
|
|
653
|
-
matchIndex: -1,
|
|
654
|
-
status: {
|
|
655
|
-
code: response.status,
|
|
656
|
-
text: response.statusText,
|
|
657
|
-
isAbort: false
|
|
658
|
-
}
|
|
659
|
-
};
|
|
660
|
-
if (method === "POST") {
|
|
661
|
-
return findAll ? [retObj] : retObj;
|
|
662
|
-
}
|
|
663
|
-
const matchIndex = items.findIndex(item => item[property] === value);
|
|
664
|
-
count++;
|
|
665
|
-
if (matchIndex >= 0) {
|
|
666
|
-
retObj.match = items[matchIndex];
|
|
667
|
-
retObj.matchIndex = matchIndex;
|
|
668
|
-
if (!findAll) {
|
|
669
|
-
return retObj;
|
|
670
|
-
} else {
|
|
671
|
-
ret.push(retObj);
|
|
672
|
-
}
|
|
673
|
-
return checkCache(count);
|
|
674
|
-
}
|
|
675
|
-
if (count >= responses.length) {
|
|
676
|
-
return findAll ? ret : null;
|
|
677
|
-
}
|
|
678
|
-
return checkCache(count);
|
|
679
|
-
});
|
|
680
|
-
};
|
|
681
|
-
if (!responses || !responses.length) return findAll ? ret : null;
|
|
682
|
-
return checkCache(count);
|
|
683
|
-
});
|
|
684
|
-
});
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
/**
|
|
688
|
-
* Check if request.url looks like a list result type url, if it contains a UUID it is likely not.
|
|
689
|
-
* @param {*} requestURL
|
|
690
|
-
* @param {*} matchURL
|
|
691
|
-
* @returns
|
|
692
|
-
*/
|
|
693
|
-
function looksLikeResultListUrl(requestURL, matchURL) {
|
|
694
|
-
const uuid = getUUID(requestURL.toString());
|
|
695
|
-
return !uuid.id && requestURL.indexOf(matchURL) >= 0;
|
|
696
|
-
}
|
|
697
|
-
function extractDataArray(obj) {
|
|
698
|
-
for (var prop in obj._embedded) {
|
|
699
|
-
if (!obj._embedded.hasOwnProperty(prop)) continue;
|
|
700
|
-
return obj._embedded[prop];
|
|
701
|
-
}
|
|
702
|
-
return [obj];
|
|
703
|
-
}
|
|
704
|
-
async function serializeResponse(response) {
|
|
705
|
-
return response.text().then(data => {
|
|
706
|
-
return JSON.parse(data);
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
function getUUID(str) {
|
|
710
|
-
const UUID_REG_EXP = /.+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*?/;
|
|
711
|
-
const match = UUID_REG_EXP.exec(str);
|
|
712
|
-
let id, shortPath;
|
|
713
|
-
if (match && match[1]) {
|
|
714
|
-
id = match[1];
|
|
715
|
-
shortPath = str.split(`/${id}`)[0];
|
|
716
|
-
}
|
|
717
|
-
return {
|
|
718
|
-
id: id,
|
|
719
|
-
shortPath: shortPath
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
function getCacheNameFromUrl(url) {
|
|
723
|
-
const API_REG_EXP = /p\/.+com\/(.*?)\//;
|
|
724
|
-
const matchArray = url.toString().match(API_REG_EXP);
|
|
725
|
-
return matchArray && matchArray.length >= 1 ? matchArray[1] : null;
|
|
726
|
-
}
|
|
727
|
-
function expiredCache(timeCached) {
|
|
728
|
-
const now = new Date().getTime();
|
|
729
|
-
return Math.abs(now - timeCached) >= CACHED_EXPIRY_TIMESTAMP;
|
|
730
|
-
}
|
|
731
|
-
export default _fetch;
|
|
408
|
+
export default Fetch;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as Download } from './Download';
|
|
2
|
-
export { default as fetch, applyMiddleware
|
|
2
|
+
export { default as fetch, applyMiddleware } from './Fetch';
|
|
3
|
+
export { default as CacheAPI } from './middleware/CacheAPI';
|
|
3
4
|
export * from "./utils/FetchUtils";
|
|
4
5
|
export * from "./constants/FetchConstants";
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { unwrapResponseData } from '../Fetch';
|
|
2
|
+
import { DELETE_FROM_CACHE, UPDATE_CACHE } from '../constants/FetchConstants';
|
|
3
|
+
import { dispatch, serializeData } from "@selkirk-systems/state-management";
|
|
4
|
+
const grayCSS = "color:gray;font-weight:lighter";
|
|
5
|
+
const blueCSS = "color:#87A7DB;font-weight:lighter";
|
|
6
|
+
const bodyCSS = "font-weight:bold";
|
|
7
|
+
const prevCSS = "color:gray;font-weight:bold";
|
|
8
|
+
const nextCSS = "color:#6bc679;font-weight:bold";
|
|
9
|
+
|
|
10
|
+
//30 minutes
|
|
11
|
+
const CACHED_EXPIRY_TIMESTAMP = 30 * 60000;
|
|
12
|
+
const _caches = {};
|
|
13
|
+
const DATA_METHODS = {
|
|
14
|
+
"GET": null,
|
|
15
|
+
"PATCH": null,
|
|
16
|
+
"POST": null,
|
|
17
|
+
"PUT": null
|
|
18
|
+
};
|
|
19
|
+
const responseObjectJson = {
|
|
20
|
+
status: 200,
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json'
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const CacheAPI = Fetch => (url, options = {}) => {
|
|
26
|
+
const cacheName = getCacheNameFromUrl(url);
|
|
27
|
+
|
|
28
|
+
//HANDLE: Service worker BS, environment is different don't cache if were in a service worker.
|
|
29
|
+
if (!self || !cacheName) {
|
|
30
|
+
console.log(`CacheAPI.getCacheNameFromUrl: No cacheName found in - ${url.toString()} `);
|
|
31
|
+
return Fetch(url, options);
|
|
32
|
+
}
|
|
33
|
+
let _cache = _caches[cacheName];
|
|
34
|
+
async function cacheResponse([response, isAbort]) {
|
|
35
|
+
const status = response.status.code;
|
|
36
|
+
const headers = response.request.headers;
|
|
37
|
+
const method = response.request.method;
|
|
38
|
+
if (status >= 200 && status < 400 && headers.get('content-type') === "application/json") {
|
|
39
|
+
if (DATA_METHODS.hasOwnProperty(method)) {
|
|
40
|
+
const data = serializeData(response.data);
|
|
41
|
+
|
|
42
|
+
//HANDLE: List/Array like responses
|
|
43
|
+
if (data.page && !data.items || data.items && data.items.length === 0) {
|
|
44
|
+
deleteFromCache(_cache, url, response);
|
|
45
|
+
return [response, isAbort];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//HANDLE: Record like response
|
|
49
|
+
if (data.id) {
|
|
50
|
+
let uuid = getUUID(url.toString());
|
|
51
|
+
let matchOptions = {};
|
|
52
|
+
if (method === "POST") {
|
|
53
|
+
uuid = {
|
|
54
|
+
id: data.id,
|
|
55
|
+
shortPath: url.toString()
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* POST new records are hard, what cached list do we put the new record in? what about sort? blag. Don't have a good answer yet
|
|
61
|
+
* For now, new records will be put in the first cached entry that matches at the 0 index, this 'should' be page=0 cached entries
|
|
62
|
+
* and the records should all show up in UI at the top. This is the most common UX for adding records to a list,
|
|
63
|
+
* one day some how figure out a less fragile robust method.
|
|
64
|
+
*
|
|
65
|
+
* Perhaps we should just refetch all the matched cache urls... but this could be huge and slow and network heavy...
|
|
66
|
+
*/
|
|
67
|
+
await cacheFindAllLike({
|
|
68
|
+
cache: _cache,
|
|
69
|
+
url: uuid.shortPath,
|
|
70
|
+
property: "id",
|
|
71
|
+
value: data.id,
|
|
72
|
+
method: method
|
|
73
|
+
}).then(async matches => {
|
|
74
|
+
if (matches.length) {
|
|
75
|
+
const finalOptions = {
|
|
76
|
+
...responseObjectJson
|
|
77
|
+
};
|
|
78
|
+
finalOptions.headers['Time-Cached'] = new Date().getTime();
|
|
79
|
+
await matches.forEach(async match => {
|
|
80
|
+
const items = extractDataArray(match.data);
|
|
81
|
+
|
|
82
|
+
//This is where we put the new record on the top of the cached list, even if a new network request would place it somewhere
|
|
83
|
+
// else based on sort etc.
|
|
84
|
+
if (method === "POST") {
|
|
85
|
+
items.unshift(data);
|
|
86
|
+
} else {
|
|
87
|
+
items[match.matchIndex] = data;
|
|
88
|
+
}
|
|
89
|
+
const responseObj = new Response(JSON.stringify(match.data), finalOptions);
|
|
90
|
+
dispatch(UPDATE_CACHE, {
|
|
91
|
+
url: new URL(match.request.url),
|
|
92
|
+
response: match.response
|
|
93
|
+
});
|
|
94
|
+
return _cache.put(match.request.url, responseObj);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return [response, isAbort];
|
|
99
|
+
}
|
|
100
|
+
const finalOptions = {
|
|
101
|
+
...responseObjectJson
|
|
102
|
+
};
|
|
103
|
+
finalOptions.headers['Time-Cached'] = new Date().getTime();
|
|
104
|
+
const responseObj = new Response(JSON.stringify(response.data), finalOptions);
|
|
105
|
+
_cache.put(url, responseObj);
|
|
106
|
+
dispatch(UPDATE_CACHE, {
|
|
107
|
+
url: url,
|
|
108
|
+
response: response
|
|
109
|
+
});
|
|
110
|
+
return [response, isAbort];
|
|
111
|
+
}
|
|
112
|
+
if (method === "DELETE") {
|
|
113
|
+
deleteFromCache(_cache, url, response);
|
|
114
|
+
return [response, isAbort];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return [response, isAbort];
|
|
118
|
+
}
|
|
119
|
+
function cacheMatch() {
|
|
120
|
+
//HANDLE: Data updates, always return fresh data
|
|
121
|
+
if (options.skipCache || options.method && DATA_METHODS.hasOwnProperty(options.method)) {
|
|
122
|
+
return Fetch(url, options).then(cacheResponse);
|
|
123
|
+
}
|
|
124
|
+
const uuid = getUUID(url.toString());
|
|
125
|
+
|
|
126
|
+
//HANDLE: individual records, if we are looking for a record check cache for any array or search results with this record
|
|
127
|
+
if (uuid.id) {
|
|
128
|
+
return cacheFindLike({
|
|
129
|
+
cache: _cache,
|
|
130
|
+
url: uuid.shortPath,
|
|
131
|
+
property: "id",
|
|
132
|
+
value: uuid.id
|
|
133
|
+
}).then(match => {
|
|
134
|
+
if (match) {
|
|
135
|
+
logCacheHit(url, match.match);
|
|
136
|
+
//Behind the scenes still fetch the record and update the cache but don't make the user wait for it.
|
|
137
|
+
Fetch(url, options).then(cacheResponse);
|
|
138
|
+
const responseObj = new Response(JSON.stringify(match.match));
|
|
139
|
+
return Promise.resolve([{
|
|
140
|
+
request: null,
|
|
141
|
+
response: responseObj,
|
|
142
|
+
data: match.match,
|
|
143
|
+
status: {
|
|
144
|
+
code: 200,
|
|
145
|
+
text: '',
|
|
146
|
+
isAbort: false
|
|
147
|
+
}
|
|
148
|
+
}, false]);
|
|
149
|
+
}
|
|
150
|
+
logCacheMiss(url);
|
|
151
|
+
return Fetch(url, options).then(cacheResponse);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return _cache.match(url).then(response => {
|
|
155
|
+
if (response) {
|
|
156
|
+
const timeCached = response.headers.get('Time-Cached');
|
|
157
|
+
if (expiredCache(timeCached)) {
|
|
158
|
+
logCacheExpired(url, response);
|
|
159
|
+
_cache.delete(url);
|
|
160
|
+
return Fetch(url, options).then(cacheResponse);
|
|
161
|
+
}
|
|
162
|
+
return unwrapResponseData(response).then(obj => {
|
|
163
|
+
logCacheHit(url, obj, timeCached);
|
|
164
|
+
Fetch(url, options).then(cacheResponse);
|
|
165
|
+
return Promise.resolve([{
|
|
166
|
+
request: null,
|
|
167
|
+
response: response,
|
|
168
|
+
data: obj,
|
|
169
|
+
status: {
|
|
170
|
+
code: response.status,
|
|
171
|
+
text: response.statusText,
|
|
172
|
+
isAbort: false
|
|
173
|
+
}
|
|
174
|
+
}, false]);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
logCacheMiss(url);
|
|
178
|
+
return Fetch(url, options).then(cacheResponse);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if (_cache) {
|
|
182
|
+
return cacheMatch();
|
|
183
|
+
}
|
|
184
|
+
return caches.open(cacheName).then(cache => {
|
|
185
|
+
_caches[cacheName] = cache;
|
|
186
|
+
_cache = cache;
|
|
187
|
+
}).then(cacheMatch);
|
|
188
|
+
};
|
|
189
|
+
function deleteFromCache(_cache, url, response) {
|
|
190
|
+
_cache.delete(url);
|
|
191
|
+
dispatch(DELETE_FROM_CACHE, {
|
|
192
|
+
url: url,
|
|
193
|
+
response: response
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function cacheFindAllLike(options) {
|
|
197
|
+
const finalOptions = {
|
|
198
|
+
...options,
|
|
199
|
+
findAll: true
|
|
200
|
+
};
|
|
201
|
+
return cacheFindLike(finalOptions);
|
|
202
|
+
}
|
|
203
|
+
function cacheFindLike(options) {
|
|
204
|
+
const cache = options.cache;
|
|
205
|
+
const url = options.url;
|
|
206
|
+
const property = options.property;
|
|
207
|
+
const value = options.value;
|
|
208
|
+
const findAll = options.findAll;
|
|
209
|
+
const matchOptions = options.matchOptions;
|
|
210
|
+
const method = options.method || "GET";
|
|
211
|
+
return cache.keys().then(keys => {
|
|
212
|
+
const matchingRequests = [];
|
|
213
|
+
for (let i = 0; i < keys.length; i++) {
|
|
214
|
+
const request = keys[i];
|
|
215
|
+
if (looksLikeResultListUrl(request.url, url)) {
|
|
216
|
+
matchingRequests.push(cache.match(request, matchOptions).then(match => {
|
|
217
|
+
match._request = request;
|
|
218
|
+
return match;
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return Promise.all(matchingRequests).then(responses => {
|
|
223
|
+
let count = 0;
|
|
224
|
+
const ret = [];
|
|
225
|
+
const checkCache = async index => {
|
|
226
|
+
if (index >= responses.length) return ret;
|
|
227
|
+
return serializeResponse(responses[index]).then(json => {
|
|
228
|
+
const items = extractDataArray(json);
|
|
229
|
+
const response = responses[count];
|
|
230
|
+
const retObj = {
|
|
231
|
+
request: response._request,
|
|
232
|
+
response: response,
|
|
233
|
+
data: json,
|
|
234
|
+
match: json,
|
|
235
|
+
matchIndex: -1,
|
|
236
|
+
status: {
|
|
237
|
+
code: response.status,
|
|
238
|
+
text: response.statusText,
|
|
239
|
+
isAbort: false
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
if (method === "POST") {
|
|
243
|
+
return findAll ? [retObj] : retObj;
|
|
244
|
+
}
|
|
245
|
+
const matchIndex = items.findIndex(item => item[property] === value);
|
|
246
|
+
count++;
|
|
247
|
+
if (matchIndex >= 0) {
|
|
248
|
+
retObj.match = items[matchIndex];
|
|
249
|
+
retObj.matchIndex = matchIndex;
|
|
250
|
+
if (!findAll) {
|
|
251
|
+
return retObj;
|
|
252
|
+
} else {
|
|
253
|
+
ret.push(retObj);
|
|
254
|
+
}
|
|
255
|
+
return checkCache(count);
|
|
256
|
+
}
|
|
257
|
+
if (count >= responses.length) {
|
|
258
|
+
return findAll ? ret : null;
|
|
259
|
+
}
|
|
260
|
+
return checkCache(count);
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
if (!responses || !responses.length) return findAll ? ret : null;
|
|
264
|
+
return checkCache(count);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Check if request.url looks like a list result type url, if it contains a UUID it is likely not.
|
|
271
|
+
* @param {*} requestURL
|
|
272
|
+
* @param {*} matchURL
|
|
273
|
+
* @returns
|
|
274
|
+
*/
|
|
275
|
+
function looksLikeResultListUrl(requestURL, matchURL) {
|
|
276
|
+
const uuid = getUUID(requestURL.toString());
|
|
277
|
+
return !uuid.id && requestURL.indexOf(matchURL) >= 0;
|
|
278
|
+
}
|
|
279
|
+
function extractDataArray(obj) {
|
|
280
|
+
for (var prop in obj._embedded) {
|
|
281
|
+
if (!obj._embedded.hasOwnProperty(prop)) continue;
|
|
282
|
+
return obj._embedded[prop];
|
|
283
|
+
}
|
|
284
|
+
return [obj];
|
|
285
|
+
}
|
|
286
|
+
async function serializeResponse(response) {
|
|
287
|
+
return response.text().then(data => {
|
|
288
|
+
return JSON.parse(data);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
function getUUID(str) {
|
|
292
|
+
const UUID_REG_EXP = /.+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*?/;
|
|
293
|
+
const match = UUID_REG_EXP.exec(str);
|
|
294
|
+
let id, shortPath;
|
|
295
|
+
if (match && match[1]) {
|
|
296
|
+
id = match[1];
|
|
297
|
+
shortPath = str.split(`/${id}`)[0];
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
id: id,
|
|
301
|
+
shortPath: shortPath
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function getCacheNameFromUrl(url) {
|
|
305
|
+
const API_REG_EXP = /\/.+com\/(.*?)\//;
|
|
306
|
+
const matchArray = url.toString().match(API_REG_EXP);
|
|
307
|
+
return matchArray && matchArray.length >= 1 ? matchArray[1] : null;
|
|
308
|
+
}
|
|
309
|
+
function expiredCache(timeCached) {
|
|
310
|
+
const now = new Date().getTime();
|
|
311
|
+
return Math.abs(now - timeCached) >= CACHED_EXPIRY_TIMESTAMP;
|
|
312
|
+
}
|
|
313
|
+
function logCacheHit(url, cache, timeCached) {
|
|
314
|
+
console.groupCollapsed(`%ccache %cHIT %c${url}`, blueCSS, bodyCSS, grayCSS);
|
|
315
|
+
console.log(`Cached at: ${new Date(parseInt(timeCached, 10))}`);
|
|
316
|
+
console.log(`Data`, cache);
|
|
317
|
+
console.log(`URL`, url);
|
|
318
|
+
console.groupEnd();
|
|
319
|
+
}
|
|
320
|
+
function logCacheExpired(url, cache) {
|
|
321
|
+
console.groupCollapsed(`%ccache %cEXPIRED %c${url}`, blueCSS, bodyCSS, grayCSS);
|
|
322
|
+
console.log(`URL`, url);
|
|
323
|
+
console.log(`Cache`, cache);
|
|
324
|
+
console.groupEnd();
|
|
325
|
+
}
|
|
326
|
+
function logCacheMiss(url) {
|
|
327
|
+
console.groupCollapsed(`%ccache %cMISS %c${url}`, blueCSS, bodyCSS, grayCSS);
|
|
328
|
+
console.log(`URL`, url);
|
|
329
|
+
console.groupEnd();
|
|
330
|
+
}
|
|
331
|
+
export default CacheAPI;
|