Skip to content

Commit

Permalink
Bv fetch module (#136)
Browse files Browse the repository at this point in the history
* adding new module, bvFetch

* adding bvFetch module

* changes to the err callback implementation

* updating module structure

* adding function description

* adding error unit test case

* updating test cases and readme

* updating README

* updating bv-ui-core README

* changing callback name

* updating readme

* 2.9.0

* updating test case

* updating test case
  • Loading branch information
VarshaAdigaBV authored Mar 25, 2024
1 parent 1970c92 commit d9b40ba
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ module's directory.
## Modules

- [body](./lib/body)
- [bvFetch](./lib/bvFetch/)
- [checkHighContrast](./lib/checkHighContrast)
- [cookie](./lib/cookie)
- [cookieConsent](./lib/cookieConsent)
Expand Down
47 changes: 47 additions & 0 deletions lib/bvFetch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# BvFetch

The BvFetch module provides methods to cache duplicate API calls and interact with the cacheStorage


## The following methods are provided:

## BvFetch Parameters
`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.

## 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.

## bvFetchFunc Return Value
`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.

## flushCache Method Parameters
This method takes no parameters.

## flushCache Return Value
`Promise<void>:` A promise indicating the completion of cache flush operation.


## Usage with of `BvFetch`:

```js
var BvFetch = require('bv-ui-core/lib/bvFetch')

// Initialize BV Fetch instance
const bvFetch = new BVFetch({
canBeCached: canBeCached, // optional
cacheName: "bvCache" // optional, default is "bvCache"
});

// Make API calls using bvFetchFunc method
bvFetch.bvFetchFunc('https://api.example.com/data')
.then(response => {
// Handle response
})
.catch(error => {
// Handle error
});
```
128 changes: 128 additions & 0 deletions lib/bvFetch/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@

/**
* @fileOverview
* Provides api response caching utilties
*/

const { fetch } = require('../polyfills/fetch')

module.exports = function BvFetch ({ shouldCache, cacheName }) {
this.shouldCache = shouldCache;
this.cacheName = cacheName || 'bvCache';
this.fetchPromises = new Map();

/**
* Generates a unique cache key for the given URL and options.
* @param {string} url - The URL of the API endpoint.
* @param {Object} options - Optional request options.
* @returns {string} The generated cache key.
*/

this.generateCacheKey = (url, options) => {
const optionsString = (Object.keys(options).length > 0) ? JSON.stringify(options) : '';
const key = url + optionsString;
return key;
};

/**
* Fetches data from the API endpoint, caches responses, and handles caching logic.
* @param {string} url - The URL of the API endpoint.
* @param {Object} options - Optional request options.
* @returns {Promise<Response>} A promise resolving to the API response.
*/

this.bvFetchFunc = (url, options = {}) => {
// get the key
const cacheKey = this.generateCacheKey(url, options);

// check if its available in the cache
return caches.open(this.cacheName)
.then(currentCache => currentCache.match(cacheKey))
.then(cachedResponse => {
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();
}
}

// check if there is an ongoing promise
if (this.fetchPromises.has(cacheKey)) {
return this.fetchPromises.get(cacheKey);
}

// Make a new call
const newPromise = fetch(url, options);

// Push the newPromise to the fetchPromises Map
this.fetchPromises.set(cacheKey, newPromise);

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());

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;
});
};

/**
* Clears all cache entries stored in the cache storage.
* @returns {Promise<void>} A promise indicating cache flush completion.
*/

this.flushCache = () => {
return caches.open(this.cacheName).then(cache => {
return cache.keys().then(keys => {
const deletionPromises = keys.map(key => cache.delete(key));
return Promise.all(deletionPromises);
});
});
};

}
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "bv-ui-core",
"version": "2.8.2",
"version": "2.9.0",
"license": "Apache 2.0",
"description": "Bazaarvoice UI-related JavaScript",
"repository": {
Expand Down
156 changes: 156 additions & 0 deletions test/unit/bvFetch/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//Imports

var BvFetch = require('../../../lib/bvFetch');

describe('BvFetch', function () {
let bvFetchInstance;
let cacheStub;
let cacheStorage;

beforeEach(function () {
bvFetchInstance = new BvFetch({
shouldCache: null,
cacheName: 'testCache'
});

// Define cacheStorage as a Map
cacheStorage = new Map();

// Stubbing caches.open
cacheStub = sinon.stub(caches, 'open').resolves({
match: key => {
const cachedResponse = cacheStorage.get(key);
return Promise.resolve(cachedResponse);
},
put: (key, response) => {
cacheStorage.set(key, response);
return Promise.resolve();
}
});

});

afterEach(function () {
bvFetchInstance = null;
// Restore the original method after each test
caches.open.restore();
});

it('should generate correct cache key', function () {
const url = 'https://jsonplaceholder.typicode.com/todos';
const options = {};
const expectedKey = 'https://jsonplaceholder.typicode.com/todos';
const generatedKey = bvFetchInstance.generateCacheKey(url, options);
expect(generatedKey).to.equal(expectedKey);
});


it('should fetch from cache when the response is cached', function (done) {
const url = 'https://jsonplaceholder.typicode.com/todos';
const options = {};

// Mocking cache response
const mockResponse = new Response('Mock Data', {
status: 200,
statusText: 'OK',
headers: {
'Cache-Control': 'max-age=3600',
'X-Cached-Time': Date.now()
}
});

const cacheKey = bvFetchInstance.generateCacheKey(url, options);

// Overriding the stub for this specific test case
caches.open.resolves({
match: (key) => {
expect(key).to.equal(cacheKey);
Promise.resolve(mockResponse)
},
put: (key, response) => {
cacheStorage.set(key, response);
return Promise.resolve();
}
});

bvFetchInstance.bvFetchFunc(url, options)
.then(response => {
// Check if response is fetched from cache
expect(response).to.not.be.null;

// Check if response is cached
const cachedResponse = cacheStorage.get(cacheKey);
expect(cachedResponse).to.not.be.null;

// Check if caches.open was called
expect(cacheStub.called).to.be.true;

done();
})
.catch(error => {
done(error); // Call done with error if any
})
});


it('should fetch from network when response is not cached', function (done) {
const url = 'https://jsonplaceholder.typicode.com/todos';
const options = {};

const cacheKey = bvFetchInstance.generateCacheKey(url, options);

caches.open.resolves({
match: (key) => {
expect(key).to.equal(cacheKey);
Promise.resolve(null)
},
put: (key, response) => {
cacheStorage.set(key, response);
return Promise.resolve();
}
});


bvFetchInstance.bvFetchFunc(url, options)
.then(response => {
// Check if response is fetched from network
expect(response).to.not.be.null;
console.log(response.body)

// Check if caches.match was called
expect(cacheStub.called).to.be.true;

done();
})
.catch(done);
});

it('should not cache response when there is an error', function (done) {
const url = 'https://jsonplaceholder.typicode.com/todos';
const options = {};

// Define shouldCache directly in bvFetchInstance
bvFetchInstance.shouldCache = (res) => {
return false
};

bvFetchInstance.bvFetchFunc(url, options)
.then(response => {
// Check if response is fetched from network
expect(response).to.not.be.null;
console.log(response.body)

// Check if caches.match was called
expect(cacheStub.calledOnce).to.be.true;

// Check if response is not cached
const cachedResponse = cacheStorage.get(url);
expect(cachedResponse).to.be.undefined;

done();
})
.catch(done);
});


});

0 comments on commit d9b40ba

Please sign in to comment.