Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

bv-ui-core

Package Overview
Dependencies
Maintainers
10
Versions
49
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bv-ui-core - npm Package Compare versions

Comparing version 2.9.1 to 2.9.2

304

lib/bvFetch/index.js

@@ -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);
});
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc