UNPKG

7.63 kBJavaScriptView Raw
1'use strict';
2
3var onFinished = require('on-finished')
4 , VError = require('verror')
5 , replaceDateHeader = require('./replace-date-header')
6 , getHttpResponseData = require('./parse-http-response');
7
8/**
9 * Return an instance that can be used to write to a cache store/engine
10 * @param {Object} opts
11 * @param {Function} callback
12 * @return {Object}
13 */
14module.exports = function getExpeditiousCacheMiddleware (opts) {
15
16 // Verify options are valid. Will throw an AssertionError if not
17 require('./verify-options')(opts);
18
19 // An expeditious instance that we will use for caching operations
20 var cache = opts.expeditious;
21
22 // Module that will be used to prevent us attempting to cache the same
23 // request multiple times concurrently, thus reducing memory bloat and
24 // unecessary cpu usgae
25 var cacheLocks = require('./cache-locks')(opts);
26
27 // Determines if caching should occur for a given request
28 var shouldCache = require('./should-cache')(opts);
29
30 // Generates the key used to cache a request
31 var genCacheKey = require('./cache-key')(opts);
32
33 // Determine the ttl for cache entries
34 var getCacheExpiry = require('./cache-expiry')(opts);
35
36
37 return function _expeditiousCacheMiddleware (req, res, next) {
38 var log = require('./log')(req)
39 , needsStorage = false
40 , cacheKey = genCacheKey(req);
41
42 log('checking cache for request using key "%s"', cacheKey);
43
44 // Stop here if the request should not be cached. Think of the environment!
45 if (!shouldCache(req)) {
46 return next();
47 }
48
49 // This request could already be cached, so let's start by checking that
50 cache.get({
51 key: cacheKey
52 }, onCacheReturned);
53
54
55 /**
56 * Callback for when we load a value from cache, decides if we proceed with
57 * the request or send back the cached data (if it existed)
58 * @param {Error} err
59 * @param {String} value
60 */
61 function onCacheReturned (err, httpData) {
62 log('loaded data from cache:\n%s', JSON.stringify(httpData, null, 2));
63
64 if (err) {
65 log('cache error, proceed with request');
66 proceedWithRequest();
67 } else if (httpData) {
68 log('cache hit');
69 respondWithCache(httpData);
70 } else {
71 log('cache miss, proceed with request');
72 proceedWithRequest();
73 }
74 }
75
76 /**
77 * Respond to a request with a value that the cache returned
78 * @param {Object} httpData
79 */
80 function respondWithCache (httpData) {
81 log('responding with cached value');
82
83 res.set('etag', httpData.etag);
84
85 if (req.fresh) {
86 log('returning a 304, etag matches');
87 // req.fresh is an express "defineProperty" added that will perform
88 // a check that the incoming etag matches the one on the res headers
89 res.status(304);
90 res.end();
91 } else {
92 log('returning cached data, etags did not match');
93 // We're writing a HTTP response body directly to the socket, therefore
94 // we use socket.write instead of .send or others to prevent adding
95 // unwanted headers and response data
96 res.socket.write(replaceDateHeader(httpData.completeHttpBody));
97 res.end();
98 }
99 }
100
101 /**
102 * Called once we finish writing data to the client.
103 * Writes the returned data to the cache if no errors occurred.
104 * @param {Error} err
105 */
106 function onResponseFinished (err, httpData) {
107 /* istanbul ignore else */
108 if (err) {
109 log('error processing request, not storing response in cache');
110
111 cacheLocks.removeLock(cacheKey);
112 } else if (needsStorage) {
113
114 // If the cache time is 0 don't bother doing a write
115 if (getCacheExpiry(res.statusCode) === 0) {
116 log('cache time for %s is 0 - will not be cached', res.statusCode);
117 cacheLocks.removeLock(cacheKey);
118 } else {
119 log(
120 'writing response to storage with key "%s", response:\n%s',
121 cacheKey,
122 JSON.stringify(httpData, null, 2)
123 );
124
125 cache.set({
126 key: cacheKey,
127 val: httpData,
128 ttl: getCacheExpiry(res.statusCode)
129 }, function (err) {
130 if (err) {
131 log('failed to write cache');
132 log(err);
133 } else {
134 log(
135 'wrote response to storage with key "%s"',
136 req.originalUrl,
137 cacheKey
138 );
139 }
140
141 cacheLocks.removeLock(cacheKey);
142 });
143 }
144
145 }
146 }
147
148
149 /**
150 * Processes this request using the handler the express application has
151 * defined, but will cache the response from that handler
152 * @return {undefined}
153 */
154 function proceedWithRequest () {
155 // Hacky, but so is overwriting res.end, res.send, etc. By overwriting
156 // res.socket.write we simplify our job since we can cache the "raw" http
157 // response and don't need to rebuild it for future requests
158 res.socket.write = (function (res) {
159 var buf = ''
160 , isInitialWrite = true
161 , _write = res.socket.write.bind(res.socket);
162
163 // We listen for the request to emit "end" or "finish" and then write to
164 // the cache since we have everything we need to do so
165 onFinished(res, function (err) {
166 /* istanbul ignore else */
167 if (err) {
168 log('request finished with error, not parsing response for cache');
169 onResponseFinished(err);
170 } else if (needsStorage) {
171 log('request finished, parsing response for cache');
172 var res = null;
173
174 try {
175 res = getHttpResponseData(new Buffer(buf));
176 } catch (e) {
177 err = new VError(e, 'failed to parse http response');
178 }
179
180 onResponseFinished(err, res);
181 } else {
182 log(
183 'request didn\'t need to be cached, another request for this ' +
184 'resource was cached first'
185 );
186 }
187 });
188
189 return function customWrite (body) {
190 // On the initial write for this response we need to check if a
191 // matching request is already being written to the cache, and if it
192 // is then we won't attempt to cache this since it would have a large
193 // performance penalty in high concurrency environments
194 /* istanbul ignore else */
195 if (isInitialWrite) {
196 log(
197 'initial write called for request %s determining if caching ' +
198 'is required',
199 req.originalUrl
200 );
201
202 isInitialWrite = false;
203
204 // If a lock does not already exist we need to cache this response
205 needsStorage = !cacheLocks.isLocked(cacheKey);
206
207 /* istanbul ignore else */
208 if (needsStorage) {
209 log('caching entry needs to be created for this request');
210 cacheLocks.addLock(cacheKey);
211 }
212 }
213
214 /* istanbul ignore else */
215 if (needsStorage) {
216 // Build this cache entry so it can be written to the cache
217 buf += body.toString();
218 }
219
220 // We still need to write to the original socket.write function
221 _write.apply(_write, Array.prototype.slice.call(arguments));
222 };
223 })(res);
224
225 // Send the request to the next function in the routing stack, our custom
226 // res.socket.write above will be invoked as soon as the application
227 // starts writing a response
228 next();
229 }
230 };
231};