bv-ui-core
Advanced tools
Comparing version 2.9.1 to 2.9.2
@@ -9,6 +9,8 @@ | ||
module.exports = function BvFetch ({ shouldCache, cacheName }) { | ||
module.exports = function BvFetch ({ shouldCache, cacheName, cacheLimit }) { | ||
this.shouldCache = shouldCache; | ||
this.cacheName = cacheName || 'bvCache'; | ||
this.cacheLimit = cacheLimit * 1024 * 1024 || 10 * 1024 * 1024; | ||
this.fetchPromises = new Map(); | ||
this.cachedUrls = new Set(); | ||
@@ -29,2 +31,124 @@ /** | ||
/** | ||
* Retrieves cached URLs from the cache storage associated with the provided cache name. | ||
* @returns {void} | ||
*/ | ||
this.retrieveCachedUrls = () => { | ||
// Open the Cache Storage | ||
caches.open(this.cacheName).then(cache => { | ||
// Get all cache keys | ||
cache.keys().then(keys => { | ||
keys.forEach(request => { | ||
this.cachedUrls.add(request.url); | ||
}); | ||
}); | ||
}); | ||
} | ||
//callretrieveCachedUrls function to set the cache URL set with the cached URLS | ||
this.retrieveCachedUrls(); | ||
/** | ||
* Fetches data from the specified URL, caches the response, and returns the response. | ||
* @param {string} url - The URL from which to fetch data. | ||
* @param {string} cacheKey - The cache key associated with the fetched data. | ||
* @returns {Promise<Response>} A Promise that resolves with the fetched response. | ||
* @throws {Error} Throws an error if there's any problem fetching the data. | ||
*/ | ||
this.fetchDataAndCache = (url, options = {}, cacheKey) => { | ||
return fetch(url,options) | ||
.then((response) => { | ||
// initiate caching of response and return the response | ||
this.cacheData(response, cacheKey); | ||
return response.clone(); | ||
}) | ||
.catch(function (error) { | ||
throw new Error('Error fetching data: ' + error); | ||
}); | ||
} | ||
/** | ||
* Caches the provided response with the specified cache key if it meets the criteria for caching. | ||
* @param {Response} response - The response object to be cached. | ||
* @param {string} cacheKey - The cache key associated with the response. | ||
* @returns {void} | ||
*/ | ||
this.cacheData = (response, cacheKey) => { | ||
const errJson = response.clone(); | ||
let canBeCached = true; | ||
// Check for error in response obj | ||
errJson.json().then(json => { | ||
if (typeof this.shouldCache === 'function') { | ||
canBeCached = this.shouldCache(json); | ||
} | ||
}).then(() => { | ||
if (canBeCached) { | ||
const clonedResponse = response.clone() | ||
const newHeaders = new Headers(); | ||
clonedResponse.headers.forEach((value, key) => { | ||
newHeaders.append(key, value); | ||
}); | ||
newHeaders.append('X-Bazaarvoice-Cached-Time', Date.now()) | ||
// Get response text to calculate its size | ||
clonedResponse.text().then(text => { | ||
// Calculate size of response text in bytes | ||
const sizeInBytes = new Blob([text]).size; | ||
// Append response size to headers | ||
newHeaders.append('X-Bazaarvoice-Response-Size', sizeInBytes); | ||
// Create new Response object with modified headers | ||
const newResponse = new Response(clonedResponse._bodyBlob || clonedResponse.body, { | ||
status: clonedResponse.status, | ||
statusText: clonedResponse.statusText, | ||
headers: newHeaders | ||
}); | ||
// Cache the response | ||
caches.open(this.cacheName).then(cache => { | ||
cache.put(cacheKey, newResponse); | ||
//add key to cachedUrls set | ||
this.cachedUrls.add(cacheKey); | ||
}); | ||
}); | ||
} | ||
}) | ||
} | ||
/** | ||
* Function to fetch data from cache. | ||
* @param {string} cacheKey - The cache key to fetch data from the cache. | ||
* @returns {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. | ||
* @throws {Error} Throws an error if there's any problem fetching from cache. | ||
*/ | ||
this.fetchFromCache = (cacheKey) => { | ||
// Check if the URL is in the set of cached URLs | ||
if (!this.cachedUrls.has(cacheKey)) { | ||
return Promise.resolve(null); | ||
} | ||
// Open the cache and try to match the URL | ||
return caches.open(this.cacheName) | ||
.then((cache) => { | ||
return cache.match(cacheKey) | ||
.then((cachedResponse) => { | ||
const cachedTime = cachedResponse.headers.get('X-Bazaarvoice-Cached-Time'); | ||
const ttl = cachedResponse.headers.get('Cache-Control').match(/max-age=(\d+)/)[1]; | ||
const currentTimestamp = Date.now(); | ||
const cacheAge = (currentTimestamp - cachedTime) / 1000; | ||
if (cacheAge < ttl) { | ||
// Cached response found | ||
return cachedResponse.clone(); | ||
} | ||
}) | ||
}) | ||
.catch((error) => { | ||
throw new Error('Error fetching from cache: ' + error); | ||
}); | ||
} | ||
/** | ||
* Fetches data from the API endpoint, caches responses, and handles caching logic. | ||
@@ -37,81 +161,35 @@ * @param {string} url - The URL of the API endpoint. | ||
this.bvFetchFunc = (url, options = {}) => { | ||
// get the key | ||
const cacheKey = this.generateCacheKey(url, options); | ||
// If an ongoing fetch promise exists for the URL, return it | ||
if (this.fetchPromises.has(cacheKey)) { | ||
return this.fetchPromises.get(cacheKey).then(res => res.clone()); | ||
} | ||
// check if its available in the cache | ||
return caches.open(this.cacheName) | ||
.then(currentCache => currentCache.match(cacheKey)) | ||
.then(cachedResponse => { | ||
// Check if response is available in cache | ||
const newPromise = this.fetchFromCache(cacheKey) | ||
.then((cachedResponse) => { | ||
// If response found in cache, return it | ||
if (cachedResponse) { | ||
const cachedTime = cachedResponse.headers.get('X-Cached-Time'); | ||
const ttl = cachedResponse.headers.get('Cache-Control').match(/max-age=(\d+)/)[1]; | ||
const currentTimestamp = Date.now(); | ||
const cacheAge = (currentTimestamp - cachedTime) / 1000; | ||
if (cacheAge < ttl) { | ||
// Cached response found | ||
return cachedResponse.clone(); | ||
} | ||
return cachedResponse; | ||
} | ||
// If response not found in cache, fetch from API and cache it | ||
return this.fetchDataAndCache(url, options, cacheKey); | ||
}); | ||
// check if there is an ongoing promise | ||
if (this.fetchPromises.has(cacheKey)) { | ||
return this.fetchPromises.get(cacheKey).then(res => res.clone()); | ||
} | ||
// Store the ongoing fetch promise | ||
this.fetchPromises.set(cacheKey, newPromise); | ||
// Make a new call | ||
const newPromise = fetch(url, options); | ||
//initiate cache cleanUp | ||
this.debounceCleanupExpiredCache(); | ||
// Push the newPromise to the fetchPromises Map | ||
this.fetchPromises.set(cacheKey, newPromise); | ||
// When fetch completes or fails, remove the promise from the store | ||
newPromise.finally(() => { | ||
this.fetchPromises.delete(cacheKey); | ||
}); | ||
return newPromise | ||
.then(response => { | ||
const clonedResponse = response.clone(); | ||
const errJson = clonedResponse.clone() | ||
let canBeCached = true; | ||
return errJson.json().then(json => { | ||
if (typeof this.shouldCache === 'function') { | ||
canBeCached = this.shouldCache(json); | ||
} | ||
return response | ||
}).then(res => { | ||
if (canBeCached) { | ||
const newHeaders = new Headers(); | ||
clonedResponse.headers.forEach((value, key) => { | ||
newHeaders.append(key, value); | ||
}); | ||
newHeaders.append('X-Cached-Time', Date.now()); | ||
return newPromise.then(res => res.clone()); | ||
} | ||
const newResponse = new Response(clonedResponse._bodyBlob, { | ||
status: clonedResponse.status, | ||
statusText: clonedResponse.statusText, | ||
headers: newHeaders | ||
}); | ||
//Delete promise from promise map once its resolved | ||
this.fetchPromises.delete(cacheKey); | ||
return caches.open(this.cacheName) | ||
.then(currentCache => | ||
currentCache.put(cacheKey, newResponse) | ||
) | ||
.then(() => res); | ||
} | ||
else { | ||
//Delete promise from promise map if error exists | ||
this.fetchPromises.delete(cacheKey); | ||
return res | ||
} | ||
}); | ||
}) | ||
}) | ||
.catch(err => { | ||
// Remove the promise that was pushed earlier | ||
this.fetchPromises.delete(cacheKey); | ||
throw err; | ||
}); | ||
}; | ||
/** | ||
@@ -130,3 +208,87 @@ * Clears all cache entries stored in the cache storage. | ||
}; | ||
this.manageCache = () => { | ||
// Delete expired cache entries | ||
caches.open(this.cacheName).then(cache => { | ||
cache.keys().then(keys => { | ||
keys.forEach(key => { | ||
cache.match(key).then(response => { | ||
const cachedTime = response.headers.get('X-Bazaarvoice-Cached-Time'); | ||
const ttl = response.headers.get('Cache-Control').match(/max-age=(\d+)/)[1]; | ||
const currentTimestamp = Date.now(); | ||
const cacheAge = (currentTimestamp - cachedTime) / 1000; | ||
if (cacheAge >= ttl) { | ||
cache.delete(key); | ||
this.cachedUrls.delete(key); | ||
} | ||
}); | ||
}); | ||
}); | ||
}); | ||
// Calculate total size of cached responses | ||
let totalSize = 0; | ||
caches.open(this.cacheName).then(cache => { | ||
cache.keys().then(keys => { | ||
// Create an array of promises for cache match operations | ||
const matchPromises = keys.map(key => | ||
cache.match(key).then(response => { | ||
const sizeHeader = response.headers.get('X-Bazaarvoice-Response-Size'); | ||
return parseInt(sizeHeader, 10); | ||
}) | ||
); | ||
// wait for all match promises to resolve | ||
return Promise.all(matchPromises) | ||
.then(sizes => sizes.reduce((acc, size) => acc + size, 0)); | ||
}).then(size => { | ||
totalSize = size; | ||
// If total size exceeds 10 MB, delete old cache entries | ||
if (totalSize > this.cacheLimit) { | ||
// create an array of cached responses | ||
const cacheEntries = []; | ||
return cache.keys().then(keys => { | ||
const cachesResEntries = keys.map(key => | ||
cache.match(key).then(response => { | ||
const sizeHeader = response.headers.get('X-Bazaarvoice-Response-Size'); | ||
const lastAccessedTime = response.headers.get('X-Bazaarvoice-Cached-Time'); | ||
cacheEntries.push({ key, size: parseInt(sizeHeader, 10), lastAccessedTime }); | ||
}) | ||
); | ||
return Promise.all(cachesResEntries) | ||
.then(() => { | ||
// Sort cache entries by last accessed time in ascending order | ||
cacheEntries.sort((a, b) => a.lastAccessedTime - b.lastAccessedTime); | ||
// Delete older cache entries until total size is under 10 MB | ||
let currentSize = totalSize; | ||
cacheEntries.forEach(entry => { | ||
if (currentSize > this.cacheLimit) { | ||
cache.delete(entry.key); | ||
this.cachedUrls.delete(entry.key); | ||
currentSize -= entry.size; | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
}); | ||
}); | ||
}; | ||
function debounce (func, delay) { | ||
let timer; | ||
return function () { | ||
clearTimeout(timer); | ||
timer = setTimeout(() => { | ||
func.apply(this, arguments); | ||
}, delay); | ||
}; | ||
} | ||
this.debounceCleanupExpiredCache = debounce(this.manageCache, 8000); | ||
} | ||
} |
@@ -10,8 +10,7 @@ # BvFetch | ||
`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. | ||
`cacheName (String):` Optional. Specifies the name of the cache to be used. If not provided, the default cache name 'bvCache' will be used. | ||
`cacheLimit (Integer)`: Optional. Specifies the cache size limit for the cache storage. Its value should be in MB. Default value is 10 MB. | ||
## bvFetchFunc Method Parameters | ||
`url (String):` The URL of the API endpoint to fetch data from. | ||
`options (Object):` Optional request options such as headers, method, etc., as supported by the Fetch API. | ||
@@ -22,9 +21,55 @@ | ||
## generateCacheKey Method Parameters: | ||
`url (String):` The URL of the API endpoint. | ||
`options (Object):` Optional request options. | ||
## generateCacheKey Return Value: | ||
`string:` The generated cache key. | ||
## retrieveCachedUrls Method | ||
Retrieves cached URLs from the cache storage associated with the provided cache name. | ||
## retrieveCachedUrls Parameters | ||
This method takes no parameters. | ||
## retrieveCachedUrls Return Value | ||
`void:` This method does not return anything. | ||
## fetchDataAndCache Method | ||
Fetches data from the specified URL, caches the response, and returns the response. | ||
## Parameters | ||
`url (String):` The URL from which to fetch data. | ||
`options (Object):` Optional request options such as headers, method, etc., as supported by the | ||
Fetch API. | ||
`cacheKey (String):` | ||
The cache key associated with the fetched data. | ||
## Return Value | ||
`Promise<Response>:` A promise that resolves to the fetched response. | ||
## fetchFromCache Method | ||
Function to fetch data from cache. | ||
## Parameters | ||
`cacheKey (String):` The cache key to fetch data from the cache. | ||
## Return Value | ||
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. | ||
## cacheData Method | ||
Caches the provided response with the specified cache key if it meets the criteria for caching. | ||
## Parameters | ||
`response (Response):` The response object to be cached. | ||
`cacheKey (String):` The cache key associated with the response. | ||
## Return Value | ||
`void:` This method does not return anything. | ||
## flushCache Method Parameters | ||
This method takes no parameters. | ||
## flushCache Return Value | ||
`Promise<void>:` A promise indicating the completion of cache flush operation. | ||
## manageCache Method | ||
Manages the cache by deleting expired cache entries and maintaining the cache size limit. | ||
## Parameters | ||
This method takes no parameters. | ||
## Return Value | ||
`void:` This method does not return anything. | ||
## Usage with of `BvFetch`: | ||
@@ -31,0 +76,0 @@ |
{ | ||
"name": "bv-ui-core", | ||
"version": "2.9.1", | ||
"version": "2.9.2", | ||
"license": "Apache 2.0", | ||
@@ -5,0 +5,0 @@ "description": "Bazaarvoice UI-related JavaScript", |
@@ -68,3 +68,3 @@ //Imports | ||
expect(key).to.equal(cacheKey); | ||
Promise.resolve(mockResponse) | ||
return Promise.resolve(mockResponse) | ||
}, | ||
@@ -77,2 +77,6 @@ put: (key, response) => { | ||
// Simulate that the response is cached | ||
bvFetchInstance.cachedUrls.add(cacheKey); | ||
// Call the function under test | ||
bvFetchInstance.bvFetchFunc(url, options) | ||
@@ -96,4 +100,4 @@ .then(response => { | ||
}); | ||
it('should fetch from network when response is not cached', function (done) { | ||
@@ -103,9 +107,8 @@ const url = 'https://jsonplaceholder.typicode.com/todos'; | ||
const cacheKey = bvFetchInstance.generateCacheKey(url, options); | ||
const matchSpy = sinon.spy((key) => { | ||
expect(key).to.equal(cacheKey); | ||
Promise.resolve(null) | ||
}); | ||
caches.open.resolves({ | ||
match: (key) => { | ||
expect(key).to.equal(cacheKey); | ||
Promise.resolve(null) | ||
}, | ||
match: matchSpy, | ||
put: (key, response) => { | ||
@@ -125,3 +128,3 @@ cacheStorage.set(key, response); | ||
// Check if caches.match was called | ||
expect(cacheStub.called).to.be.true; | ||
expect(matchSpy.called).to.be.false; | ||
@@ -141,3 +144,3 @@ done(); | ||
}; | ||
bvFetchInstance.bvFetchFunc(url, options) | ||
@@ -150,3 +153,3 @@ .then(response => { | ||
// Check if caches.match was called | ||
expect(cacheStub.calledOnce).to.be.true; | ||
expect(cacheStub.calledOnce).to.be.false; | ||
@@ -162,3 +165,57 @@ // Check if response is not cached | ||
it('should delete cache when size is greater than 10 MB', function (done) { | ||
// Mock cache entries exceeding 10 MB | ||
const mockCacheEntries = [ | ||
{ key: 'key1', size: 6000000 }, // 6 MB | ||
{ key: 'key2', size: 6000000 } // 6 MB | ||
// Add more entries as needed to exceed 10 MB | ||
]; | ||
// Stub cache operations | ||
const deleteSpy = sinon.spy( | ||
(key) => { | ||
const index = mockCacheEntries.findIndex(entry => entry.key === key); | ||
if (index !== -1) { | ||
mockCacheEntries.splice(index, 1); // Delete entry from mock cache entries | ||
} | ||
return Promise.resolve(true); | ||
} | ||
) | ||
caches.open.resolves({ | ||
keys: () => Promise.resolve(mockCacheEntries.map(entry => entry.key)), | ||
match: (key) => { | ||
const entry = mockCacheEntries.find(entry => entry.key === key); | ||
if (entry) { | ||
return Promise.resolve({ | ||
headers: new Headers({ | ||
'X-Bazaarvoice-Response-Size': entry.size.toString(), | ||
'X-Bazaarvoice-Cached-Time': Date.now(), | ||
'Cache-Control': 'max-age=3600' | ||
}) | ||
}); | ||
} | ||
else { | ||
return Promise.resolve(null); | ||
} | ||
}, | ||
delete: deleteSpy | ||
}); | ||
// Create a new instance of BvFetch | ||
const bvFetchInstance = new BvFetch({ shouldCache: true }); | ||
// Call manageCache function | ||
bvFetchInstance.manageCache() | ||
setTimeout(() => { | ||
// Ensure cache deletion occurred until the total size is under 10 MB | ||
const totalSizeAfterDeletion = mockCacheEntries.reduce((acc, entry) => acc + entry.size, 0); | ||
expect(totalSizeAfterDeletion).to.be.at.most(10 * 1024 * 1024); // Total size should be under 10 MB | ||
// Ensure cache.delete was called for each deleted entry | ||
expect(deleteSpy.called).to.be.true; | ||
expect(deleteSpy.callCount).to.equal(mockCacheEntries.length); | ||
done(); | ||
}, 500); | ||
}); | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
263726
6141