bv-ui-core 2.8.2 → 2.9.2

Sign up to get free protection for your applications and to get access to all the features.
package/README.md CHANGED
@@ -38,6 +38,7 @@ module's directory.
38
38
  ## Modules
39
39
 
40
40
  - [body](./lib/body)
41
+ - [bvFetch](./lib/bvFetch/)
41
42
  - [checkHighContrast](./lib/checkHighContrast)
42
43
  - [cookie](./lib/cookie)
43
44
  - [cookieConsent](./lib/cookieConsent)
@@ -0,0 +1,92 @@
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
+ `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
+
13
+ ## bvFetchFunc Method Parameters
14
+ `url (String):` The URL of the API endpoint to fetch data from.
15
+ `options (Object):` Optional request options such as headers, method, etc., as supported by the Fetch API.
16
+
17
+ ## bvFetchFunc Return Value
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.
19
+
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
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.
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.
62
+ ## flushCache Return Value
63
+ `Promise<void>:` A promise indicating the completion of cache flush operation.
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
+
72
+
73
+ ## Usage with of `BvFetch`:
74
+
75
+ ```js
76
+ var BvFetch = require('bv-ui-core/lib/bvFetch')
77
+
78
+ // Initialize BV Fetch instance
79
+ const bvFetch = new BVFetch({
80
+ canBeCached: canBeCached, // optional
81
+ cacheName: "bvCache" // optional, default is "bvCache"
82
+ });
83
+
84
+ // Make API calls using bvFetchFunc method
85
+ bvFetch.bvFetchFunc('https://api.example.com/data')
86
+ .then(response => {
87
+ // Handle response
88
+ })
89
+ .catch(error => {
90
+ // Handle error
91
+ });
92
+ ```
@@ -0,0 +1,290 @@
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, cacheLimit }) {
10
+ this.shouldCache = shouldCache;
11
+ this.cacheName = cacheName || 'bvCache';
12
+ this.cacheLimit = cacheLimit * 1024 * 1024 || 10 * 1024 * 1024;
13
+ this.fetchPromises = new Map();
14
+ this.cachedUrls = new Set();
15
+
16
+ /**
17
+ * Generates a unique cache key for the given URL and options.
18
+ * @param {string} url - The URL of the API endpoint.
19
+ * @param {Object} options - Optional request options.
20
+ * @returns {string} The generated cache key.
21
+ */
22
+
23
+ this.generateCacheKey = (url, options) => {
24
+ const optionsString = (Object.keys(options).length > 0) ? JSON.stringify(options) : '';
25
+ const key = url + optionsString;
26
+ return key;
27
+ };
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
+
151
+ /**
152
+ * Fetches data from the API endpoint, caches responses, and handles caching logic.
153
+ * @param {string} url - The URL of the API endpoint.
154
+ * @param {Object} options - Optional request options.
155
+ * @returns {Promise<Response>} A promise resolving to the API response.
156
+ */
157
+
158
+ this.bvFetchFunc = (url, options = {}) => {
159
+
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
+ }
165
+
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
170
+ if (cachedResponse) {
171
+ return cachedResponse;
172
+ }
173
+ // If response not found in cache, fetch from API and cache it
174
+ return this.fetchDataAndCache(url, options, cacheKey);
175
+ });
176
+
177
+ // Store the ongoing fetch promise
178
+ this.fetchPromises.set(cacheKey, newPromise);
179
+
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
+ }
190
+
191
+
192
+ /**
193
+ * Clears all cache entries stored in the cache storage.
194
+ * @returns {Promise<void>} A promise indicating cache flush completion.
195
+ */
196
+
197
+ this.flushCache = () => {
198
+ return caches.open(this.cacheName).then(cache => {
199
+ return cache.keys().then(keys => {
200
+ const deletionPromises = keys.map(key => cache.delete(key));
201
+ return Promise.all(deletionPromises);
202
+ });
203
+ });
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);
289
+
290
+ }
@@ -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.removeChild(script);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bv-ui-core",
3
- "version": "2.8.2",
3
+ "version": "2.9.2",
4
4
  "license": "Apache 2.0",
5
5
  "description": "Bazaarvoice UI-related JavaScript",
6
6
  "repository": {
@@ -0,0 +1,213 @@
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
+ return Promise.resolve(mockResponse)
69
+ },
70
+ put: (key, response) => {
71
+ cacheStorage.set(key, response);
72
+ return Promise.resolve();
73
+ }
74
+ });
75
+
76
+ // Simulate that the response is cached
77
+ bvFetchInstance.cachedUrls.add(cacheKey);
78
+
79
+ // Call the function under test
80
+ bvFetchInstance.bvFetchFunc(url, options)
81
+ .then(response => {
82
+ // Check if response is fetched from cache
83
+ expect(response).to.not.be.null;
84
+
85
+ // Check if response is cached
86
+ const cachedResponse = cacheStorage.get(cacheKey);
87
+ expect(cachedResponse).to.not.be.null;
88
+
89
+ // Check if caches.open was called
90
+ expect(cacheStub.called).to.be.true;
91
+
92
+ done();
93
+ })
94
+ .catch(error => {
95
+ done(error); // Call done with error if any
96
+ })
97
+ });
98
+
99
+
100
+ it('should fetch from network when response is not cached', function (done) {
101
+ const url = 'https://jsonplaceholder.typicode.com/todos';
102
+ const options = {};
103
+
104
+ const matchSpy = sinon.spy((key) => {
105
+ expect(key).to.equal(cacheKey);
106
+ Promise.resolve(null)
107
+ });
108
+ caches.open.resolves({
109
+ match: matchSpy,
110
+ put: (key, response) => {
111
+ cacheStorage.set(key, response);
112
+ return Promise.resolve();
113
+ }
114
+ });
115
+
116
+
117
+ bvFetchInstance.bvFetchFunc(url, options)
118
+ .then(response => {
119
+ // Check if response is fetched from network
120
+ expect(response).to.not.be.null;
121
+ console.log(response.body)
122
+
123
+ // Check if caches.match was called
124
+ expect(matchSpy.called).to.be.false;
125
+
126
+ done();
127
+ })
128
+ .catch(done);
129
+ });
130
+
131
+ it('should not cache response when there is an error', function (done) {
132
+ const url = 'https://jsonplaceholder.typicode.com/todos';
133
+ const options = {};
134
+
135
+ // Define shouldCache directly in bvFetchInstance
136
+ bvFetchInstance.shouldCache = (res) => {
137
+ return false
138
+ };
139
+
140
+ bvFetchInstance.bvFetchFunc(url, options)
141
+ .then(response => {
142
+ // Check if response is fetched from network
143
+ expect(response).to.not.be.null;
144
+ console.log(response.body)
145
+
146
+ // Check if caches.match was called
147
+ expect(cacheStub.calledOnce).to.be.false;
148
+
149
+ // Check if response is not cached
150
+ const cachedResponse = cacheStorage.get(url);
151
+ expect(cachedResponse).to.be.undefined;
152
+
153
+ done();
154
+ })
155
+ .catch(done);
156
+ });
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
+ });
212
+
213
+ });