Caching in Node.js and HTML5 Session Storage in action

 I am going to be illustrating a very powerful use case of caching on the server side and the client side in this post using Node.js in memory cache and HTML5 Session Storage.

Let us understand the advantages:

Server Side Caching:

When we have a heavy duty operation in the server side that involves either
a) Streaming in static contents from file system or
b) Obtaining static html from a content management system or
c) Obtaining global lookups from the database,

 it makes sense to employ caching at server side to see a significant performance boost. For the server side I am going to be employing the node-cache. The API is simple and easy to use.

Client Side Caching:

When we have operations such as
a) Context sensitive help
b) Client Constants
c) Resource Bundles

it makes sense to employ the caching in the client side to reduce the number of calls to the server.

With the advent of HTML5 we have two types of client side caching techniques.

a) Local Storage: Data is persisted over different tabs or windows and even on closure of the web browser.

b) Session Storage: Data is persisted for every top-level browsing context. Data is lost on closure of the tab, window or browser.


Alright enough of the marketing for caching, lets get down to business now.

1) Creating the Express Project:

Create a quick express project in your workspace by hitting "express Node_HTML5_Cache" from the cmd prompt. If this is your first time on express and need more details to get your environment set with express, please read my previous post here.

2) Creating the UI and adding the HTML5 Session Storage

a)  Add the following to layout.jade

doctype 5
html
            head
                        script(type='text/javascript', src='/jquery/js/jquery.min.js')
            body
                        a(title='Home', href="/") Home |
                        a(title='GO TO PAGE 1', href="/getPage1") Page 1
                        block content

Summary:
We are adding two links for two pages - HOME and PAGE1

b) Add the following to index.jade


extends layout

block content
            h1= title
            p Welcome to #{title}
            script(type='text/javascript', src='/javascripts/uiHelper.js')
            input.button1(type='submit', value='GET HELP CONTENT',id="help")
            div#pageInfo

Summary:
We are adding a uiHelper.js which holds our ajax calls to the server side. We are also adding a button that triggers the call to fetch the page specific help info. Page Info div will hold the data. 

Note: This solution can be extended for context sensitive help in any web 2.0 application.

c) Add the following to page1.jade:


extends layout

block content
            h1= title
            p Welcome to Page 1
            script(type='text/javascript', src='/javascripts/uiHelper.js')
            input.button1(type='submit', value='GET HELP CONTENT',id="help")
            div#pageInfo


Summary:
We are doing the same thing as we did in the home page in index.jade here.

d) Adding the HTML5 Session Storage to uiHelper.js:




 $(document).ready(function () {
            $('.button1').live('click', function () {
                        var contextObject= getContextObject();
                        var theContent = getTheContent(contextObject);
    });
   
    function getContextObject(){
            var contextObject = window.location.pathname;
                        var regex = new RegExp("/", 'g');
        contextObject = contextObject.replace(regex, "");
       
        switch (contextObject) {
            case '':
                contextObject = 'homePage';
                break;
            case 'getPage1':
                        contextObject = 'page1';
                break;
        }
        console.log("requested context help page --> "+contextObject);
        return contextObject;
    }
   
    function getTheContent(contextObject) {
                var clientDataStore = window.sessionStorage;
                var theContent = "";
               
                //check if the content exists in client cache
                theContent = clientDataStore.getItem('_myKey'+contextObject);
               
                //Make the server side call to get the data if cache is empty on client
                if (theContent == null || theContent.length <= 0) {
                         console.log('return content from server side.............');
                    $.ajax({
                        async:false,
                        url: '/getTheContent/' + contextObject,
                        type: "GET",
                        cache: false,
                        timeout: 5000,
                        complete: function () {},
                        success: function (callback) {
                            theContent = callback;
                            clientDataStore.setItem('_myKey'+contextObject, theContent); // <_myKeypage1 , contents of page 1>
                            console.log("................."+theContent);
                            $('#pageInfo').html('');
                                            $("#pageInfo").css("display", "block");       
                                            $('#pageInfo').html(theContent);
                        },
                        error: function (event)
                        { console.log('Error:::'+event); }
                    });
                } else {
                    // return the content from client side cache
                    console.log('return content from client side cache .............');
                    return theContent;
                }

    return theContent;
}


 });

Summary:

1) We are adding a click event on the button click.

2) We get the contextObject meaning the requesting page ie HOME or PAGE1.

3) I am adding a regex operator to make the URL readable. Please make a note of these ie homePage and page1 contextObject for the two page respectively.

4) Next we check if the data exists in the HTML5 Session Storage. If the data is present we render the contents from here else we make the ajax call to the server side.

5) We render the help contents which is streamed in to the pageinfo div.

6) If the cache is empty the result from the server is stored into the cache with appropriate key.


3) Creating the Server side and adding the in memory cache

a) Create a folder called help under the project and add two files - homePage and page1. This is same as the point I have asked you to specially note in above pointer. You can add any HTML dummy contents in each of the file such as
<h2> Home Page Help Info</h2>

and

<h2> Page1 Help Info</h2>
 respectively to each of the file.

b) Add the following code to app.js





/**
 * Module dependencies.
 */

var express = require('express')
  , routes = require('./routes')
  , user = require('./routes/user')
  , http = require('http')
  , path = require('path')
  , myCache = require('memory-cache');

var app = express();

app.configure(function(){
  app.set('port', process.env.PORT || 3000);
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.favicon());
  app.use(express.logger('dev'));
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(path.join(__dirname, 'public')));
  app.use(express.static(path.join(__dirname, 'help')));
});

app.configure('development', function(){
  app.use(express.errorHandler());
});

app.get('/', routes.index);
app.get('/users', user.list);
app.get('/getPage1',routes.getPage1);
app.get('/getTheContent/:contextObject', routes.getTheContent);
myCache.put("rootFolder", __dirname)


http.createServer(app).listen(app.get('port'), function(){
  console.log("Express server listening on port " + app.get('port'));
});


Summary:

1) Import the module for caching.
2) Store the root folder that contains the data for the context sensitive help.
3) Add the get requests.

c) Add the following code to index.js and define the route handlers




/*
 * GET home page.
 */

 var contextHelpRouter= require('./routeHelper/contextHelpRouteHelper.js');


exports.index = function(req, res){
  res.render('index', { title: 'HOME' });
};

exports.getPage1 = function(req, res){
  res.render('page1', { title: 'PAGE 1' });
};

exports.getTheContent = function(req, res){
            var contextObject = req.params.contextObject;
    res.send(contextHelpRouter.getTheContent(contextObject));
 
};



d) Add the following code to the contextHelpRouteHelper.js under routes/routeHelper folder.



/**
 * New node file
 */


var myCache = require('memory-cache');
var fs = require('fs');
var path = require('path');



function getTheContent(contextObject) {
    console.log("server side processing starts here.....................................");
  
   //check if server side cache has data
    var isLoaded = myCache.get("_myServerKey_isLoaded");
   
    //get from file system  
    if (isLoaded == null){
            // get the root folder dir packed from app.js start up
            var rootFolder = myCache.get("rootFolder") + "/help/";
            console.log("root folder is ...."+rootFolder);
            loadFromFileSystem(rootFolder);
    }else{
            // load from in memory server side cache
           
    }

            var theContent = "";
            theContent = myCache.get("_myKey" + contextObject);
    console.log("server side processing ends here.....................................");
    return theContent;
   
};

function loadFromFileSystem(helpFolder) {
    // match with the filenames under \help\ folder
    var arrFiles = ["homePage","page1"];
    for (var i = 0; i < arrFiles.length; i++) {
        if (path.existsSync(helpFolder + arrFiles[i])) {
            theContent = fs.readFileSync(helpFolder + arrFiles[i], 'utf8');
            if (theContent != null)
                myCache.put('_myKey' + arrFiles[i], theContent.toString());//<_myKeypage1>, <content from file>
          
        }
    }
    myCache.put("_myServerKey_isLoaded", "yes");
};

exports.getTheContent = getTheContent;


Summary:

1) We follow the same technique as we did in the client side , however this time with a different server side cache.

2) As well as we load the entire data from all the files in one go and cater subsequent request from the cache. We can tweak this further to load on server start up. We trade for a delayed server start to a high performance system at runtime with this technique.  

4) Putting it all together.

Run the application, you will see the following screen:

a)

b) Click on the GET HELP CONTENT button - You will see the server side call from client in the firebug and on the server console you will see the data streamed in from the file and stored in the cache.


c) Click on the button again - You will see no server side call is made and data is rendered from the client HTML5 Session Storage.



d) You can now switch the link to Page1 - You will see on clicking the button that the data is read from the server side cache and then stored in the client side cache for catering subsequent requests.

Very simple concepts however really lethal in boosting performance in large scale applications.

Happy Coding!!!

2 comments: