bv-ui-core 2.9.1 → 2.9.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -7,23 +7,68 @@ The BvFetch module provides methods to cache duplicate API calls and interact wi
7
7
 
8
8
  ## BvFetch Parameters
9
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
10
  `cacheName (String):` Optional. Specifies the name of the cache to be used. If not provided, the default cache name 'bvCache' will be used.
11
+ `cacheLimit (Integer)`: Optional. Specifies the cache size limit for the cache storage. Its value should be in MB. Default value is 10 MB.
12
12
 
13
13
  ## bvFetchFunc Method Parameters
14
14
  `url (String):` The URL of the API endpoint to fetch data from.
15
-
16
15
  `options (Object):` Optional request options such as headers, method, etc., as supported by the Fetch API.
17
16
 
18
17
  ## bvFetchFunc Return Value
19
18
  `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
19
 
21
- ## flushCache Method Parameters
20
+ ## generateCacheKey Method Parameters:
21
+ `url (String):` The URL of the API endpoint.
22
+ `options (Object):` Optional request options.
23
+ ## generateCacheKey Return Value:
24
+ `string:` The generated cache key.
25
+
26
+ ## retrieveCachedUrls Method
27
+ Retrieves cached URLs from the cache storage associated with the provided cache name.
28
+ ## retrieveCachedUrls Parameters
22
29
  This method takes no parameters.
30
+ ## retrieveCachedUrls Return Value
31
+ `void:` This method does not return anything.
32
+
33
+ ## fetchDataAndCache Method
34
+ Fetches data from the specified URL, caches the response, and returns the response.
35
+ ## Parameters
36
+ `url (String):` The URL from which to fetch data.
37
+ `options (Object):` Optional request options such as headers, method, etc., as supported by the
38
+ Fetch API.
39
+ `cacheKey (String):`
40
+ The cache key associated with the fetched data.
41
+ ## Return Value
42
+ `Promise<Response>:` A promise that resolves to the fetched response.
43
+
44
+ ## fetchFromCache Method
45
+ Function to fetch data from cache.
46
+ ## Parameters
47
+ `cacheKey (String):` The cache key to fetch data from the cache.
48
+ ## Return Value
49
+ Promise<Response|null>: A Promise that resolves with a Response object if the data is found in cache, or null if the data is not cached or expired.
23
50
 
51
+ ## cacheData Method
52
+ Caches the provided response with the specified cache key if it meets the criteria for caching.
53
+ ## Parameters
54
+ `response (Response):` The response object to be cached.
55
+ `cacheKey (String):` The cache key associated with the response.
56
+ ## Return Value
57
+ `void:` This method does not return anything.
58
+
59
+
60
+ ## flushCache Method Parameters
61
+ This method takes no parameters.
24
62
  ## flushCache Return Value
25
63
  `Promise<void>:` A promise indicating the completion of cache flush operation.
26
64
 
65
+ ## manageCache Method
66
+ Manages the cache by deleting expired cache entries and maintaining the cache size limit.
67
+ ## Parameters
68
+ This method takes no parameters.
69
+ ## Return Value
70
+ `void:` This method does not return anything.
71
+
27
72
 
28
73
  ## Usage with of `BvFetch`:
29
74
 
@@ -6,10 +6,12 @@
6
6
 
7
7
  const { fetch } = require('../polyfills/fetch')
8
8
 
9
- module.exports = function BvFetch ({ shouldCache, cacheName }) {
9
+ module.exports = function BvFetch ({ shouldCache, cacheName, cacheLimit }) {
10
10
  this.shouldCache = shouldCache;
11
11
  this.cacheName = cacheName || 'bvCache';
12
+ this.cacheLimit = cacheLimit * 1024 * 1024 || 10 * 1024 * 1024;
12
13
  this.fetchPromises = new Map();
14
+ this.cachedUrls = new Set();
13
15
 
14
16
  /**
15
17
  * Generates a unique cache key for the given URL and options.
@@ -24,6 +26,128 @@ module.exports = function BvFetch ({ shouldCache, cacheName }) {
24
26
  return key;
25
27
  };
26
28
 
29
+ /**
30
+ * Retrieves cached URLs from the cache storage associated with the provided cache name.
31
+ * @returns {void}
32
+ */
33
+
34
+ this.retrieveCachedUrls = () => {
35
+ // Open the Cache Storage
36
+ caches.open(this.cacheName).then(cache => {
37
+ // Get all cache keys
38
+ cache.keys().then(keys => {
39
+ keys.forEach(request => {
40
+ this.cachedUrls.add(request.url);
41
+ });
42
+ });
43
+ });
44
+
45
+ }
46
+
47
+ //callretrieveCachedUrls function to set the cache URL set with the cached URLS
48
+ this.retrieveCachedUrls();
49
+
50
+ /**
51
+ * Fetches data from the specified URL, caches the response, and returns the response.
52
+ * @param {string} url - The URL from which to fetch data.
53
+ * @param {string} cacheKey - The cache key associated with the fetched data.
54
+ * @returns {Promise<Response>} A Promise that resolves with the fetched response.
55
+ * @throws {Error} Throws an error if there's any problem fetching the data.
56
+ */
57
+ this.fetchDataAndCache = (url, options = {}, cacheKey) => {
58
+ return fetch(url,options)
59
+ .then((response) => {
60
+ // initiate caching of response and return the response
61
+ this.cacheData(response, cacheKey);
62
+ return response.clone();
63
+ })
64
+ .catch(function (error) {
65
+ throw new Error('Error fetching data: ' + error);
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Caches the provided response with the specified cache key if it meets the criteria for caching.
71
+ * @param {Response} response - The response object to be cached.
72
+ * @param {string} cacheKey - The cache key associated with the response.
73
+ * @returns {void}
74
+ */
75
+
76
+ this.cacheData = (response, cacheKey) => {
77
+ const errJson = response.clone();
78
+ let canBeCached = true;
79
+ // Check for error in response obj
80
+ errJson.json().then(json => {
81
+ if (typeof this.shouldCache === 'function') {
82
+ canBeCached = this.shouldCache(json);
83
+ }
84
+ }).then(() => {
85
+ if (canBeCached) {
86
+ const clonedResponse = response.clone()
87
+ const newHeaders = new Headers();
88
+ clonedResponse.headers.forEach((value, key) => {
89
+ newHeaders.append(key, value);
90
+ });
91
+ newHeaders.append('X-Bazaarvoice-Cached-Time', Date.now())
92
+ // Get response text to calculate its size
93
+ clonedResponse.text().then(text => {
94
+ // Calculate size of response text in bytes
95
+ const sizeInBytes = new Blob([text]).size;
96
+
97
+ // Append response size to headers
98
+ newHeaders.append('X-Bazaarvoice-Response-Size', sizeInBytes);
99
+
100
+ // Create new Response object with modified headers
101
+ const newResponse = new Response(clonedResponse._bodyBlob || clonedResponse.body, {
102
+ status: clonedResponse.status,
103
+ statusText: clonedResponse.statusText,
104
+ headers: newHeaders
105
+ });
106
+ // Cache the response
107
+ caches.open(this.cacheName).then(cache => {
108
+ cache.put(cacheKey, newResponse);
109
+ //add key to cachedUrls set
110
+ this.cachedUrls.add(cacheKey);
111
+ });
112
+ });
113
+ }
114
+ })
115
+ }
116
+
117
+ /**
118
+ * Function to fetch data from cache.
119
+ * @param {string} cacheKey - The cache key to fetch data from the cache.
120
+ * @returns {Promise<Response|null>} A Promise that resolves with a Response object if the data is found in cache,
121
+ * or null if the data is not cached or expired.
122
+ * @throws {Error} Throws an error if there's any problem fetching from cache.
123
+ */
124
+ this.fetchFromCache = (cacheKey) => {
125
+ // Check if the URL is in the set of cached URLs
126
+ if (!this.cachedUrls.has(cacheKey)) {
127
+ return Promise.resolve(null);
128
+ }
129
+
130
+ // Open the cache and try to match the URL
131
+ return caches.open(this.cacheName)
132
+ .then((cache) => {
133
+ return cache.match(cacheKey)
134
+ .then((cachedResponse) => {
135
+
136
+ const cachedTime = cachedResponse.headers.get('X-Bazaarvoice-Cached-Time');
137
+ const ttl = cachedResponse.headers.get('Cache-Control').match(/max-age=(\d+)/)[1];
138
+ const currentTimestamp = Date.now();
139
+ const cacheAge = (currentTimestamp - cachedTime) / 1000;
140
+ if (cacheAge < ttl) {
141
+ // Cached response found
142
+ return cachedResponse.clone();
143
+ }
144
+ })
145
+ })
146
+ .catch((error) => {
147
+ throw new Error('Error fetching from cache: ' + error);
148
+ });
149
+ }
150
+
27
151
  /**
28
152
  * Fetches data from the API endpoint, caches responses, and handles caching logic.
29
153
  * @param {string} url - The URL of the API endpoint.
@@ -32,84 +156,38 @@ module.exports = function BvFetch ({ shouldCache, cacheName }) {
32
156
  */
33
157
 
34
158
  this.bvFetchFunc = (url, options = {}) => {
35
- // get the key
159
+
36
160
  const cacheKey = this.generateCacheKey(url, options);
161
+ // If an ongoing fetch promise exists for the URL, return it
162
+ if (this.fetchPromises.has(cacheKey)) {
163
+ return this.fetchPromises.get(cacheKey).then(res => res.clone());
164
+ }
37
165
 
38
- // check if its available in the cache
39
- return caches.open(this.cacheName)
40
- .then(currentCache => currentCache.match(cacheKey))
41
- .then(cachedResponse => {
166
+ // Check if response is available in cache
167
+ const newPromise = this.fetchFromCache(cacheKey)
168
+ .then((cachedResponse) => {
169
+ // If response found in cache, return it
42
170
  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
- }
171
+ return cachedResponse;
52
172
  }
173
+ // If response not found in cache, fetch from API and cache it
174
+ return this.fetchDataAndCache(url, options, cacheKey);
175
+ });
53
176
 
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
- }
177
+ // Store the ongoing fetch promise
178
+ this.fetchPromises.set(cacheKey, newPromise);
58
179
 
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
- }
180
+ //initiate cache cleanUp
181
+ this.debounceCleanupExpiredCache();
182
+
183
+ // When fetch completes or fails, remove the promise from the store
184
+ newPromise.finally(() => {
185
+ this.fetchPromises.delete(cacheKey);
186
+ });
187
+
188
+ return newPromise.then(res => res.clone());
189
+ }
103
190
 
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
191
 
114
192
  /**
115
193
  * Clears all cache entries stored in the cache storage.
@@ -124,5 +202,89 @@ module.exports = function BvFetch ({ shouldCache, cacheName }) {
124
202
  });
125
203
  });
126
204
  };
205
+
206
+ this.manageCache = () => {
207
+ // Delete expired cache entries
208
+ caches.open(this.cacheName).then(cache => {
209
+ cache.keys().then(keys => {
210
+ keys.forEach(key => {
211
+ cache.match(key).then(response => {
212
+ const cachedTime = response.headers.get('X-Bazaarvoice-Cached-Time');
213
+ const ttl = response.headers.get('Cache-Control').match(/max-age=(\d+)/)[1];
214
+ const currentTimestamp = Date.now();
215
+ const cacheAge = (currentTimestamp - cachedTime) / 1000;
216
+ if (cacheAge >= ttl) {
217
+ cache.delete(key);
218
+ this.cachedUrls.delete(key);
219
+ }
220
+ });
221
+ });
222
+ });
223
+ });
224
+
225
+ // Calculate total size of cached responses
226
+ let totalSize = 0;
227
+ caches.open(this.cacheName).then(cache => {
228
+ cache.keys().then(keys => {
229
+ // Create an array of promises for cache match operations
230
+ const matchPromises = keys.map(key =>
231
+ cache.match(key).then(response => {
232
+ const sizeHeader = response.headers.get('X-Bazaarvoice-Response-Size');
233
+ return parseInt(sizeHeader, 10);
234
+ })
235
+ );
236
+
237
+ // wait for all match promises to resolve
238
+ return Promise.all(matchPromises)
239
+ .then(sizes => sizes.reduce((acc, size) => acc + size, 0));
240
+ }).then(size => {
241
+ totalSize = size;
242
+ // If total size exceeds 10 MB, delete old cache entries
243
+ if (totalSize > this.cacheLimit) {
244
+
245
+ // create an array of cached responses
246
+ const cacheEntries = [];
247
+ return cache.keys().then(keys => {
248
+ const cachesResEntries = keys.map(key =>
249
+ cache.match(key).then(response => {
250
+ const sizeHeader = response.headers.get('X-Bazaarvoice-Response-Size');
251
+ const lastAccessedTime = response.headers.get('X-Bazaarvoice-Cached-Time');
252
+ cacheEntries.push({ key, size: parseInt(sizeHeader, 10), lastAccessedTime });
253
+ })
254
+ );
255
+
256
+ return Promise.all(cachesResEntries)
257
+ .then(() => {
258
+ // Sort cache entries by last accessed time in ascending order
259
+ cacheEntries.sort((a, b) => a.lastAccessedTime - b.lastAccessedTime);
260
+
261
+ // Delete older cache entries until total size is under 10 MB
262
+ let currentSize = totalSize;
263
+ cacheEntries.forEach(entry => {
264
+ if (currentSize > this.cacheLimit) {
265
+ cache.delete(entry.key);
266
+ this.cachedUrls.delete(entry.key);
267
+ currentSize -= entry.size;
268
+ }
269
+ });
270
+ });
271
+ });
272
+ }
273
+ });
274
+ });
275
+ };
276
+
277
+
278
+ function debounce (func, delay) {
279
+ let timer;
280
+ return function () {
281
+ clearTimeout(timer);
282
+ timer = setTimeout(() => {
283
+ func.apply(this, arguments);
284
+ }, delay);
285
+ };
286
+ }
287
+
288
+ this.debounceCleanupExpiredCache = debounce(this.manageCache, 8000);
127
289
 
128
- }
290
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bv-ui-core",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
4
4
  "license": "Apache 2.0",
5
5
  "description": "Bazaarvoice UI-related JavaScript",
6
6
  "repository": {
@@ -65,7 +65,7 @@ describe('BvFetch', function () {
65
65
  caches.open.resolves({
66
66
  match: (key) => {
67
67
  expect(key).to.equal(cacheKey);
68
- Promise.resolve(mockResponse)
68
+ return Promise.resolve(mockResponse)
69
69
  },
70
70
  put: (key, response) => {
71
71
  cacheStorage.set(key, response);
@@ -73,6 +73,10 @@ describe('BvFetch', function () {
73
73
  }
74
74
  });
75
75
 
76
+ // Simulate that the response is cached
77
+ bvFetchInstance.cachedUrls.add(cacheKey);
78
+
79
+ // Call the function under test
76
80
  bvFetchInstance.bvFetchFunc(url, options)
77
81
  .then(response => {
78
82
  // Check if response is fetched from cache
@@ -91,19 +95,18 @@ describe('BvFetch', function () {
91
95
  done(error); // Call done with error if any
92
96
  })
93
97
  });
94
-
95
98
 
99
+
96
100
  it('should fetch from network when response is not cached', function (done) {
97
101
  const url = 'https://jsonplaceholder.typicode.com/todos';
98
102
  const options = {};
99
103
 
100
- const cacheKey = bvFetchInstance.generateCacheKey(url, options);
101
-
104
+ const matchSpy = sinon.spy((key) => {
105
+ expect(key).to.equal(cacheKey);
106
+ Promise.resolve(null)
107
+ });
102
108
  caches.open.resolves({
103
- match: (key) => {
104
- expect(key).to.equal(cacheKey);
105
- Promise.resolve(null)
106
- },
109
+ match: matchSpy,
107
110
  put: (key, response) => {
108
111
  cacheStorage.set(key, response);
109
112
  return Promise.resolve();
@@ -118,7 +121,7 @@ describe('BvFetch', function () {
118
121
  console.log(response.body)
119
122
 
120
123
  // Check if caches.match was called
121
- expect(cacheStub.called).to.be.true;
124
+ expect(matchSpy.called).to.be.false;
122
125
 
123
126
  done();
124
127
  })
@@ -133,7 +136,7 @@ describe('BvFetch', function () {
133
136
  bvFetchInstance.shouldCache = (res) => {
134
137
  return false
135
138
  };
136
-
139
+
137
140
  bvFetchInstance.bvFetchFunc(url, options)
138
141
  .then(response => {
139
142
  // Check if response is fetched from network
@@ -141,7 +144,7 @@ describe('BvFetch', function () {
141
144
  console.log(response.body)
142
145
 
143
146
  // Check if caches.match was called
144
- expect(cacheStub.calledOnce).to.be.true;
147
+ expect(cacheStub.calledOnce).to.be.false;
145
148
 
146
149
  // Check if response is not cached
147
150
  const cachedResponse = cacheStorage.get(url);
@@ -152,5 +155,59 @@ describe('BvFetch', function () {
152
155
  .catch(done);
153
156
  });
154
157
 
158
+ it('should delete cache when size is greater than 10 MB', function (done) {
159
+ // Mock cache entries exceeding 10 MB
160
+ const mockCacheEntries = [
161
+ { key: 'key1', size: 6000000 }, // 6 MB
162
+ { key: 'key2', size: 6000000 } // 6 MB
163
+ // Add more entries as needed to exceed 10 MB
164
+ ];
165
+
166
+ // Stub cache operations
167
+ const deleteSpy = sinon.spy(
168
+ (key) => {
169
+ const index = mockCacheEntries.findIndex(entry => entry.key === key);
170
+ if (index !== -1) {
171
+ mockCacheEntries.splice(index, 1); // Delete entry from mock cache entries
172
+ }
173
+ return Promise.resolve(true);
174
+ }
175
+ )
176
+ caches.open.resolves({
177
+ keys: () => Promise.resolve(mockCacheEntries.map(entry => entry.key)),
178
+ match: (key) => {
179
+ const entry = mockCacheEntries.find(entry => entry.key === key);
180
+ if (entry) {
181
+ return Promise.resolve({
182
+ headers: new Headers({
183
+ 'X-Bazaarvoice-Response-Size': entry.size.toString(),
184
+ 'X-Bazaarvoice-Cached-Time': Date.now(),
185
+ 'Cache-Control': 'max-age=3600'
186
+ })
187
+ });
188
+ }
189
+ else {
190
+ return Promise.resolve(null);
191
+ }
192
+ },
193
+ delete: deleteSpy
194
+ });
195
+
196
+ // Create a new instance of BvFetch
197
+ const bvFetchInstance = new BvFetch({ shouldCache: true });
198
+
199
+ // Call manageCache function
200
+ bvFetchInstance.manageCache()
201
+ setTimeout(() => {
202
+ // Ensure cache deletion occurred until the total size is under 10 MB
203
+ const totalSizeAfterDeletion = mockCacheEntries.reduce((acc, entry) => acc + entry.size, 0);
204
+ expect(totalSizeAfterDeletion).to.be.at.most(10 * 1024 * 1024); // Total size should be under 10 MB
205
+ // Ensure cache.delete was called for each deleted entry
206
+ expect(deleteSpy.called).to.be.true;
207
+ expect(deleteSpy.callCount).to.equal(mockCacheEntries.length);
208
+ done();
209
+ }, 500);
210
+
211
+ });
155
212
 
156
213
  });