@selkirk-systems/fetch 1.2.0 → 1.3.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 +210 -8
- package/dist/index.js +1 -0
- package/dist/utils/FetchUtils.js +74 -0
- package/lib/Fetch.js +291 -9
- package/lib/index.js +2 -0
- package/lib/utils/FetchUtils.js +118 -0
- package/package.json +2 -2
package/dist/Fetch.js
CHANGED
|
@@ -438,7 +438,8 @@ export const putJsonInCache = (cache, url, json) => {
|
|
|
438
438
|
const response = new Response(JSON.stringify(json), responseObjectJson);
|
|
439
439
|
return cache.put(url, response);
|
|
440
440
|
};
|
|
441
|
-
|
|
441
|
+
const _caches = {};
|
|
442
|
+
const _updatingCache = {};
|
|
442
443
|
const DATA_METHODS = {
|
|
443
444
|
"GET": null,
|
|
444
445
|
"PATCH": null,
|
|
@@ -446,25 +447,84 @@ const DATA_METHODS = {
|
|
|
446
447
|
"PUT": null
|
|
447
448
|
};
|
|
448
449
|
const _fetch = (url, options = {}) => {
|
|
449
|
-
|
|
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) {
|
|
450
454
|
return Fetch(url, options);
|
|
451
455
|
}
|
|
452
|
-
|
|
456
|
+
let _cache = _caches[cacheName];
|
|
457
|
+
async function cacheResponse([response, isAbort]) {
|
|
453
458
|
const status = response.status.code;
|
|
454
459
|
const headers = response.request.headers;
|
|
455
460
|
const method = response.request.method;
|
|
456
461
|
if (status >= 200 && status < 400 && headers.get('content-type') === "application/json") {
|
|
457
|
-
if (DATA_METHODS.hasOwnProperty(method)
|
|
462
|
+
if (DATA_METHODS.hasOwnProperty(method)) {
|
|
458
463
|
const data = serializeData(response.data);
|
|
464
|
+
|
|
465
|
+
//HANDLE: List/Array like responses
|
|
459
466
|
if (data.page && !data.items || data.items && data.items.length === 0) {
|
|
460
467
|
deleteFromCache(_cache, url, response);
|
|
461
468
|
return [response, isAbort];
|
|
462
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
|
+
}
|
|
463
523
|
const finalOptions = {
|
|
464
524
|
...responseObjectJson
|
|
465
525
|
};
|
|
466
526
|
finalOptions.headers['Time-Cached'] = new Date().getTime();
|
|
467
|
-
const responseObj = new Response(JSON.stringify(response), finalOptions);
|
|
527
|
+
const responseObj = new Response(JSON.stringify(response.data), finalOptions);
|
|
468
528
|
_cache.put(url, responseObj);
|
|
469
529
|
dispatch(UPDATE_CACHE, {
|
|
470
530
|
url: url,
|
|
@@ -481,9 +541,37 @@ const _fetch = (url, options = {}) => {
|
|
|
481
541
|
}
|
|
482
542
|
function cacheMatch() {
|
|
483
543
|
//HANDLE: Data updates, always return fresh data
|
|
484
|
-
if (options.method && DATA_METHODS.hasOwnProperty(options.method)) {
|
|
544
|
+
if (options.skipCache || options.method && DATA_METHODS.hasOwnProperty(options.method)) {
|
|
485
545
|
return Fetch(url, options).then(cacheResponse);
|
|
486
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
|
+
}
|
|
487
575
|
return _cache.match(url).then(response => {
|
|
488
576
|
if (response) {
|
|
489
577
|
const timeCached = response.headers.get('Time-Cached');
|
|
@@ -496,7 +584,7 @@ const _fetch = (url, options = {}) => {
|
|
|
496
584
|
return Promise.resolve([{
|
|
497
585
|
request: null,
|
|
498
586
|
response: response,
|
|
499
|
-
data: obj
|
|
587
|
+
data: obj,
|
|
500
588
|
status: {
|
|
501
589
|
code: response.status,
|
|
502
590
|
text: response.statusText,
|
|
@@ -511,7 +599,8 @@ const _fetch = (url, options = {}) => {
|
|
|
511
599
|
if (_cache) {
|
|
512
600
|
return cacheMatch();
|
|
513
601
|
}
|
|
514
|
-
return caches.open(
|
|
602
|
+
return caches.open(cacheName).then(cache => {
|
|
603
|
+
_caches[cacheName] = cache;
|
|
515
604
|
_cache = cache;
|
|
516
605
|
}).then(cacheMatch);
|
|
517
606
|
};
|
|
@@ -522,6 +611,119 @@ function deleteFromCache(_cache, url, response) {
|
|
|
522
611
|
response: response
|
|
523
612
|
});
|
|
524
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
|
+
}
|
|
525
727
|
function expiredCache(timeCached) {
|
|
526
728
|
const now = new Date().getTime();
|
|
527
729
|
return Math.abs(now - timeCached) >= CACHED_EXPIRY_TIMESTAMP;
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import Fetch from '../Fetch';
|
|
2
|
+
export const createUrlParams = (params, existingParams) => {
|
|
3
|
+
//SORT the params alphabetically, very important for cache lookups as the search a params in different order will cause it to not be matched.
|
|
4
|
+
params = Object.keys(params).sort().reduce((obj, key) => {
|
|
5
|
+
obj[key] = params[key];
|
|
6
|
+
return obj;
|
|
7
|
+
}, {});
|
|
8
|
+
const searchParams = existingParams ? existingParams : new URLSearchParams();
|
|
9
|
+
for (var prop in params) {
|
|
10
|
+
if (!params.hasOwnProperty(prop) || params[prop] === undefined || params[prop] === null) continue;
|
|
11
|
+
if (Array.isArray(params[prop])) {
|
|
12
|
+
if (prop === "sort") {
|
|
13
|
+
searchParams.delete("sort");
|
|
14
|
+
}
|
|
15
|
+
for (let i = 0; i < params[prop].length; i++) {
|
|
16
|
+
const e = params[prop][i];
|
|
17
|
+
if (searchParams.has(prop) && prop !== "sort") {
|
|
18
|
+
searchParams.set(prop, e);
|
|
19
|
+
} else {
|
|
20
|
+
searchParams.append(prop, e);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (searchParams.has(prop)) {
|
|
26
|
+
searchParams.set(prop, params[prop]);
|
|
27
|
+
} else {
|
|
28
|
+
searchParams.append(prop, params[prop]);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return searchParams;
|
|
32
|
+
};
|
|
33
|
+
export async function fetchPagedURL(url, callback, options) {
|
|
34
|
+
const finalOptions = {
|
|
35
|
+
skipCache: false,
|
|
36
|
+
...options
|
|
37
|
+
};
|
|
38
|
+
callback = callback ? callback : () => {};
|
|
39
|
+
let ret = [];
|
|
40
|
+
const proxyUrl = url.toString().split("/p/");
|
|
41
|
+
const [response] = await Fetch(url, finalOptions);
|
|
42
|
+
const {
|
|
43
|
+
data,
|
|
44
|
+
status
|
|
45
|
+
} = response;
|
|
46
|
+
const size = new TextEncoder().encode(JSON.stringify(data)).length;
|
|
47
|
+
const kiloBytes = size / 1024;
|
|
48
|
+
const megaBytes = kiloBytes / 1024;
|
|
49
|
+
if (status.code > 299 || status.code < 200) {
|
|
50
|
+
return [response, null];
|
|
51
|
+
}
|
|
52
|
+
const results = {
|
|
53
|
+
page: data.page.number + 1,
|
|
54
|
+
totalPages: data.page.totalPages,
|
|
55
|
+
totalElements: data.page.totalElements,
|
|
56
|
+
pageSize: data.page.size,
|
|
57
|
+
kiloBytes: kiloBytes,
|
|
58
|
+
megaBytes: megaBytes
|
|
59
|
+
};
|
|
60
|
+
callback(results);
|
|
61
|
+
data.items = data._embedded[[Object.keys(data._embedded)[0]]];
|
|
62
|
+
ret = ret.concat(data.items || []);
|
|
63
|
+
if (data._links && data._links.next) {
|
|
64
|
+
const nextUrl = new URL(`${proxyUrl[0]}/p/${data._links.next.href}`);
|
|
65
|
+
nextUrl.search = createUrlParams({}, nextUrl.searchParams);
|
|
66
|
+
const [err, nextPage] = await fetchPagedURL(nextUrl, callback, options);
|
|
67
|
+
if (err) return [err, nextPage];
|
|
68
|
+
ret = ret.concat(nextPage);
|
|
69
|
+
}
|
|
70
|
+
return [null, ret];
|
|
71
|
+
}
|
|
72
|
+
function timeout(ms) {
|
|
73
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
74
|
+
}
|
package/lib/Fetch.js
CHANGED
|
@@ -574,7 +574,12 @@ export const putJsonInCache = ( cache, url, json ) => {
|
|
|
574
574
|
|
|
575
575
|
}
|
|
576
576
|
|
|
577
|
-
|
|
577
|
+
const _caches = {};
|
|
578
|
+
|
|
579
|
+
const _updatingCache = {
|
|
580
|
+
|
|
581
|
+
}
|
|
582
|
+
|
|
578
583
|
const DATA_METHODS = {
|
|
579
584
|
"GET": null,
|
|
580
585
|
"PATCH": null,
|
|
@@ -582,13 +587,20 @@ const DATA_METHODS = {
|
|
|
582
587
|
"PUT": null
|
|
583
588
|
}
|
|
584
589
|
|
|
590
|
+
|
|
591
|
+
|
|
585
592
|
const _fetch = ( url, options = {} ) => {
|
|
586
593
|
|
|
587
|
-
|
|
594
|
+
const cacheName = getCacheNameFromUrl( url );
|
|
595
|
+
|
|
596
|
+
//HANDLE: Service worker BS, environment is different don't cache if were in a service worker.
|
|
597
|
+
if ( !self || !cacheName ) {
|
|
588
598
|
return Fetch( url, options );
|
|
589
599
|
}
|
|
590
600
|
|
|
591
|
-
|
|
601
|
+
let _cache = _caches[cacheName];
|
|
602
|
+
|
|
603
|
+
async function cacheResponse( [response, isAbort] ) {
|
|
592
604
|
|
|
593
605
|
const status = response.status.code;
|
|
594
606
|
const headers = response.request.headers;
|
|
@@ -597,10 +609,11 @@ const _fetch = ( url, options = {} ) => {
|
|
|
597
609
|
|
|
598
610
|
if ( status >= 200 && status < 400 && headers.get( 'content-type' ) === "application/json" ) {
|
|
599
611
|
|
|
600
|
-
if ( DATA_METHODS.hasOwnProperty( method )
|
|
612
|
+
if ( DATA_METHODS.hasOwnProperty( method ) ) {
|
|
601
613
|
|
|
602
614
|
const data = serializeData( response.data );
|
|
603
615
|
|
|
616
|
+
//HANDLE: List/Array like responses
|
|
604
617
|
if ( data.page && !data.items || data.items && data.items.length === 0 ) {
|
|
605
618
|
|
|
606
619
|
deleteFromCache( _cache, url, response );
|
|
@@ -609,10 +622,69 @@ const _fetch = ( url, options = {} ) => {
|
|
|
609
622
|
|
|
610
623
|
}
|
|
611
624
|
|
|
625
|
+
//HANDLE: Record like response
|
|
626
|
+
if ( data.id ) {
|
|
627
|
+
|
|
628
|
+
let uuid = getUUID( url.toString() );
|
|
629
|
+
let matchOptions = {};
|
|
630
|
+
|
|
631
|
+
if ( method === "POST" ) {
|
|
632
|
+
|
|
633
|
+
uuid = {
|
|
634
|
+
id: data.id,
|
|
635
|
+
shortPath: url.toString()
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* 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
|
|
642
|
+
* 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
|
|
643
|
+
* and the records should all show up in UI at the top. This is the most common UX for adding records to a list,
|
|
644
|
+
* one day some how figure out a less fragile robust method.
|
|
645
|
+
*
|
|
646
|
+
* Perhaps we should just refetch all the matched cache urls... but this could be huge and slow and network heavy...
|
|
647
|
+
*/
|
|
648
|
+
await cacheFindAllLike( { cache: _cache, url: uuid.shortPath, property: "id", value: data.id, method: method } )
|
|
649
|
+
.then( async ( matches ) => {
|
|
650
|
+
|
|
651
|
+
if ( matches.length ) {
|
|
652
|
+
|
|
653
|
+
const finalOptions = { ...responseObjectJson };
|
|
654
|
+
finalOptions.headers['Time-Cached'] = new Date().getTime();
|
|
655
|
+
|
|
656
|
+
await matches.forEach( async match => {
|
|
657
|
+
|
|
658
|
+
const items = extractDataArray( match.data );
|
|
659
|
+
|
|
660
|
+
//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
|
|
661
|
+
// else based on sort etc.
|
|
662
|
+
if ( method === "POST" ) {
|
|
663
|
+
items.unshift( data );
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
items[match.matchIndex] = data;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const responseObj = new Response( JSON.stringify( match.data ), finalOptions );
|
|
670
|
+
|
|
671
|
+
dispatch( UPDATE_CACHE, { url: new URL( match.request.url ), response: match.response } );
|
|
672
|
+
|
|
673
|
+
return _cache.put( match.request.url, responseObj );
|
|
674
|
+
|
|
675
|
+
} )
|
|
676
|
+
|
|
677
|
+
}
|
|
678
|
+
} )
|
|
679
|
+
|
|
680
|
+
return [response, isAbort];
|
|
681
|
+
|
|
682
|
+
}
|
|
683
|
+
|
|
612
684
|
const finalOptions = { ...responseObjectJson };
|
|
613
685
|
finalOptions.headers['Time-Cached'] = new Date().getTime();
|
|
614
686
|
|
|
615
|
-
const responseObj = new Response( JSON.stringify( response ), finalOptions );
|
|
687
|
+
const responseObj = new Response( JSON.stringify( response.data ), finalOptions );
|
|
616
688
|
_cache.put( url, responseObj );
|
|
617
689
|
|
|
618
690
|
dispatch( UPDATE_CACHE, { url: url, response: response } );
|
|
@@ -637,12 +709,45 @@ const _fetch = ( url, options = {} ) => {
|
|
|
637
709
|
function cacheMatch() {
|
|
638
710
|
|
|
639
711
|
//HANDLE: Data updates, always return fresh data
|
|
640
|
-
if ( options.method && DATA_METHODS.hasOwnProperty( options.method ) ) {
|
|
712
|
+
if ( options.skipCache || options.method && DATA_METHODS.hasOwnProperty( options.method ) ) {
|
|
641
713
|
|
|
642
714
|
return Fetch( url, options ).then( cacheResponse );
|
|
643
715
|
|
|
644
716
|
}
|
|
645
717
|
|
|
718
|
+
const uuid = getUUID( url.toString() );
|
|
719
|
+
|
|
720
|
+
//HANDLE: individual records, if we are looking for a record check cache for any array or search results with this record
|
|
721
|
+
if ( uuid.id ) {
|
|
722
|
+
|
|
723
|
+
return cacheFindLike( { cache: _cache, url: uuid.shortPath, property: "id", value: uuid.id } ).then( ( match ) => {
|
|
724
|
+
|
|
725
|
+
if ( match ) {
|
|
726
|
+
|
|
727
|
+
//Behind the scenes still fetch the record and update the cache but don't make the user wait for it.
|
|
728
|
+
Fetch( url, options ).then( cacheResponse );
|
|
729
|
+
|
|
730
|
+
const responseObj = new Response( JSON.stringify( match.match ) );
|
|
731
|
+
|
|
732
|
+
return Promise.resolve( [{
|
|
733
|
+
request: null,
|
|
734
|
+
response: responseObj,
|
|
735
|
+
data: match.match,
|
|
736
|
+
status: {
|
|
737
|
+
code: 200,
|
|
738
|
+
text: '',
|
|
739
|
+
isAbort: false
|
|
740
|
+
},
|
|
741
|
+
}, false] )
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
return Fetch( url, options ).then( cacheResponse );
|
|
746
|
+
|
|
747
|
+
} )
|
|
748
|
+
|
|
749
|
+
}
|
|
750
|
+
|
|
646
751
|
|
|
647
752
|
return _cache.match( url ).then( ( response ) => {
|
|
648
753
|
|
|
@@ -663,7 +768,7 @@ const _fetch = ( url, options = {} ) => {
|
|
|
663
768
|
return Promise.resolve( [{
|
|
664
769
|
request: null,
|
|
665
770
|
response: response,
|
|
666
|
-
data: obj
|
|
771
|
+
data: obj,
|
|
667
772
|
status: {
|
|
668
773
|
code: response.status,
|
|
669
774
|
text: response.statusText,
|
|
@@ -679,17 +784,21 @@ const _fetch = ( url, options = {} ) => {
|
|
|
679
784
|
} )
|
|
680
785
|
}
|
|
681
786
|
|
|
682
|
-
if ( _cache ) {
|
|
683
787
|
|
|
788
|
+
|
|
789
|
+
if ( _cache ) {
|
|
684
790
|
return cacheMatch()
|
|
685
791
|
}
|
|
686
792
|
|
|
687
|
-
|
|
793
|
+
|
|
794
|
+
return caches.open( cacheName ).then( ( cache ) => {
|
|
795
|
+
_caches[cacheName] = cache;
|
|
688
796
|
_cache = cache;
|
|
689
797
|
} ).then( cacheMatch )
|
|
690
798
|
|
|
691
799
|
}
|
|
692
800
|
|
|
801
|
+
|
|
693
802
|
function deleteFromCache( _cache, url, response ) {
|
|
694
803
|
|
|
695
804
|
_cache.delete( url );
|
|
@@ -697,10 +806,183 @@ function deleteFromCache( _cache, url, response ) {
|
|
|
697
806
|
|
|
698
807
|
}
|
|
699
808
|
|
|
809
|
+
function cacheFindAllLike( options ) {
|
|
810
|
+
|
|
811
|
+
const finalOptions = {
|
|
812
|
+
...options,
|
|
813
|
+
findAll: true
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return cacheFindLike( finalOptions );
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function cacheFindLike( options ) {
|
|
820
|
+
|
|
821
|
+
const cache = options.cache;
|
|
822
|
+
const url = options.url;
|
|
823
|
+
const property = options.property;
|
|
824
|
+
const value = options.value;
|
|
825
|
+
const findAll = options.findAll;
|
|
826
|
+
const matchOptions = options.matchOptions
|
|
827
|
+
const method = options.method || "GET";
|
|
828
|
+
|
|
829
|
+
return cache.keys().then( ( keys ) => {
|
|
830
|
+
|
|
831
|
+
const matchingRequests = [];
|
|
832
|
+
|
|
833
|
+
for ( let i = 0; i < keys.length; i++ ) {
|
|
834
|
+
|
|
835
|
+
const request = keys[i];
|
|
836
|
+
|
|
837
|
+
if ( looksLikeResultListUrl( request.url, url ) ) {
|
|
838
|
+
|
|
839
|
+
matchingRequests.push( cache.match( request, matchOptions ).then( ( match ) => {
|
|
840
|
+
|
|
841
|
+
match._request = request;
|
|
842
|
+
|
|
843
|
+
return match;
|
|
844
|
+
} ) )
|
|
845
|
+
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return Promise.all( matchingRequests ).then( ( responses ) => {
|
|
851
|
+
let count = 0;
|
|
852
|
+
|
|
853
|
+
const ret = [];
|
|
854
|
+
|
|
855
|
+
const checkCache = async ( index ) => {
|
|
856
|
+
|
|
857
|
+
if ( index >= responses.length ) return ret;
|
|
858
|
+
|
|
859
|
+
return serializeResponse( responses[index] ).then( json => {
|
|
860
|
+
|
|
861
|
+
const items = extractDataArray( json );
|
|
862
|
+
const response = responses[count];
|
|
863
|
+
|
|
864
|
+
const retObj = {
|
|
865
|
+
request: response._request,
|
|
866
|
+
response: response,
|
|
867
|
+
data: json,
|
|
868
|
+
match: json,
|
|
869
|
+
matchIndex: -1,
|
|
870
|
+
status: {
|
|
871
|
+
code: response.status,
|
|
872
|
+
text: response.statusText,
|
|
873
|
+
isAbort: false
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if ( method === "POST" ) {
|
|
878
|
+
return findAll ? [retObj] : retObj;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const matchIndex = items.findIndex( item => item[property] === value );
|
|
882
|
+
|
|
883
|
+
count++;
|
|
884
|
+
|
|
885
|
+
if ( matchIndex >= 0 ) {
|
|
886
|
+
|
|
887
|
+
retObj.match = items[matchIndex];
|
|
888
|
+
retObj.matchIndex = matchIndex;
|
|
889
|
+
|
|
890
|
+
if ( !findAll ) {
|
|
891
|
+
return retObj;
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
ret.push( retObj );
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return checkCache( count );
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
if ( count >= responses.length ) {
|
|
902
|
+
return findAll ? ret : null;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
return checkCache( count );
|
|
906
|
+
|
|
907
|
+
} )
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if ( !responses || !responses.length ) return findAll ? ret : null;
|
|
911
|
+
|
|
912
|
+
return checkCache( count );
|
|
913
|
+
} )
|
|
914
|
+
} );
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Check if request.url looks like a list result type url, if it contains a UUID it is likely not.
|
|
921
|
+
* @param {*} requestURL
|
|
922
|
+
* @param {*} matchURL
|
|
923
|
+
* @returns
|
|
924
|
+
*/
|
|
925
|
+
function looksLikeResultListUrl( requestURL, matchURL ) {
|
|
926
|
+
|
|
927
|
+
const uuid = getUUID( requestURL.toString() );
|
|
928
|
+
|
|
929
|
+
return !uuid.id && requestURL.indexOf( matchURL ) >= 0;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
function extractDataArray( obj ) {
|
|
934
|
+
for ( var prop in obj._embedded ) {
|
|
935
|
+
if ( !obj._embedded.hasOwnProperty( prop ) ) continue;
|
|
936
|
+
return obj._embedded[prop];
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return [obj];
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async function serializeResponse( response ) {
|
|
943
|
+
|
|
944
|
+
return response.text().then( data => {
|
|
945
|
+
return JSON.parse( data );
|
|
946
|
+
} )
|
|
947
|
+
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function getUUID( str ) {
|
|
951
|
+
|
|
952
|
+
const UUID_REG_EXP = /.+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*?/;
|
|
953
|
+
const match = UUID_REG_EXP.exec( str );
|
|
954
|
+
|
|
955
|
+
let id, shortPath;
|
|
956
|
+
|
|
957
|
+
if ( match && match[1] ) {
|
|
958
|
+
id = match[1];
|
|
959
|
+
shortPath = str.split( `/${id}` )[0];
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
return {
|
|
963
|
+
id: id,
|
|
964
|
+
shortPath: shortPath
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
function getCacheNameFromUrl( url ) {
|
|
972
|
+
|
|
973
|
+
const API_REG_EXP = /p\/.+com\/(.*?)\//;
|
|
974
|
+
const matchArray = url.toString().match( API_REG_EXP );
|
|
975
|
+
|
|
976
|
+
return matchArray && matchArray.length >= 1 ? matchArray[1] : null;
|
|
977
|
+
|
|
978
|
+
}
|
|
979
|
+
|
|
700
980
|
function expiredCache( timeCached ) {
|
|
981
|
+
|
|
701
982
|
const now = new Date().getTime();
|
|
702
983
|
|
|
703
984
|
return Math.abs( now - timeCached ) >= CACHED_EXPIRY_TIMESTAMP;
|
|
985
|
+
|
|
704
986
|
}
|
|
705
987
|
|
|
706
988
|
export default _fetch;
|
package/lib/index.js
CHANGED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import Fetch from '../Fetch';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const createUrlParams = ( params, existingParams ) => {
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
//SORT the params alphabetically, very important for cache lookups as the search a params in different order will cause it to not be matched.
|
|
9
|
+
params = Object.keys( params ).sort().reduce(
|
|
10
|
+
( obj, key ) => {
|
|
11
|
+
obj[key] = params[key];
|
|
12
|
+
return obj;
|
|
13
|
+
},
|
|
14
|
+
{}
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const searchParams = existingParams ? existingParams : new URLSearchParams();
|
|
18
|
+
|
|
19
|
+
for ( var prop in params ) {
|
|
20
|
+
if ( !params.hasOwnProperty( prop ) || params[prop] === undefined || params[prop] === null ) continue;
|
|
21
|
+
|
|
22
|
+
if ( Array.isArray( params[prop] ) ) {
|
|
23
|
+
|
|
24
|
+
if ( prop === "sort" ) {
|
|
25
|
+
searchParams.delete( "sort" );
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for ( let i = 0; i < params[prop].length; i++ ) {
|
|
29
|
+
const e = params[prop][i];
|
|
30
|
+
if ( searchParams.has( prop ) && prop !== "sort" ) {
|
|
31
|
+
searchParams.set( prop, e );
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
searchParams.append( prop, e );
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if ( searchParams.has( prop ) ) {
|
|
41
|
+
searchParams.set( prop, params[prop] );
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
searchParams.append( prop, params[prop] );
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
return searchParams;
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
export async function fetchPagedURL( url, callback, options ) {
|
|
56
|
+
|
|
57
|
+
const finalOptions = {
|
|
58
|
+
skipCache: false,
|
|
59
|
+
...options
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
callback = callback ? callback : () => { };
|
|
63
|
+
|
|
64
|
+
let ret = [];
|
|
65
|
+
|
|
66
|
+
const proxyUrl = url.toString().split( "/p/" );
|
|
67
|
+
|
|
68
|
+
const [response] = await Fetch( url, finalOptions );
|
|
69
|
+
const { data, status } = response;
|
|
70
|
+
|
|
71
|
+
const size = new TextEncoder().encode( JSON.stringify( data ) ).length;
|
|
72
|
+
const kiloBytes = size / 1024;
|
|
73
|
+
const megaBytes = kiloBytes / 1024;
|
|
74
|
+
|
|
75
|
+
if ( status.code > 299 || status.code < 200 ) {
|
|
76
|
+
return [response, null];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const results = {
|
|
80
|
+
page: data.page.number + 1,
|
|
81
|
+
totalPages: data.page.totalPages,
|
|
82
|
+
totalElements: data.page.totalElements,
|
|
83
|
+
pageSize: data.page.size,
|
|
84
|
+
kiloBytes: kiloBytes,
|
|
85
|
+
megaBytes: megaBytes
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
callback( results );
|
|
89
|
+
|
|
90
|
+
data.items = data._embedded[[Object.keys( data._embedded )[0]]];
|
|
91
|
+
|
|
92
|
+
ret = ret.concat( data.items || [] );
|
|
93
|
+
|
|
94
|
+
if ( data._links && data._links.next ) {
|
|
95
|
+
|
|
96
|
+
const nextUrl = new URL( `${proxyUrl[0]}/p/${data._links.next.href}` );
|
|
97
|
+
|
|
98
|
+
nextUrl.search = createUrlParams( {}, nextUrl.searchParams );
|
|
99
|
+
|
|
100
|
+
const [err, nextPage] = await fetchPagedURL( nextUrl, callback, options );
|
|
101
|
+
|
|
102
|
+
if ( err ) return [err, nextPage];
|
|
103
|
+
|
|
104
|
+
ret = ret.concat( nextPage );
|
|
105
|
+
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return [null, ret];
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
function timeout( ms ) {
|
|
114
|
+
|
|
115
|
+
return new Promise( resolve => setTimeout( resolve, ms ) );
|
|
116
|
+
|
|
117
|
+
}
|
|
118
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selkirk-systems/fetch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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": "d981c2a39edb04be17105f9c328942380ccb32f7"
|
|
40
40
|
}
|