text: /*!
   * Connect - staticCache
   * Copyright(c) 2011 Sencha Inc.
   * MIT Licensed
   */
  
  /**
   * Module dependencies.
   */
  
  var deprecate = require('depd')('connect');
  var utils = require('../utils')
      , parseurl = require('parseurl')
      , Cache = require('../cache')
      , fresh = require('fresh');
  var merge = require('utils-merge');
  
  /**
   * Static cache:
   *
   * Status: Deprecated. This middleware will be removed in
   * Connect 3.0. You may be interested in:
   *
   *   - [st](https://github.com/isaacs/st)
   *
   * Enables a memory cache layer on top of
   * the `static()` middleware, serving popular
   * static files.
   *
   * By default a maximum of 128 objects are
   * held in cache, with a max of 256k each,
   * totalling ~32mb.
   *
   * A Least-Recently-Used (LRU) cache algo
   * is implemented through the `Cache` object,
   * simply rotating cache objects as they are
   * hit. This means that increasingly popular
   * objects maintain their positions while
   * others get shoved out of the stack and
   * garbage collected.
   *
   * Benchmarks:
   *
   *     static(): 2700 rps
   *     node-static: 5300 rps
   *     static() + staticCache(): 7500 rps
   *
   * Options:
   *
   *   - `maxObjects`  max cache objects [128]
   *   - `maxLength`  max cache object length 256kb
   *
   * @param {Object} options
   * @return {Function}
   * @api public
   */
  
  module.exports = function staticCache(options) {
      var options = options || {}
          , cache = new Cache(options.maxObjects || 128)
          , maxlen = options.maxLength || 1024 * 256;
  
      return function staticCache(req, res, next) {
          var key = cacheKey(req)
              , ranges = req.headers.range
              , hasCookies = req.headers.cookie
              , hit = cache.get(key);
  
          // cache static
          // TODO: change from staticCache() -> cache()
          // and make this work for any request
          req.on('static', function (stream) {
              var headers = res._headers
                  , cc = utils.parseCacheControl(headers['cache-control'] || '')
                  , contentLength = headers['content-length']
                  , hit;
  
              // dont cache set-cookie responses
              if (headers['set-cookie']) return hasCookies = true;
  
              // dont cache when cookies are present
              if (hasCookies) return;
  
              // ignore larger files
              if (!contentLength || contentLength > maxlen) return;
  
              // don't cache partial files
              if (headers['content-range']) return;
  
              // dont cache items we shouldn't be
              // TODO: real support for must-revalidate / no-cache
              if (cc['no-cache']
                  || cc['no-store']
                  || cc['private']
                  || cc['must-revalidate']) return;
  
              // if already in cache then validate
              if (hit = cache.get(key)) {
                  if (headers.etag == hit[0].etag) {
                      hit[0].date = new Date;
                      return;
                  } else {
                      cache.remove(key);
                  }
              }
  
              // validation notifiactions don't contain a steam
              if (null == stream) return;
  
              // add the cache object
              var arr = [];
  
              // store the chunks
              stream.on('data', function (chunk) {
                  arr.push(chunk);
              });
  
              // flag it as complete
              stream.on('end', function () {
                  var cacheEntry = cache.add(key);
                  delete headers['x-cache']; // Clean up (TODO: others)
                  cacheEntry.push(200);
                  cacheEntry.push(headers);
                  cacheEntry.push.apply(cacheEntry, arr);
              });
          });
  
          if (req.method == 'GET' || req.method == 'HEAD') {
              if (ranges) {
                  next();
              } else if (!hasCookies && hit && !mustRevalidate(req, hit)) {
                  res.setHeader('X-Cache', 'HIT');
                  respondFromCache(req, res, hit);
              } else {
                  res.setHeader('X-Cache', 'MISS');
                  next();
              }
          } else {
              next();
          }
      }
  };
  
  module.exports = deprecate.function(module.exports,
      'staticCache: use varnish or similar reverse proxy caches');
  
  /**
   * Respond with the provided cached value.
   * TODO: Assume 200 code, that's iffy.
   *
   * @param {Object} req
   * @param {Object} res
   * @param {Object} cacheEntry
   * @return {String}
   * @api private
   */
  
  function respondFromCache(req, res, cacheEntry) {
      var status = cacheEntry[0]
          , headers = merge({}, cacheEntry[1])
          , content = cacheEntry.slice(2);
  
      headers.age = (new Date - new Date(headers.date)) / 1000 || 0;
  
      switch (req.method) {
          case 'HEAD':
              res.writeHead(status, headers);
              res.end();
              break;
          case 'GET':
              if (fresh(req.headers, headers)) {
                  headers['content-length'] = 0;
                  res.writeHead(304, headers);
                  res.end();
              } else {
                  res.writeHead(status, headers);
  
                  function write() {
                      while (content.length) {
                          if (false === res.write(content.shift())) {
                              res.once('drain', write);
                              return;
                          }
                      }
                      res.end();
                  }
  
                  write();
              }
              break;
          default:
              // This should never happen.
              res.writeHead(500, '');
              res.end();
      }
  }
  
  /**
   * Determine whether or not a cached value must be revalidated.
   *
   * @param {Object} req
   * @param {Object} cacheEntry
   * @return {String}
   * @api private
   */
  
  function mustRevalidate(req, cacheEntry) {
      var cacheHeaders = cacheEntry[1]
          , reqCC = utils.parseCacheControl(req.headers['cache-control'] || '')
          , cacheCC = utils.parseCacheControl(cacheHeaders['cache-control'] || '')
          , cacheAge = (new Date - new Date(cacheHeaders.date)) / 1000 || 0;
  
      if (cacheCC['no-cache']
          || cacheCC['must-revalidate']
          || cacheCC['proxy-revalidate']) return true;
  
      if (reqCC['no-cache']) return true;
  
      if (null != reqCC['max-age']) return reqCC['max-age'] < cacheAge;
  
      if (null != cacheCC['max-age']) return cacheCC['max-age'] < cacheAge;
  
      return false;
  }
  
  /**
   * The key to use in the cache. For now, this is the URL path and query.
   *
   * 'http://example.com?key=value' -> '/?key=value'
   *
   * @param {Object} req
   * @return {String}
   * @api private
   */
  
  function cacheKey(req) {
      return parseurl(req).path;
  }
  