bv-ui-core 2.8.1 → 2.9.1
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +1 -0
- package/lib/bvFetch/README.md +47 -0
- package/lib/bvFetch/index.js +128 -0
- package/lib/global/index.js +6 -6
- package/lib/loader/index.js +3 -2
- package/package.json +1 -1
- package/test/unit/bvFetch/index.spec.js +156 -0
package/README.md
CHANGED
@@ -0,0 +1,47 @@
|
|
1
|
+
# BvFetch
|
2
|
+
|
3
|
+
The BvFetch module provides methods to cache duplicate API calls and interact with the cacheStorage
|
4
|
+
|
5
|
+
|
6
|
+
## The following methods are provided:
|
7
|
+
|
8
|
+
## BvFetch Parameters
|
9
|
+
`shouldCache (Function):` A function that takes the API response JSON as input and returns a boolean indicating whether to cache the response or not. This allows you to implement custom logic based on the response content. If caching is desired, the function should return true; otherwise, false.
|
10
|
+
|
11
|
+
`cacheName (String):` Optional. Specifies the name of the cache to be used. If not provided, the default cache name 'bvCache' will be used.
|
12
|
+
|
13
|
+
## bvFetchFunc Method Parameters
|
14
|
+
`url (String):` The URL of the API endpoint to fetch data from.
|
15
|
+
|
16
|
+
`options (Object):` Optional request options such as headers, method, etc., as supported by the Fetch API.
|
17
|
+
|
18
|
+
## bvFetchFunc Return Value
|
19
|
+
`Promise<Response>:` A promise that resolves to the API response. If the response is cached, it returns the cached response. Otherwise, it fetches data from the API endpoint, caches the response according to the caching logic, and returns the fetched response.
|
20
|
+
|
21
|
+
## flushCache Method Parameters
|
22
|
+
This method takes no parameters.
|
23
|
+
|
24
|
+
## flushCache Return Value
|
25
|
+
`Promise<void>:` A promise indicating the completion of cache flush operation.
|
26
|
+
|
27
|
+
|
28
|
+
## Usage with of `BvFetch`:
|
29
|
+
|
30
|
+
```js
|
31
|
+
var BvFetch = require('bv-ui-core/lib/bvFetch')
|
32
|
+
|
33
|
+
// Initialize BV Fetch instance
|
34
|
+
const bvFetch = new BVFetch({
|
35
|
+
canBeCached: canBeCached, // optional
|
36
|
+
cacheName: "bvCache" // optional, default is "bvCache"
|
37
|
+
});
|
38
|
+
|
39
|
+
// Make API calls using bvFetchFunc method
|
40
|
+
bvFetch.bvFetchFunc('https://api.example.com/data')
|
41
|
+
.then(response => {
|
42
|
+
// Handle response
|
43
|
+
})
|
44
|
+
.catch(error => {
|
45
|
+
// Handle error
|
46
|
+
});
|
47
|
+
```
|
@@ -0,0 +1,128 @@
|
|
1
|
+
|
2
|
+
/**
|
3
|
+
* @fileOverview
|
4
|
+
* Provides api response caching utilties
|
5
|
+
*/
|
6
|
+
|
7
|
+
const { fetch } = require('../polyfills/fetch')
|
8
|
+
|
9
|
+
module.exports = function BvFetch ({ shouldCache, cacheName }) {
|
10
|
+
this.shouldCache = shouldCache;
|
11
|
+
this.cacheName = cacheName || 'bvCache';
|
12
|
+
this.fetchPromises = new Map();
|
13
|
+
|
14
|
+
/**
|
15
|
+
* Generates a unique cache key for the given URL and options.
|
16
|
+
* @param {string} url - The URL of the API endpoint.
|
17
|
+
* @param {Object} options - Optional request options.
|
18
|
+
* @returns {string} The generated cache key.
|
19
|
+
*/
|
20
|
+
|
21
|
+
this.generateCacheKey = (url, options) => {
|
22
|
+
const optionsString = (Object.keys(options).length > 0) ? JSON.stringify(options) : '';
|
23
|
+
const key = url + optionsString;
|
24
|
+
return key;
|
25
|
+
};
|
26
|
+
|
27
|
+
/**
|
28
|
+
* Fetches data from the API endpoint, caches responses, and handles caching logic.
|
29
|
+
* @param {string} url - The URL of the API endpoint.
|
30
|
+
* @param {Object} options - Optional request options.
|
31
|
+
* @returns {Promise<Response>} A promise resolving to the API response.
|
32
|
+
*/
|
33
|
+
|
34
|
+
this.bvFetchFunc = (url, options = {}) => {
|
35
|
+
// get the key
|
36
|
+
const cacheKey = this.generateCacheKey(url, options);
|
37
|
+
|
38
|
+
// check if its available in the cache
|
39
|
+
return caches.open(this.cacheName)
|
40
|
+
.then(currentCache => currentCache.match(cacheKey))
|
41
|
+
.then(cachedResponse => {
|
42
|
+
if (cachedResponse) {
|
43
|
+
const cachedTime = cachedResponse.headers.get('X-Cached-Time');
|
44
|
+
const ttl = cachedResponse.headers.get('Cache-Control').match(/max-age=(\d+)/)[1];
|
45
|
+
const currentTimestamp = Date.now();
|
46
|
+
const cacheAge = (currentTimestamp - cachedTime) / 1000;
|
47
|
+
|
48
|
+
if (cacheAge < ttl) {
|
49
|
+
// Cached response found
|
50
|
+
return cachedResponse.clone();
|
51
|
+
}
|
52
|
+
}
|
53
|
+
|
54
|
+
// check if there is an ongoing promise
|
55
|
+
if (this.fetchPromises.has(cacheKey)) {
|
56
|
+
return this.fetchPromises.get(cacheKey).then(res => res.clone());
|
57
|
+
}
|
58
|
+
|
59
|
+
// Make a new call
|
60
|
+
const newPromise = fetch(url, options);
|
61
|
+
|
62
|
+
// Push the newPromise to the fetchPromises Map
|
63
|
+
this.fetchPromises.set(cacheKey, newPromise);
|
64
|
+
|
65
|
+
return newPromise
|
66
|
+
.then(response => {
|
67
|
+
const clonedResponse = response.clone();
|
68
|
+
const errJson = clonedResponse.clone()
|
69
|
+
let canBeCached = true;
|
70
|
+
return errJson.json().then(json => {
|
71
|
+
if (typeof this.shouldCache === 'function') {
|
72
|
+
canBeCached = this.shouldCache(json);
|
73
|
+
}
|
74
|
+
return response
|
75
|
+
}).then(res => {
|
76
|
+
if (canBeCached) {
|
77
|
+
const newHeaders = new Headers();
|
78
|
+
clonedResponse.headers.forEach((value, key) => {
|
79
|
+
newHeaders.append(key, value);
|
80
|
+
});
|
81
|
+
newHeaders.append('X-Cached-Time', Date.now());
|
82
|
+
|
83
|
+
const newResponse = new Response(clonedResponse._bodyBlob, {
|
84
|
+
status: clonedResponse.status,
|
85
|
+
statusText: clonedResponse.statusText,
|
86
|
+
headers: newHeaders
|
87
|
+
});
|
88
|
+
//Delete promise from promise map once its resolved
|
89
|
+
this.fetchPromises.delete(cacheKey);
|
90
|
+
|
91
|
+
return caches.open(this.cacheName)
|
92
|
+
.then(currentCache =>
|
93
|
+
currentCache.put(cacheKey, newResponse)
|
94
|
+
)
|
95
|
+
.then(() => res);
|
96
|
+
}
|
97
|
+
else {
|
98
|
+
//Delete promise from promise map if error exists
|
99
|
+
this.fetchPromises.delete(cacheKey);
|
100
|
+
|
101
|
+
return res
|
102
|
+
}
|
103
|
+
|
104
|
+
});
|
105
|
+
})
|
106
|
+
})
|
107
|
+
.catch(err => {
|
108
|
+
// Remove the promise that was pushed earlier
|
109
|
+
this.fetchPromises.delete(cacheKey);
|
110
|
+
throw err;
|
111
|
+
});
|
112
|
+
};
|
113
|
+
|
114
|
+
/**
|
115
|
+
* Clears all cache entries stored in the cache storage.
|
116
|
+
* @returns {Promise<void>} A promise indicating cache flush completion.
|
117
|
+
*/
|
118
|
+
|
119
|
+
this.flushCache = () => {
|
120
|
+
return caches.open(this.cacheName).then(cache => {
|
121
|
+
return cache.keys().then(keys => {
|
122
|
+
const deletionPromises = keys.map(key => cache.delete(key));
|
123
|
+
return Promise.all(deletionPromises);
|
124
|
+
});
|
125
|
+
});
|
126
|
+
};
|
127
|
+
|
128
|
+
}
|
package/lib/global/index.js
CHANGED
@@ -19,20 +19,20 @@ var getGlobal = function () {
|
|
19
19
|
global import in all bundle use cases
|
20
20
|
*/
|
21
21
|
if (globalObj && globalObj.__esModule) {
|
22
|
-
const
|
23
|
-
get: function (target, prop
|
22
|
+
const proxyGlobal = new Proxy(globalObj, {
|
23
|
+
get: function (target, prop) {
|
24
24
|
if (prop === 'default') {
|
25
25
|
return target
|
26
26
|
}
|
27
27
|
|
28
|
-
return
|
28
|
+
return target[prop]
|
29
29
|
},
|
30
30
|
|
31
31
|
set: function (target, prop, value) {
|
32
|
-
|
32
|
+
target[prop] = value
|
33
|
+
return true
|
33
34
|
},
|
34
|
-
}
|
35
|
-
const proxyGlobal = new Proxy(globalObj, override)
|
35
|
+
})
|
36
36
|
return proxyGlobal
|
37
37
|
}
|
38
38
|
return globalObj
|
package/lib/loader/index.js
CHANGED
@@ -152,8 +152,9 @@ module.exports = {
|
|
152
152
|
done = true;
|
153
153
|
clearTimeout(timeoutHandle);
|
154
154
|
script.onload = script.onreadystatechange = script.onerror = null;
|
155
|
-
script.parentNode
|
156
|
-
|
155
|
+
if (script && script.parentNode) {
|
156
|
+
script.parentNode.removeChild(script);
|
157
|
+
}
|
157
158
|
if (!err) {
|
158
159
|
_loadedUrls[url] = true;
|
159
160
|
|
package/package.json
CHANGED
@@ -0,0 +1,156 @@
|
|
1
|
+
//Imports
|
2
|
+
|
3
|
+
var BvFetch = require('../../../lib/bvFetch');
|
4
|
+
|
5
|
+
describe('BvFetch', function () {
|
6
|
+
let bvFetchInstance;
|
7
|
+
let cacheStub;
|
8
|
+
let cacheStorage;
|
9
|
+
|
10
|
+
beforeEach(function () {
|
11
|
+
bvFetchInstance = new BvFetch({
|
12
|
+
shouldCache: null,
|
13
|
+
cacheName: 'testCache'
|
14
|
+
});
|
15
|
+
|
16
|
+
// Define cacheStorage as a Map
|
17
|
+
cacheStorage = new Map();
|
18
|
+
|
19
|
+
// Stubbing caches.open
|
20
|
+
cacheStub = sinon.stub(caches, 'open').resolves({
|
21
|
+
match: key => {
|
22
|
+
const cachedResponse = cacheStorage.get(key);
|
23
|
+
return Promise.resolve(cachedResponse);
|
24
|
+
},
|
25
|
+
put: (key, response) => {
|
26
|
+
cacheStorage.set(key, response);
|
27
|
+
return Promise.resolve();
|
28
|
+
}
|
29
|
+
});
|
30
|
+
|
31
|
+
});
|
32
|
+
|
33
|
+
afterEach(function () {
|
34
|
+
bvFetchInstance = null;
|
35
|
+
// Restore the original method after each test
|
36
|
+
caches.open.restore();
|
37
|
+
});
|
38
|
+
|
39
|
+
it('should generate correct cache key', function () {
|
40
|
+
const url = 'https://jsonplaceholder.typicode.com/todos';
|
41
|
+
const options = {};
|
42
|
+
const expectedKey = 'https://jsonplaceholder.typicode.com/todos';
|
43
|
+
const generatedKey = bvFetchInstance.generateCacheKey(url, options);
|
44
|
+
expect(generatedKey).to.equal(expectedKey);
|
45
|
+
});
|
46
|
+
|
47
|
+
|
48
|
+
it('should fetch from cache when the response is cached', function (done) {
|
49
|
+
const url = 'https://jsonplaceholder.typicode.com/todos';
|
50
|
+
const options = {};
|
51
|
+
|
52
|
+
// Mocking cache response
|
53
|
+
const mockResponse = new Response('Mock Data', {
|
54
|
+
status: 200,
|
55
|
+
statusText: 'OK',
|
56
|
+
headers: {
|
57
|
+
'Cache-Control': 'max-age=3600',
|
58
|
+
'X-Cached-Time': Date.now()
|
59
|
+
}
|
60
|
+
});
|
61
|
+
|
62
|
+
const cacheKey = bvFetchInstance.generateCacheKey(url, options);
|
63
|
+
|
64
|
+
// Overriding the stub for this specific test case
|
65
|
+
caches.open.resolves({
|
66
|
+
match: (key) => {
|
67
|
+
expect(key).to.equal(cacheKey);
|
68
|
+
Promise.resolve(mockResponse)
|
69
|
+
},
|
70
|
+
put: (key, response) => {
|
71
|
+
cacheStorage.set(key, response);
|
72
|
+
return Promise.resolve();
|
73
|
+
}
|
74
|
+
});
|
75
|
+
|
76
|
+
bvFetchInstance.bvFetchFunc(url, options)
|
77
|
+
.then(response => {
|
78
|
+
// Check if response is fetched from cache
|
79
|
+
expect(response).to.not.be.null;
|
80
|
+
|
81
|
+
// Check if response is cached
|
82
|
+
const cachedResponse = cacheStorage.get(cacheKey);
|
83
|
+
expect(cachedResponse).to.not.be.null;
|
84
|
+
|
85
|
+
// Check if caches.open was called
|
86
|
+
expect(cacheStub.called).to.be.true;
|
87
|
+
|
88
|
+
done();
|
89
|
+
})
|
90
|
+
.catch(error => {
|
91
|
+
done(error); // Call done with error if any
|
92
|
+
})
|
93
|
+
});
|
94
|
+
|
95
|
+
|
96
|
+
it('should fetch from network when response is not cached', function (done) {
|
97
|
+
const url = 'https://jsonplaceholder.typicode.com/todos';
|
98
|
+
const options = {};
|
99
|
+
|
100
|
+
const cacheKey = bvFetchInstance.generateCacheKey(url, options);
|
101
|
+
|
102
|
+
caches.open.resolves({
|
103
|
+
match: (key) => {
|
104
|
+
expect(key).to.equal(cacheKey);
|
105
|
+
Promise.resolve(null)
|
106
|
+
},
|
107
|
+
put: (key, response) => {
|
108
|
+
cacheStorage.set(key, response);
|
109
|
+
return Promise.resolve();
|
110
|
+
}
|
111
|
+
});
|
112
|
+
|
113
|
+
|
114
|
+
bvFetchInstance.bvFetchFunc(url, options)
|
115
|
+
.then(response => {
|
116
|
+
// Check if response is fetched from network
|
117
|
+
expect(response).to.not.be.null;
|
118
|
+
console.log(response.body)
|
119
|
+
|
120
|
+
// Check if caches.match was called
|
121
|
+
expect(cacheStub.called).to.be.true;
|
122
|
+
|
123
|
+
done();
|
124
|
+
})
|
125
|
+
.catch(done);
|
126
|
+
});
|
127
|
+
|
128
|
+
it('should not cache response when there is an error', function (done) {
|
129
|
+
const url = 'https://jsonplaceholder.typicode.com/todos';
|
130
|
+
const options = {};
|
131
|
+
|
132
|
+
// Define shouldCache directly in bvFetchInstance
|
133
|
+
bvFetchInstance.shouldCache = (res) => {
|
134
|
+
return false
|
135
|
+
};
|
136
|
+
|
137
|
+
bvFetchInstance.bvFetchFunc(url, options)
|
138
|
+
.then(response => {
|
139
|
+
// Check if response is fetched from network
|
140
|
+
expect(response).to.not.be.null;
|
141
|
+
console.log(response.body)
|
142
|
+
|
143
|
+
// Check if caches.match was called
|
144
|
+
expect(cacheStub.calledOnce).to.be.true;
|
145
|
+
|
146
|
+
// Check if response is not cached
|
147
|
+
const cachedResponse = cacheStorage.get(url);
|
148
|
+
expect(cachedResponse).to.be.undefined;
|
149
|
+
|
150
|
+
done();
|
151
|
+
})
|
152
|
+
.catch(done);
|
153
|
+
});
|
154
|
+
|
155
|
+
|
156
|
+
});
|