{"id":200,"date":"2016-05-13T10:09:05","date_gmt":"2016-05-13T10:09:05","guid":{"rendered":"http:\/\/muthu.co\/?p=200"},"modified":"2021-01-02T14:05:50","modified_gmt":"2021-01-02T14:05:50","slug":"client-side-caching-of-api-responses","status":"publish","type":"post","link":"http:\/\/write.muthu.co\/client-side-caching-of-api-responses\/","title":{"rendered":"Client side caching of API responses"},"content":{"rendered":"

Talk about caching and we talk about how we are going to\u00a0put everything into redis or memcache on our servers and send back cached responses to our\u00a0clients and make our applications respond\u00a0faster by taking the load off the databases. Below is a flow of how a typical caching\u00a0mechanism\u00a0looks like.<\/p>\n

\"Client<\/a>
Client Server with Cache<\/figcaption><\/figure>\n

While server caching is a phenomenal way of improving the response time but\u00a0we still have to\u00a0serve the responses whether we hit the database or not. The client requests still reach our application servers, be understood, sent to the appropriate handlers, processed, checked for the availability in cache, response created and sent back. So the \u00a0scenario when our request goes from client to cache server and comes back looks something like this.<\/p>\n

\"Data<\/a>
Data Flow from Client to Server to Client with Server Cache<\/figcaption><\/figure>\n

If you take a good look at your API calls, you would notice that some of the requests are just redundant and gets called on\u00a0refresh or on\u00a0all pages. A good high performing system would not only take\u00a0the load off the databases but also off the application servers. The way to do that would be to stop\u00a0making duplicate requests to the\u00a0application servers by caching the\u00a0responses in the\u00a0client itself. You may store the data in\u00a0localStorage, sessionStorage or cookies<\/a> as per the needs of your application.<\/p>\n

\"Client<\/a>
Client side cache system<\/figcaption><\/figure>\n

One of the applications we are working on had a similar problem. We were making multiple calls to the\u00a0same route on every page. The APIs were\u00a0called by our directives to load the data into its\u00a0select box. We cannot avoid\u00a0those calls because they were\u00a0necessary but since they were redundant we came up with our own client side caching system.<\/p>\n

Here is a snapshot of the network tab\u00a0after navigating through 3 pages without client side cache.<\/p>\n

\"Network<\/a>
Network tab showing redundant multiple API calls. The colors showing duplicates.<\/figcaption><\/figure>\n

Below is a snapshot of the network tab after navigating through the same\u00a03\u00a0pages but this time with client side cache enabled.<\/p>\n

\"Network<\/a>
Network tab snapshot after enabling client side cache<\/figcaption><\/figure>\n

The API calls almost halved and the\u00a0page load is faster\u00a0now.<\/p>\n

The caching system<\/h3>\n

To make sure we don’t touch any of our existing client side code and still be able to implement the caching system we wrote listeners for our http requests by overriding the\u00a0XMLHttpRequest<\/code> open<\/code> and send<\/code> method.\u00a0So basically making sure that all GET requests go through our overridden open method and all the responses to APIs that we intend to cache gets cached.<\/p>\n

This is how we used to call our API previously,<\/p>\n

\/\/Our old API call\r\nApiService.getAll('subject-types').then(function(response) {\r\n      scope.subjectTypes = response.data;\r\n});<\/pre>\n

And now we call it with an extra parameter.<\/p>\n

\/\/Our new API call\r\nApiService.getAll('subject-types', true).then(function(response) {\r\n      scope.subjectTypes = response.data;\r\n});<\/pre>\n

the true<\/code> passed into the method tells the method that this API call needs to be cached. This is what the getAll<\/code> method looks like in the APIService<\/code>.<\/p>\n

\/**\r\n     * \r\n     * @param  {[type]} routeprefix - API url\r\n     * @param  {[type]} cacheit - Wether this API needs to be cached or not\r\n     * @param  {[type]} urlsthatwillaffectthisget - the APIs that will effect the state of the cached data of this API\r\n     * \r\n     *\/\r\n    function getAll(routeprefix, cacheit, urlsthatwillaffectthisget) {\r\n        var url = '\/api\/' + routeprefix;\r\n        if (cacheit) {\r\n            apicache.register(url, urlsthatwillaffectthisget);\r\n        }\r\n        var cacheddata = apicache.get(url);\r\n        if (cacheddata) {\r\n            if(window.debug){\r\n                console.info(\"data from cache\", url);\r\n            }\r\n            return new Promise(function(resolve, reject) {\r\n                resolve({\r\n                    \"data\": cacheddata\r\n                });\r\n            });\r\n        } else {\r\n            return $http({\r\n                method: 'GET',\r\n                url: '\/api\/' + routeprefix\r\n            });\r\n        }\r\n    }<\/pre>\n

As you can see in the code, I am checking if the\u00a0API needs to be cached, if yes then registering a listener for it.<\/p>\n

Whatever goes inside cache may change in the server when we add, update or delete a\u00a0record. In that case we may have to delete the cached information so that we can get the new value. To make that happen we need to identify the API calls that effect the stored value, this is where our third parameter\u00a0urlsthatwillaffectthisget<\/code> comes into picture. Take a look at the code below.<\/p>\n

var urlthataffecthisgetroute = ['students\/{{id}}\/academics\/{{id}}','students\/{{id}}\/academic'];\/\/one save and one put\r\n\r\nApiService.getAll('students\/' + studentId + '\/academics\/' + studentAcademicId, true, urlthataffecthisgetroute)\r\n.then(function(response) {\r\n      scope.academic = response.data;\r\n);\r\n<\/pre>\n

We do not know what will be the exact PUT or DELETE urls, so we simply provide the url signature with UUIDs as {{id}}. So a match of this URL call will automatically delete the stored key from the client storage. If there is no third parameter then its assumed that there is no other API change affecting this value, so it will not change throughout the user session.<\/p>\n

The full caching module code is as below.<\/p>\n

\/**\r\n * API Response Caching System\r\n * This module creates a global variable called 'apicache' and makes it available throughout.\r\n *\/\r\n(function() {\r\n\r\n    \/**\r\n     * \r\n     * Variables to store our XMLHttpRequest object methods\r\n     *\/\r\n    var open = window.XMLHttpRequest.prototype.open,\r\n        send = window.XMLHttpRequest.prototype.send,\r\n        onReadyStateChange;\r\n\r\n    \/**\r\n     * The prefix helps in keeping our keys unique making sure it doesn't conflict with the other keys\r\n     *\/\r\n    var _keyprefix = \"_storagekey_\";\r\n\r\n    \/**\r\n     * The regex to replace all my UUIDs to some other more easily storable format\r\n     *\/\r\n    var uuid_regex = \/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/g;\r\n\r\n    \/**\r\n     * XMLHttpRequest object\r\n     *\/\r\n    var xmlhttp = window.XMLHttpRequest;\r\n\r\n    var url;\r\n\r\n    \/**\r\n     * Method that overrides our XMLHttpRequest open method\r\n     * \r\n     * @param  String method GET,POST,PUT,DELETE etc\r\n     * @param  String url    API url\r\n     * @param  String async  sync or async\r\n     *\/\r\n    function openReplacement(method, url, async) {\r\n        var syncMode = async !== false ? 'async' : 'sync';\r\n        this.url = url;\r\n        return open.apply(this, arguments);\r\n    }\r\n\r\n    \/**\r\n     * Method that overrides our XMLHttpRequest send method\r\n     * \r\n     * @param  Object data object passed as payload\r\n     * \r\n     *\/\r\n    function sendReplacement(data) {\r\n        \/\/console.log('Sending HTTP request data : ', data);\r\n\r\n        if (this.onreadystatechange) {\r\n            this._onreadystatechange = this.onreadystatechange;\r\n        }\r\n        this.onreadystatechange = onReadyStateChangeReplacement;\r\n        return send.apply(this, arguments);\r\n    }\r\n\r\n    \/**\r\n     * Method thats called when the response is recived from the server\r\n     *\/\r\n    function onReadyStateChangeReplacement() {\r\n        \/\/console.log('HTTP request ready state changed : ' + this.readyState);\r\n        \/\/console.log(this.responseText);\r\n        if (this.status == 200) {\r\n            \/\/store only if the route returns success\r\n            storeIntoCache(this.url, this.responseText);\r\n        }\r\n        if (this._onreadystatechange) {\r\n            return this._onreadystatechange.apply(this, arguments);\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Store data into cache. url as key and responseText as value\r\n     *\/\r\n    function storeIntoCache(url, responseText) {\r\n        if (urlRegisteredForCache(url)) {\r\n            sessionStorage.setItem(_keyprefix + url, responseText);\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Get From Cache using the URL as key. \r\n     *\/\r\n    function getFromCache(url) {\r\n        return JSON.parse(sessionStorage.getItem(_keyprefix + url));\r\n    }\r\n\r\n    function urlRegisteredForCache(url) {\r\n        var getroutes_tocache = JSON.parse(sessionStorage.getItem('getroutes_tocache'))\r\n        if (getroutes_tocache && getroutes_tocache.indexOf(url) > -1) {\r\n            return true;\r\n        }\r\n        return false;\r\n    }\r\n\r\n    function dataCachedInStorage(url) {\r\n        return sessionStorage.getItem(_keyprefix + url);\r\n    }\r\n\r\n    \/\/Overriding\r\n    window.XMLHttpRequest.prototype.open = openReplacement;\r\n    window.XMLHttpRequest.prototype.send = sendReplacement;\r\n\r\n    function getstoredgetroutes() {\r\n        return JSON.parse(sessionStorage.getItem('getroutes_tocache'));\r\n    }\r\n\r\n    \/\/We create our Cache class \r\n    function Apicache() {\r\n\r\n    }\r\n\r\n    \/\/get data from cache\r\n    Apicache.prototype.get = function(url) {\r\n        return JSON.parse(sessionStorage.getItem(_keyprefix + url));\r\n    }\r\n\r\n    \/\/delete data from cache\r\n    function remove(geturl) {\r\n        sessionStorage.removeItem(_keyprefix + geturl);\r\n        if (window.debug) {\r\n            console.info(\"data removed from cache\" + geturl);\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * check If This Route Affects Any Cached Data\r\n     *\/\r\n    Apicache.prototype.checkIfThisRouteAffectsAnyCachedData = function(url) {\r\n        url = url.replace(uuid_regex, \"{{id}}\");\r\n        var urlsthatwillaffectthisget_tocache = JSON.parse(sessionStorage.getItem('urlsthatwillaffectthisget'));\r\n        if (urlsthatwillaffectthisget_tocache && urlsthatwillaffectthisget_tocache[url]) {\r\n            urlsthatwillaffectthisget_tocache[url].forEach(function(geturl) {\r\n                remove(geturl);\r\n            });\r\n        }\r\n    }\r\n\r\n    \/**\r\n     * Register the passed url for caching. Registering basically means \r\n     * I will make sure the API response for this URL is cached\r\n     *\/\r\n    Apicache.prototype.register = function(geturl, urlsthatwillaffectthisget) {\r\n        var getroutes_tocache = JSON.parse(sessionStorage.getItem('getroutes_tocache'));\r\n        if (!getroutes_tocache) {\r\n            getroutes_tocache = []; \/\/create a fresh variable\r\n        }\r\n        if (getroutes_tocache.indexOf(geturl) == -1) {\r\n            getroutes_tocache.push(geturl);\r\n        }\r\n        var urlsthatwillaffectthisget_tocache = JSON.parse(sessionStorage.getItem('urlsthatwillaffectthisget'));\r\n        if (!urlsthatwillaffectthisget_tocache) {\r\n            urlsthatwillaffectthisget_tocache = {}; \/\/create a fresh variable\r\n        }\r\n        if (urlsthatwillaffectthisget) {\r\n            urlsthatwillaffectthisget.forEach(function(uwa) {\r\n                var uwa = uwa.replace(uuid_regex, \"{{id}}\");\r\n                if (!urlsthatwillaffectthisget_tocache[uwa]) {\r\n                    urlsthatwillaffectthisget_tocache[uwa] = [];\r\n                }\r\n                urlsthatwillaffectthisget_tocache[uwa].push(geturl);\r\n            });\r\n            sessionStorage.setItem('urlsthatwillaffectthisget', JSON.stringify(urlsthatwillaffectthisget_tocache));\r\n        }\r\n        sessionStorage.setItem('getroutes_tocache', JSON.stringify(getroutes_tocache));\r\n    }\r\n\r\n    \/\/A top level global instance where all the public ojects will be attached\r\n    var apicache = new Apicache();\r\n\r\n    \/\/ Attach the instance to window to make it globally available\r\n    window.apicache = apicache;\r\n\r\n})();<\/pre>\n

  <\/p>\n","protected":false},"excerpt":{"rendered":"

Talk about caching and we talk about how we are going to\u00a0put everything into redis or memcache on our servers and send back cached responses to our\u00a0clients and make our applications respond\u00a0faster by taking the load off the databases. Below is a flow of how a typical caching\u00a0mechanism\u00a0looks like. While server caching is a phenomenal […]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[2],"tags":[52],"_links":{"self":[{"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/posts\/200"}],"collection":[{"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/comments?post=200"}],"version-history":[{"count":1,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/posts\/200\/revisions"}],"predecessor-version":[{"id":1651,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/posts\/200\/revisions\/1651"}],"wp:attachment":[{"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/media?parent=200"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/categories?post=200"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/write.muthu.co\/wp-json\/wp\/v2\/tags?post=200"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}