Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Node Cookbook: Second Edition

You're reading from   Node Cookbook: Second Edition Transferring your JavaScript skills to server-side programming is simplified with this comprehensive cookbook. Each chapter focuses on a different aspect of Node, featuring recipes supported with lots of illustrations, tips, and hints.

Arrow left icon
Product type Paperback
Published in Apr 2014
Publisher Packt
ISBN-13 9781783280438
Length 378 pages
Edition Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
David Mark Clements David Mark Clements
Author Profile Icon David Mark Clements
David Mark Clements
Arrow right icon
View More author details
Toc

Table of Contents (18) Chapters Close

Node Cookbook Second Edition
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
1. Making a Web Server FREE CHAPTER 2. Exploring the HTTP Object 3. Working with Data Serialization 4. Interfacing with Databases 5. Employing Streams 6. Going Real Time 7. Accelerating Development with Express 8. Implementing Security, Encryption, and Authentication 9. Integrating Network Paradigms 10. Writing Your Own Node Modules 11. Taking It Live Index

Caching content in memory for immediate delivery


Directly accessing storage on each client request is not ideal. For this task, we will explore how to enhance server efficiency by accessing the disk only on the first request, caching the data from file for that first request, and serving all further requests out of the process memory.

Getting ready

We are going to improve upon the code from the previous task, so we'll be working with server.js and in the content directory, with index.html, styles.css, and script.js.

How to do it...

Let's begin by looking at our following script from the previous recipe Serving static files:

var http = require('http');
var path = require('path');
var fs = require('fs');  

var mimeTypes = {
  '.js' : 'text/javascript',
  '.html': 'text/html',
  '.css' : 'text/css'
};

http.createServer(function (request, response) {
  var lookup = path.basename(decodeURI(request.url)) || 'index.html';
  var f = 'content/'+lookup;
  fs.exists(f, function (exists) {
    if (exists) {
      fs.readFile(f, function(err,data) {
        if (err) {
          response.writeHead(500); response.end('Server Error!');
          return;
        }
        var headers = {'Content-type': mimeTypes[path.extname(lookup)]};
        response.writeHead(200, headers);
        response.end(data);
      });
    return;
    }
    response.writeHead(404); //no such file found!
    response.end('Page Not Found');
  });
}

We need to modify this code to only read the file once, load its contents into memory, and respond to all requests for that file from memory afterwards. To keep things simple and preserve maintainability, we'll extract our cache handling and content delivery into a separate function. So above http.createServer, and below mimeTypes, we'll add the following:

var cache = {};
function cacheAndDeliver(f, cb) {
  if (!cache[f]) {
    fs.readFile(f, function(err, data) {
      if (!err) {
        cache[f] = {content: data} ;
      }
      cb(err, data);
    });
    return;
  }
  console.log('loading ' + f + ' from cache');
  cb(null, cache[f].content);
}
//http.createServer

A new cache object and a new function called cacheAndDeliver have been added to store our files in memory. Our function takes the same parameters as fs.readFile so we can replace fs.readFile in the http.createServer callback while leaving the rest of the code intact as follows:

//...inside http.createServer:

fs.exists(f, function (exists) {
  if (exists) {
    cacheAndDeliver(f, function(err, data) {
      if (err) {
        response.writeHead(500);
        response.end('Server Error!');
        return; }
      var headers = {'Content-type': mimeTypes[path.extname(f)]};
      response.writeHead(200, headers);
      response.end(data);      
    });
return;
  }
//rest of path exists code (404 handling)...

When we execute our server.js file and access localhost:8080 twice, consecutively, the second request causes the console to display the following output:

loading content/index.html from cache
loading content/styles.css from cache
loading content/script.js from cache

How it works...

We defined a function called cacheAndDeliver, which like fs.readFile, takes a filename and callback as parameters. This is great because we can pass the exact same callback of fs.readFile to cacheAndDeliver, padding the server out with caching logic without adding any extra complexity visually to the inside of the http.createServer callback.

As it stands, the worth of abstracting our caching logic into an external function is arguable, but the more we build on the server's caching abilities, the more feasible and useful this abstraction becomes. Our cacheAndDeliver function checks to see if the requested content is already cached. If not, we call fs.readFile and load the data from disk.

Once we have this data, we may as well hold onto it, so it's placed into the cache object referenced by its file path (the f variable). The next time anyone requests the file, cacheAndDeliver will see that we have the file stored in the cache object and will issue an alternative callback containing the cached data. Notice that we fill the cache[f] property with another new object containing a content property. This makes it easier to extend the caching functionality in the future as we would just have to place extra properties into our cache[f] object and supply logic that interfaces with these properties accordingly.

There's more...

If we were to modify the files we are serving, the changes wouldn't be reflected until we restart the server. We can do something about that.

Reflecting content changes

To detect whether a requested file has changed since we last cached it, we must know when the file was cached and when it was last modified. To record when the file was last cached, let's extend the cache[f] object as follows:

cache[f] = {content: data,timestamp: Date.now() //store a Unix time stamp
};

That was easy! Now let's find out when the file was updated last. The fs.stat method returns an object as the second parameter of its callback. This object contains the same useful information as the command-line GNU (GNU's Not Unix!) coreutils stat. The fs.stat function supplies three time-related properties: last accessed (atime), last modified (mtime), and last changed (ctime). The difference between mtime and ctime is that ctime will reflect any alterations to the file, whereas mtime will only reflect alterations to the content of the file. Consequently, if we changed the permissions of a file, ctime would be updated but mtime would stay the same. We want to pay attention to permission changes as they happen so let's use the ctime property as shown in the following code:

//requires and mimeType object....
var cache = {};
function cacheAndDeliver(f, cb) {
  fs.stat(f, function (err, stats) {
    if (err) { return console.log('Oh no!, Eror', err); }
    var lastChanged = Date.parse(stats.ctime),
    isUpdated = (cache[f]) && lastChanged  > cache[f].timestamp;
    if (!cache[f] || isUpdated) {
      fs.readFile(f, function (err, data) {
        console.log('loading ' + f + ' from file');
        //rest of cacheAndDeliver
  }); //end of fs.stat
}

Note

If we're using Node on Windows, we may have to substitute ctime with mtime, since ctime supports at least Version 0.10.12.

The contents of cacheAndDeliver have been wrapped in an fs.stat callback, two variables have been added, and the if(!cache[f]) statement has been modified. We parse the ctime property of the second parameter dubbed stats using Date.parse to convert it to milliseconds since midnight, January 1st, 1970 (the Unix epoch) and assign it to our lastChanged variable. Then we check whether the requested file's last changed time is greater than when we cached the file (provided the file is indeed cached) and assign the result to our isUpdated variable. After that, its merely a case of adding the isUpdated Boolean to the conditional if(!cache[f]) statement via the || (or) operator. If the file is newer than our cached version (or if it isn't yet cached), we load the file from disk into the cache object.

See also

  • The Optimizing performance with streaming recipe

  • The Browser-server transmission via AJAX recipe in Chapter 3, Working with Data Serialization

  • Chapter 4, Interfacing with Databases

You have been reading a chapter from
Node Cookbook: Second Edition
Published in: Apr 2014
Publisher: Packt
ISBN-13: 9781783280438
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime