aboutsummaryrefslogblamecommitdiffstats
path: root/lib/cached-request.js
blob: f12365e11a57e5f3104115cbb5e9859b19f37d70 (plain) (tree)


























                                                             







                                                                                 



































































                                                                       





                                                                            









                                                                 








































































                                                                               
             
           
         
               
     
                               




                                     
// Released under the MIT/X11 license
// http://www.opensource.org/licenses/mit-license.php

"use strict";
var Request = require("request").Request;
var xhrMod = require("xhr");

// Initialize myStorage if we don't have it.
var myStorage = require("simple-storage").storage;
if (!myStorage.cachedRequest) {
  myStorage.cachedRequest = {};
}

// https://bugzilla.mozilla.org/582760 is still alive
var debugOption = true;
function debug(str) {
  if (debugOption) {
    console.log(str);
  }
}

/**
 * Decorating cached response object with a bit of additional
 * functionality.
 */
function CachedResponse (url) {
  if (!myStorage.cachedRequest[url]) {
    // FIXME myk's review comment
    // This creates a record in the datastore for a URL if one doesn't exist,
    // but it doesn't initialize the record.  So if some code calls
    // CachedResponse["http://example.com/"] twice without calling
    // CachedResponse.saveCached() in between, it'll get different results the
    // second time.  Better to avoid creating the record until
    // CachedResponse.savedCached() is called (alternately: initialize the record
    // when creating it).
    myStorage.cachedRequest[url] = {};
    this.url = url;
    this.text = null;
    this.status = "";
    this.statusText = "";
    this.headers = {};
  }
  else {
    var storedResponse = myStorage.cachedRequest[url];
    this.url = url;
    this.text = storedResponse.text;
    this.status = storedResponse.status;
    this.statusText = storedResponse.statusText;
    this.headers = storedResponse.headers;
  }
}

/**
 * getter returning object from this.text
 * (so we don't save essentially the same object same)
 *
 * @return Object from this.text, if this.text isn't JSONable,
 *   return null
 */
CachedResponse.prototype.__defineGetter__("json", function() {
  try {
    return JSON.parse(this.text);
  }
  catch (ex) {
    if (ex instanceof SyntaxError) {
      return null;
    } else {
      throw ex;
    }
  }
});

/**
 * save the current object's values to myStorage.
 */
CachedResponse.prototype.saveCached = function() {
  var storedResponse = myStorage.cachedRequest[this.url];
  storedResponse.text = this.text;
  storedResponse.status = this.status;
  storedResponse.statusText = this.statusText;
  storedResponse.headers = this.headers;
};

/**
 * getter returning Last-Modified header as a Date object.
 *
 * @return Date when this has Last-Modified header, or null otherwise
 */
CachedResponse.prototype.__defineGetter__("lastModified", function() {
  if (this.headers && (this.headers.hasOwnProperty("Last-Modified"))) {
    return new Date(this.headers["Last-Modified"]);
  }
  return null;
});

/**
 * Emulates Request object, but caches the result for speedier access,
 * and protection when the remote site is down.
 *
 * @opts Object with properties same as Request
 *
 * Limitations: does only GET requests, so it doesn't even have
 *   .post(), .get() or any other methods.
FIXME myk's request
I would still require consumers to call .get(), since the constructor's name
suggests it is a subclass of Request, and hewing to the original API (except
for the methods that aren't implemented) seems like the most understandable 
implementation of such a subclass.

 * Contrary to Request() opts can have onError callback, and
 * getExpiredAnyway property to allow using expired cached value,
 * if the remote connection returns error.
 */
exports.CachedRequest = function CachedRequest(opts) {
  var crStorage = new CachedResponse(opts.url);

  var req = new xhrMod.XMLHttpRequest();
  req.open("HEAD", opts.url, true);
  req.onreadystatechange = function () {
    if ((req.readyState != 4) || (req.status != 200)) {
      return ;
    }
    var curETag = req.getResponseHeader("ETag");
    // FIXME myk's review
    // In bug 643156, bz discouraged use of HEAD requests due to server-side
    // bugs and suggested using nsIURIChecker instead, which itself does a HEAD
    // request for HTTP URLs but apparently works around those bugs.  I'm not
    // sure if you can use that interface here, but it would be worth looking
    // into.
    // http://www.oxymoronical.com/experiments/apidocs/interface/nsIURIChecker
    /*
     * var linkChecker = Cc["@mozilla.org/network/urichecker;1"].
     *                   createInstance(Ci.nsIURIChecker);
     * linkChecker.init(ioService.newURI(url, null, null));
     * linkChecker.loadFlags = Ci.nsIRequest.LOAD_BACKGROUND;
     * linkChecker.asyncCheck(new AutoDownloader(url, savedTo, aWindow), null);
     *
     * or
     * var linkChecker = gLinksBeingChecked[i].
     *     QueryInterface(Components.interfaces.nsIURIChecker);
     * // nsIURIChecker returns:
     * // NS_BINDING_SUCCEEDED     link is valid
     * // NS_BINDING_FAILED        link is invalid (gave an error)
     * // NS_BINDING_ABORTED       timed out, or cancelled
     * var status = linkChecker.status;
     * if (status ==0x804b0001)        // NS_BINDING_FAILED
     *   dump(">> " + linkChecker.name + " is broken\n");
     * else if (status == 0x804b0002)   // NS_BINDING_ABORTED
     *   dump(">> " + linkChecker.name + " timed out\n");
     * else if (status == 0)             // NS_BINDING_SUCCEEDED
     *   dump("   " + linkChecker.name + " OK!\n");
     * else
     *   dump(">> " + linkChecker.name + " not checked\n");
     */
    var curLastModified = new Date(req.getResponseHeader("Last-Modified"));
    if (crStorage && crStorage.headers &&
         ((curETag == crStorage.headers.eTag) ||
         (curLastModified <= crStorage.lastModified))) {
      debug("Loading from cache!");
      // use cached values
    }
    else { // cache is not up-to-date
      new Request({
        url: opts.url,
        onComplete: function(resp) {
          if (resp.status == 200) {
            debug("Too old cache! Reloaded");
            crStorage.headers = resp.headers;
            crStorage.text = resp.text;
            crStorage.status = resp.status;
            crStorage.statusText = resp.statusText;
            crStorage.saveCached();
          }
          else {
            // If we cannot get response from the real URL,
            // we may be happy with getting our fix from
            // anywhere, including (possibly) expired cache
            if (opts.getExpiredAnyway && crStorage && crStorage.text) {
              debug("Nothing better to do! Returning expired cache.");
            }
            else {
              // We definitively lost, just call .onComplete
              // with what we have.
              opts.onComplete({
                text: resp.text,
                json: null,
                status: resp.status,
                statusText: resp.statusText,
                headers: resp.headers
              });
              // to avoid call opts.onComplete below
              return ;
            }
          }
        }
      }).get();
    }
    opts.onComplete(crStorage);
  };
  req.send();
};

//vim: set ts=2 et sw=2 textwidth=80: