UNPKG

5.97 kBJavaScriptView Raw
1
2/*!
3 * Connect - staticCache
4 * Copyright(c) 2011 Sencha Inc.
5 * MIT Licensed
6 */
7
8/**
9 * Module dependencies.
10 */
11
12var deprecate = require('depd')('connect');
13var utils = require('../utils')
14 , parseurl = require('parseurl')
15 , Cache = require('../cache')
16 , fresh = require('fresh');
17var merge = require('utils-merge');
18
19/**
20 * Static cache:
21 *
22 * Status: Deprecated. This middleware will be removed in
23 * Connect 3.0. You may be interested in:
24 *
25 * - [st](https://github.com/isaacs/st)
26 *
27 * Enables a memory cache layer on top of
28 * the `static()` middleware, serving popular
29 * static files.
30 *
31 * By default a maximum of 128 objects are
32 * held in cache, with a max of 256k each,
33 * totalling ~32mb.
34 *
35 * A Least-Recently-Used (LRU) cache algo
36 * is implemented through the `Cache` object,
37 * simply rotating cache objects as they are
38 * hit. This means that increasingly popular
39 * objects maintain their positions while
40 * others get shoved out of the stack and
41 * garbage collected.
42 *
43 * Benchmarks:
44 *
45 * static(): 2700 rps
46 * node-static: 5300 rps
47 * static() + staticCache(): 7500 rps
48 *
49 * Options:
50 *
51 * - `maxObjects` max cache objects [128]
52 * - `maxLength` max cache object length 256kb
53 *
54 * @param {Object} options
55 * @return {Function}
56 * @api public
57 */
58
59module.exports = function staticCache(options){
60 var options = options || {}
61 , cache = new Cache(options.maxObjects || 128)
62 , maxlen = options.maxLength || 1024 * 256;
63
64 return function staticCache(req, res, next){
65 var key = cacheKey(req)
66 , ranges = req.headers.range
67 , hasCookies = req.headers.cookie
68 , hit = cache.get(key);
69
70 // cache static
71 // TODO: change from staticCache() -> cache()
72 // and make this work for any request
73 req.on('static', function(stream){
74 var headers = res._headers
75 , cc = utils.parseCacheControl(headers['cache-control'] || '')
76 , contentLength = headers['content-length']
77 , hit;
78
79 // dont cache set-cookie responses
80 if (headers['set-cookie']) return hasCookies = true;
81
82 // dont cache when cookies are present
83 if (hasCookies) return;
84
85 // ignore larger files
86 if (!contentLength || contentLength > maxlen) return;
87
88 // don't cache partial files
89 if (headers['content-range']) return;
90
91 // dont cache items we shouldn't be
92 // TODO: real support for must-revalidate / no-cache
93 if ( cc['no-cache']
94 || cc['no-store']
95 || cc['private']
96 || cc['must-revalidate']) return;
97
98 // if already in cache then validate
99 if (hit = cache.get(key)){
100 if (headers.etag == hit[0].etag) {
101 hit[0].date = new Date;
102 return;
103 } else {
104 cache.remove(key);
105 }
106 }
107
108 // validation notifiactions don't contain a steam
109 if (null == stream) return;
110
111 // add the cache object
112 var arr = [];
113
114 // store the chunks
115 stream.on('data', function(chunk){
116 arr.push(chunk);
117 });
118
119 // flag it as complete
120 stream.on('end', function(){
121 var cacheEntry = cache.add(key);
122 delete headers['x-cache']; // Clean up (TODO: others)
123 cacheEntry.push(200);
124 cacheEntry.push(headers);
125 cacheEntry.push.apply(cacheEntry, arr);
126 });
127 });
128
129 if (req.method == 'GET' || req.method == 'HEAD') {
130 if (ranges) {
131 next();
132 } else if (!hasCookies && hit && !mustRevalidate(req, hit)) {
133 res.setHeader('X-Cache', 'HIT');
134 respondFromCache(req, res, hit);
135 } else {
136 res.setHeader('X-Cache', 'MISS');
137 next();
138 }
139 } else {
140 next();
141 }
142 }
143};
144
145module.exports = deprecate.function(module.exports,
146 'staticCache: use varnish or similar reverse proxy caches');
147
148/**
149 * Respond with the provided cached value.
150 * TODO: Assume 200 code, that's iffy.
151 *
152 * @param {Object} req
153 * @param {Object} res
154 * @param {Object} cacheEntry
155 * @return {String}
156 * @api private
157 */
158
159function respondFromCache(req, res, cacheEntry) {
160 var status = cacheEntry[0]
161 , headers = merge({}, cacheEntry[1])
162 , content = cacheEntry.slice(2);
163
164 headers.age = (new Date - new Date(headers.date)) / 1000 || 0;
165
166 switch (req.method) {
167 case 'HEAD':
168 res.writeHead(status, headers);
169 res.end();
170 break;
171 case 'GET':
172 if (fresh(req.headers, headers)) {
173 headers['content-length'] = 0;
174 res.writeHead(304, headers);
175 res.end();
176 } else {
177 res.writeHead(status, headers);
178
179 function write() {
180 while (content.length) {
181 if (false === res.write(content.shift())) {
182 res.once('drain', write);
183 return;
184 }
185 }
186 res.end();
187 }
188
189 write();
190 }
191 break;
192 default:
193 // This should never happen.
194 res.writeHead(500, '');
195 res.end();
196 }
197}
198
199/**
200 * Determine whether or not a cached value must be revalidated.
201 *
202 * @param {Object} req
203 * @param {Object} cacheEntry
204 * @return {String}
205 * @api private
206 */
207
208function mustRevalidate(req, cacheEntry) {
209 var cacheHeaders = cacheEntry[1]
210 , reqCC = utils.parseCacheControl(req.headers['cache-control'] || '')
211 , cacheCC = utils.parseCacheControl(cacheHeaders['cache-control'] || '')
212 , cacheAge = (new Date - new Date(cacheHeaders.date)) / 1000 || 0;
213
214 if ( cacheCC['no-cache']
215 || cacheCC['must-revalidate']
216 || cacheCC['proxy-revalidate']) return true;
217
218 if (reqCC['no-cache']) return true;
219
220 if (null != reqCC['max-age']) return reqCC['max-age'] < cacheAge;
221
222 if (null != cacheCC['max-age']) return cacheCC['max-age'] < cacheAge;
223
224 return false;
225}
226
227/**
228 * The key to use in the cache. For now, this is the URL path and query.
229 *
230 * 'http://example.com?key=value' -> '/?key=value'
231 *
232 * @param {Object} req
233 * @return {String}
234 * @api private
235 */
236
237function cacheKey(req) {
238 return parseurl(req).path;
239}