@selkirk-systems/fetch 1.2.1 → 1.4.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 +203 -9
- package/dist/index.js +1 -0
- package/dist/utils/FetchUtils.js +74 -0
- package/lib/Fetch.js +276 -10
- 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');
|
|
@@ -498,8 +579,8 @@ const _fetch = (url, options = {}) => {
|
|
|
498
579
|
_cache.delete(url);
|
|
499
580
|
return Fetch(url, options).then(cacheResponse);
|
|
500
581
|
}
|
|
501
|
-
Fetch(url, options).then(cacheResponse);
|
|
502
582
|
return unwrapResponseData(response).then(obj => {
|
|
583
|
+
Fetch(url, options).then(cacheResponse);
|
|
503
584
|
return Promise.resolve([{
|
|
504
585
|
request: null,
|
|
505
586
|
response: response,
|
|
@@ -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
|
+
data.items = data._embedded[[Object.keys(data._embedded)[0]]];
|
|
61
|
+
await callback(results, data.items);
|
|
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
|
|
|
@@ -669,9 +762,8 @@ const _fetch = ( url, options = {} ) => {
|
|
|
669
762
|
|
|
670
763
|
}
|
|
671
764
|
|
|
672
|
-
Fetch( url, options ).then( cacheResponse )
|
|
673
|
-
|
|
674
765
|
return unwrapResponseData( response ).then( ( obj ) => {
|
|
766
|
+
Fetch( url, options ).then( cacheResponse )
|
|
675
767
|
return Promise.resolve( [{
|
|
676
768
|
request: null,
|
|
677
769
|
response: response,
|
|
@@ -705,6 +797,7 @@ const _fetch = ( url, options = {} ) => {
|
|
|
705
797
|
|
|
706
798
|
}
|
|
707
799
|
|
|
800
|
+
|
|
708
801
|
function deleteFromCache( _cache, url, response ) {
|
|
709
802
|
|
|
710
803
|
_cache.delete( url );
|
|
@@ -712,10 +805,183 @@ function deleteFromCache( _cache, url, response ) {
|
|
|
712
805
|
|
|
713
806
|
}
|
|
714
807
|
|
|
808
|
+
function cacheFindAllLike( options ) {
|
|
809
|
+
|
|
810
|
+
const finalOptions = {
|
|
811
|
+
...options,
|
|
812
|
+
findAll: true
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return cacheFindLike( finalOptions );
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function cacheFindLike( options ) {
|
|
819
|
+
|
|
820
|
+
const cache = options.cache;
|
|
821
|
+
const url = options.url;
|
|
822
|
+
const property = options.property;
|
|
823
|
+
const value = options.value;
|
|
824
|
+
const findAll = options.findAll;
|
|
825
|
+
const matchOptions = options.matchOptions
|
|
826
|
+
const method = options.method || "GET";
|
|
827
|
+
|
|
828
|
+
return cache.keys().then( ( keys ) => {
|
|
829
|
+
|
|
830
|
+
const matchingRequests = [];
|
|
831
|
+
|
|
832
|
+
for ( let i = 0; i < keys.length; i++ ) {
|
|
833
|
+
|
|
834
|
+
const request = keys[i];
|
|
835
|
+
|
|
836
|
+
if ( looksLikeResultListUrl( request.url, url ) ) {
|
|
837
|
+
|
|
838
|
+
matchingRequests.push( cache.match( request, matchOptions ).then( ( match ) => {
|
|
839
|
+
|
|
840
|
+
match._request = request;
|
|
841
|
+
|
|
842
|
+
return match;
|
|
843
|
+
} ) )
|
|
844
|
+
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return Promise.all( matchingRequests ).then( ( responses ) => {
|
|
850
|
+
let count = 0;
|
|
851
|
+
|
|
852
|
+
const ret = [];
|
|
853
|
+
|
|
854
|
+
const checkCache = async ( index ) => {
|
|
855
|
+
|
|
856
|
+
if ( index >= responses.length ) return ret;
|
|
857
|
+
|
|
858
|
+
return serializeResponse( responses[index] ).then( json => {
|
|
859
|
+
|
|
860
|
+
const items = extractDataArray( json );
|
|
861
|
+
const response = responses[count];
|
|
862
|
+
|
|
863
|
+
const retObj = {
|
|
864
|
+
request: response._request,
|
|
865
|
+
response: response,
|
|
866
|
+
data: json,
|
|
867
|
+
match: json,
|
|
868
|
+
matchIndex: -1,
|
|
869
|
+
status: {
|
|
870
|
+
code: response.status,
|
|
871
|
+
text: response.statusText,
|
|
872
|
+
isAbort: false
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if ( method === "POST" ) {
|
|
877
|
+
return findAll ? [retObj] : retObj;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const matchIndex = items.findIndex( item => item[property] === value );
|
|
881
|
+
|
|
882
|
+
count++;
|
|
883
|
+
|
|
884
|
+
if ( matchIndex >= 0 ) {
|
|
885
|
+
|
|
886
|
+
retObj.match = items[matchIndex];
|
|
887
|
+
retObj.matchIndex = matchIndex;
|
|
888
|
+
|
|
889
|
+
if ( !findAll ) {
|
|
890
|
+
return retObj;
|
|
891
|
+
}
|
|
892
|
+
else {
|
|
893
|
+
ret.push( retObj );
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return checkCache( count );
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
if ( count >= responses.length ) {
|
|
901
|
+
return findAll ? ret : null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return checkCache( count );
|
|
905
|
+
|
|
906
|
+
} )
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if ( !responses || !responses.length ) return findAll ? ret : null;
|
|
910
|
+
|
|
911
|
+
return checkCache( count );
|
|
912
|
+
} )
|
|
913
|
+
} );
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/**
|
|
919
|
+
* Check if request.url looks like a list result type url, if it contains a UUID it is likely not.
|
|
920
|
+
* @param {*} requestURL
|
|
921
|
+
* @param {*} matchURL
|
|
922
|
+
* @returns
|
|
923
|
+
*/
|
|
924
|
+
function looksLikeResultListUrl( requestURL, matchURL ) {
|
|
925
|
+
|
|
926
|
+
const uuid = getUUID( requestURL.toString() );
|
|
927
|
+
|
|
928
|
+
return !uuid.id && requestURL.indexOf( matchURL ) >= 0;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
function extractDataArray( obj ) {
|
|
933
|
+
for ( var prop in obj._embedded ) {
|
|
934
|
+
if ( !obj._embedded.hasOwnProperty( prop ) ) continue;
|
|
935
|
+
return obj._embedded[prop];
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return [obj];
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
async function serializeResponse( response ) {
|
|
942
|
+
|
|
943
|
+
return response.text().then( data => {
|
|
944
|
+
return JSON.parse( data );
|
|
945
|
+
} )
|
|
946
|
+
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function getUUID( str ) {
|
|
950
|
+
|
|
951
|
+
const UUID_REG_EXP = /.+([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}).*?/;
|
|
952
|
+
const match = UUID_REG_EXP.exec( str );
|
|
953
|
+
|
|
954
|
+
let id, shortPath;
|
|
955
|
+
|
|
956
|
+
if ( match && match[1] ) {
|
|
957
|
+
id = match[1];
|
|
958
|
+
shortPath = str.split( `/${id}` )[0];
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return {
|
|
962
|
+
id: id,
|
|
963
|
+
shortPath: shortPath
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
function getCacheNameFromUrl( url ) {
|
|
971
|
+
|
|
972
|
+
const API_REG_EXP = /p\/.+com\/(.*?)\//;
|
|
973
|
+
const matchArray = url.toString().match( API_REG_EXP );
|
|
974
|
+
|
|
975
|
+
return matchArray && matchArray.length >= 1 ? matchArray[1] : null;
|
|
976
|
+
|
|
977
|
+
}
|
|
978
|
+
|
|
715
979
|
function expiredCache( timeCached ) {
|
|
980
|
+
|
|
716
981
|
const now = new Date().getTime();
|
|
717
982
|
|
|
718
983
|
return Math.abs( now - timeCached ) >= CACHED_EXPIRY_TIMESTAMP;
|
|
984
|
+
|
|
719
985
|
}
|
|
720
986
|
|
|
721
987
|
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
|
+
data.items = data._embedded[[Object.keys( data._embedded )[0]]];
|
|
89
|
+
|
|
90
|
+
await callback( results, data.items );
|
|
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.4.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": "29c0902c5e98d9679a6e4dd6977459265875ae2e"
|
|
40
40
|
}
|