@selkirk-systems/fetch 1.2.1 → 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 +202 -8
- package/dist/index.js +1 -0
- package/dist/utils/FetchUtils.js +74 -0
- package/lib/Fetch.js +275 -8
- package/lib/index.js +2 -0
- package/lib/utils/FetchUtils.js +118 -0
- package/package.json +2 -2
package/dist/Fetch.js
CHANGED
|
@@ -439,34 +439,87 @@ export const putJsonInCache = (cache, url, json) => {
|
|
|
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,
|
|
445
446
|
"POST": null,
|
|
446
447
|
"PUT": null
|
|
447
448
|
};
|
|
448
|
-
const API_REG_EXP = /p\/.+com\/(.*?)\//;
|
|
449
|
-
export const getCacheNameFromUrl = url => {
|
|
450
|
-
const matchArray = url.toString().match(API_REG_EXP);
|
|
451
|
-
return matchArray && matchArray.length >= 1 ? matchArray[1] : null;
|
|
452
|
-
};
|
|
453
449
|
const _fetch = (url, options = {}) => {
|
|
454
450
|
const cacheName = getCacheNameFromUrl(url);
|
|
451
|
+
|
|
452
|
+
//HANDLE: Service worker BS, environment is different don't cache if were in a service worker.
|
|
455
453
|
if (!self || !cacheName) {
|
|
456
454
|
return Fetch(url, options);
|
|
457
455
|
}
|
|
458
456
|
let _cache = _caches[cacheName];
|
|
459
|
-
function cacheResponse([response, isAbort]) {
|
|
457
|
+
async function cacheResponse([response, isAbort]) {
|
|
460
458
|
const status = response.status.code;
|
|
461
459
|
const headers = response.request.headers;
|
|
462
460
|
const method = response.request.method;
|
|
463
461
|
if (status >= 200 && status < 400 && headers.get('content-type') === "application/json") {
|
|
464
|
-
if (DATA_METHODS.hasOwnProperty(method)
|
|
462
|
+
if (DATA_METHODS.hasOwnProperty(method)) {
|
|
465
463
|
const data = serializeData(response.data);
|
|
464
|
+
|
|
465
|
+
//HANDLE: List/Array like responses
|
|
466
466
|
if (data.page && !data.items || data.items && data.items.length === 0) {
|
|
467
467
|
deleteFromCache(_cache, url, response);
|
|
468
468
|
return [response, isAbort];
|
|
469
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
|
+
}
|
|
470
523
|
const finalOptions = {
|
|
471
524
|
...responseObjectJson
|
|
472
525
|
};
|
|
@@ -488,9 +541,37 @@ const _fetch = (url, options = {}) => {
|
|
|
488
541
|
}
|
|
489
542
|
function cacheMatch() {
|
|
490
543
|
//HANDLE: Data updates, always return fresh data
|
|
491
|
-
if (options.method && DATA_METHODS.hasOwnProperty(options.method)) {
|
|
544
|
+
if (options.skipCache || options.method && DATA_METHODS.hasOwnProperty(options.method)) {
|
|
492
545
|
return Fetch(url, options).then(cacheResponse);
|
|
493
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
|
+
}
|
|
494
575
|
return _cache.match(url).then(response => {
|
|
495
576
|
if (response) {
|
|
496
577
|
const timeCached = response.headers.get('Time-Cached');
|
|
@@ -530,6 +611,119 @@ function deleteFromCache(_cache, url, response) {
|
|
|
530
611
|
response: response
|
|
531
612
|
});
|
|
532
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
|
+
}
|
|
533
727
|
function expiredCache(timeCached) {
|
|
534
728
|
const now = new Date().getTime();
|
|
535
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
|
@@ -576,6 +576,10 @@ export const putJsonInCache = ( cache, url, json ) => {
|
|
|
576
576
|
|
|
577
577
|
const _caches = {};
|
|
578
578
|
|
|
579
|
+
const _updatingCache = {
|
|
580
|
+
|
|
581
|
+
}
|
|
582
|
+
|
|
579
583
|
const DATA_METHODS = {
|
|
580
584
|
"GET": null,
|
|
581
585
|
"PATCH": null,
|
|
@@ -583,24 +587,20 @@ const DATA_METHODS = {
|
|
|
583
587
|
"PUT": null
|
|
584
588
|
}
|
|
585
589
|
|
|
586
|
-
const API_REG_EXP = /p\/.+com\/(.*?)\//;
|
|
587
590
|
|
|
588
|
-
export const getCacheNameFromUrl = ( url ) => {
|
|
589
|
-
const matchArray = url.toString().match( API_REG_EXP );
|
|
590
|
-
return matchArray && matchArray.length >= 1 ? matchArray[1] : null;
|
|
591
|
-
}
|
|
592
591
|
|
|
593
592
|
const _fetch = ( url, options = {} ) => {
|
|
594
593
|
|
|
595
594
|
const cacheName = getCacheNameFromUrl( url );
|
|
596
595
|
|
|
596
|
+
//HANDLE: Service worker BS, environment is different don't cache if were in a service worker.
|
|
597
597
|
if ( !self || !cacheName ) {
|
|
598
598
|
return Fetch( url, options );
|
|
599
599
|
}
|
|
600
600
|
|
|
601
601
|
let _cache = _caches[cacheName];
|
|
602
602
|
|
|
603
|
-
function cacheResponse( [response, isAbort] ) {
|
|
603
|
+
async function cacheResponse( [response, isAbort] ) {
|
|
604
604
|
|
|
605
605
|
const status = response.status.code;
|
|
606
606
|
const headers = response.request.headers;
|
|
@@ -609,10 +609,11 @@ const _fetch = ( url, options = {} ) => {
|
|
|
609
609
|
|
|
610
610
|
if ( status >= 200 && status < 400 && headers.get( 'content-type' ) === "application/json" ) {
|
|
611
611
|
|
|
612
|
-
if ( DATA_METHODS.hasOwnProperty( method )
|
|
612
|
+
if ( DATA_METHODS.hasOwnProperty( method ) ) {
|
|
613
613
|
|
|
614
614
|
const data = serializeData( response.data );
|
|
615
615
|
|
|
616
|
+
//HANDLE: List/Array like responses
|
|
616
617
|
if ( data.page && !data.items || data.items && data.items.length === 0 ) {
|
|
617
618
|
|
|
618
619
|
deleteFromCache( _cache, url, response );
|
|
@@ -621,6 +622,65 @@ const _fetch = ( url, options = {} ) => {
|
|
|
621
622
|
|
|
622
623
|
}
|
|
623
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
|
+
|
|
624
684
|
const finalOptions = { ...responseObjectJson };
|
|
625
685
|
finalOptions.headers['Time-Cached'] = new Date().getTime();
|
|
626
686
|
|
|
@@ -649,12 +709,45 @@ const _fetch = ( url, options = {} ) => {
|
|
|
649
709
|
function cacheMatch() {
|
|
650
710
|
|
|
651
711
|
//HANDLE: Data updates, always return fresh data
|
|
652
|
-
if ( options.method && DATA_METHODS.hasOwnProperty( options.method ) ) {
|
|
712
|
+
if ( options.skipCache || options.method && DATA_METHODS.hasOwnProperty( options.method ) ) {
|
|
653
713
|
|
|
654
714
|
return Fetch( url, options ).then( cacheResponse );
|
|
655
715
|
|
|
656
716
|
}
|
|
657
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
|
+
|
|
658
751
|
|
|
659
752
|
return _cache.match( url ).then( ( response ) => {
|
|
660
753
|
|
|
@@ -705,6 +798,7 @@ const _fetch = ( url, options = {} ) => {
|
|
|
705
798
|
|
|
706
799
|
}
|
|
707
800
|
|
|
801
|
+
|
|
708
802
|
function deleteFromCache( _cache, url, response ) {
|
|
709
803
|
|
|
710
804
|
_cache.delete( url );
|
|
@@ -712,10 +806,183 @@ function deleteFromCache( _cache, url, response ) {
|
|
|
712
806
|
|
|
713
807
|
}
|
|
714
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
|
+
|
|
715
980
|
function expiredCache( timeCached ) {
|
|
981
|
+
|
|
716
982
|
const now = new Date().getTime();
|
|
717
983
|
|
|
718
984
|
return Math.abs( now - timeCached ) >= CACHED_EXPIRY_TIMESTAMP;
|
|
985
|
+
|
|
719
986
|
}
|
|
720
987
|
|
|
721
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
|
}
|