Skip to content

Commit 3da2063

Browse files
author
David Longworth
authored
Merge pull request #196 from dadi/feature/performance
🚀 Performance enhancements
2 parents 01a0458 + e9cdecb commit 3da2063

35 files changed

+1514
-848
lines changed

config.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,13 @@ var conf = convict({
234234
},
235235
headers: {
236236
useGzipCompression: {
237+
doc: 'Depricated: use `useCompression` instead.',
238+
format: Boolean,
239+
default: true
240+
},
241+
useCompression: {
237242
doc:
238-
"If true, uses gzip compression and adds a 'Content-Encoding:gzip' header to the response.",
243+
"If true, uses br or gzip compression where available and adds a 'Content-Encoding: [br|gzip]' header to the response.",
239244
format: Boolean,
240245
default: true
241246
},
@@ -244,9 +249,12 @@ var conf = convict({
244249
"A set of custom cache control headers for different content types. For example 'cacheControl': { 'text/css': 'public, max-age=1000' }",
245250
format: Object,
246251
default: {
252+
'image/png': 'public, max-age=86400',
253+
'image/jpeg': 'public, max-age=86400',
247254
'text/css': 'public, max-age=86400',
248255
'text/javascript': 'public, max-age=86400',
249-
'application/javascript': 'public, max-age=86400'
256+
'application/javascript': 'public, max-age=86400',
257+
'image/x-icon': 'public, max-age=31536000000'
250258
}
251259
}
252260
},
@@ -288,6 +296,14 @@ var conf = convict({
288296
format: String,
289297
default: ''
290298
}
299+
},
300+
sentry: {
301+
dsn: {
302+
doc:
303+
"The 'DSN' to use for logging errors and events to a Sentry server. It should be similar to 'https://693ef18da3184cffa82144fde2979cbc:[email protected]/59524'.",
304+
format: String,
305+
default: ''
306+
}
291307
}
292308
},
293309
globalEvents: {
@@ -306,7 +322,6 @@ var conf = convict({
306322
default: {
307323
datasources: path.join(__dirname, '/workspace/datasources'),
308324
events: path.join(__dirname, '/workspace/events'),
309-
media: path.join(__dirname, '/workspace/media'),
310325
middleware: path.join(__dirname, '/workspace/middleware'),
311326
pages: path.join(__dirname, '/workspace/pages'),
312327
public: path.join(__dirname, '/workspace/public'),

dadi/lib/api/index.js

+5-6
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ var Api = function () {
3030
this.protocol = config.get('server.protocol') || 'http'
3131
this.redirectPort = config.get('server.redirectPort')
3232

33-
if (this.protocol === 'http') {
34-
this.httpInstance = http.createServer(this.listener)
35-
} else if (this.protocol === 'https') {
33+
if (this.protocol === 'https') {
3634
// Redirect http to https
3735
if (this.redirectPort > 0) {
3836
this.redirectInstance = http.createServer(this.redirectListener)
@@ -84,6 +82,8 @@ var Api = function () {
8482
throw new Error(exPrefix + ex.message)
8583
}
8684
}
85+
} else {
86+
this.httpInstance = http.createServer(this.listener)
8787
}
8888
}
8989

@@ -219,7 +219,7 @@ Api.prototype.listener = function (req, res) {
219219
// if end of the stack, no middleware could handle the current
220220
// request, so get matching routes from the loaded page components and
221221
// add them to the stack just before the 404 handler, then continue the loop
222-
// console.log(pathsLoaded, this.stack[stackIdx].name, this.stack[stackIdx+1] ? this.stack[stackIdx+1].name : 'nothing left!')
222+
223223
if (
224224
this.stack[stackIdx + 1] &&
225225
this.stack[stackIdx + 1].name === 'notFound' &&
@@ -242,7 +242,6 @@ Api.prototype.listener = function (req, res) {
242242
_.each(matches, match => {
243243
this.stack.splice(-1, 0, match)
244244
})
245-
} else {
246245
}
247246

248247
pathsLoaded = true
@@ -281,7 +280,7 @@ Api.prototype.redirectListener = function (req, res) {
281280
var location = 'https://' + hostname + ':' + port + req.url
282281

283282
res.setHeader('Location', location)
284-
res.statusCode = 301
283+
res.statusCode = 302
285284
res.end()
286285
}
287286

dadi/lib/auth/bearer.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ BearerAuthStrategy.prototype.getToken = function (authStrategy, done) {
2828
credentials: strategy.credentials,
2929
wallet: 'file',
3030
walletOptions: {
31-
path: config.get('paths.tokenWallets') +
31+
path:
32+
config.get('paths.tokenWallets') +
3233
'/' +
3334
help.generateTokenWalletFilename(
3435
strategy.host,

dadi/lib/cache/datasource.js

-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ var DatasourceCache = function () {
2121
this.cacheOptions = config.get('caching')
2222
this.cache = new DadiCache(this.cacheOptions)
2323

24-
DatasourceCache.numInstances = (DatasourceCache.numInstances || 0) + 1
25-
// console.log('DatasourceCache:', DatasourceCache.numInstances)
26-
2724
var directoryEnabled = this.cacheOptions.directory.enabled
2825
var redisEnabled = this.cacheOptions.redis.enabled
2926

dadi/lib/cache/index.js

+96-49
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ var crypto = require('crypto')
66
var debug = require('debug')('web:cache')
77
var path = require('path')
88
var url = require('url')
9+
var fs = require('fs')
10+
11+
var compressible = require('compressible')
12+
var mime = require('mime-types')
13+
var etag = require('etag')
914

1015
var config = require(path.join(__dirname, '/../../../config.js'))
16+
var help = require(path.join(__dirname, '/../help'))
1117

1218
var DadiCache = require('@dadi/cache')
1319

@@ -20,9 +26,6 @@ var Cache = function (server) {
2026
this.server = server
2127
this.cache = new DadiCache(config.get('caching'))
2228

23-
Cache.numInstances = (Cache.numInstances || 0) + 1
24-
// console.log('Cache:', Cache.numInstances)
25-
2629
var directoryEnabled = config.get('caching.directory.enabled')
2730
var redisEnabled = config.get('caching.redis.enabled')
2831

@@ -46,31 +49,29 @@ module.exports = function (server) {
4649
* @returns {Boolean}
4750
*/
4851
Cache.prototype.cachingEnabled = function (req) {
52+
// Check it is not a json view
4953
var query = url.parse(req.url, true).query
50-
if (query.json && query.json !== 'false') {
51-
return false
52-
}
54+
if (query.json && query.json !== 'false') return false
5355

54-
if (config.get('debug')) {
55-
return false
56-
}
56+
// Disable cache for debug mode
57+
if (config.get('debug')) return false
5758

58-
var endpoint = this.getEndpointMatchingRequest(req)
59+
// if it's in the endpoint and caching is enabled
60+
var endpoint = this.getEndpoint(req)
5961

60-
if (!endpoint) {
61-
endpoint = this.getEndpointMatchingLoadedPaths(req)
62-
}
62+
if (endpoint) {
63+
this.options.cache =
64+
typeof endpoint.page.settings.cache !== 'undefined'
65+
? endpoint.page.settings.cache
66+
: this.enabled
6367

64-
// not found in the loaded routes, let's not bother caching
65-
if (!endpoint) return false
66-
67-
if (endpoint.page && endpoint.page.settings) {
68-
this.options = endpoint.page.settings
68+
return this.enabled && this.options.cache
6969
} else {
70-
this.options.cache = false
71-
}
70+
// Otherwise it might be in the public folder
71+
var file = url.parse(req.url).pathname
7272

73-
return this.enabled && (this.options.cache || false)
73+
return compressible(mime.lookup(file))
74+
}
7475
}
7576

7677
/**
@@ -80,12 +81,7 @@ Cache.prototype.cachingEnabled = function (req) {
8081
*/
8182
Cache.prototype.getEndpointMatchingRequest = function (req) {
8283
var endpoints = this.server.components
83-
var requestUrl = url.parse(req.url, true).pathname
84-
85-
// strip trailing slash before testing
86-
if (requestUrl !== '/' && requestUrl[requestUrl.length - 1] === '/') {
87-
requestUrl = requestUrl.substring(0, requestUrl.length - 1)
88-
}
84+
var requestUrl = url.parse(req.url, true).pathname.replace(/\/+$/, '')
8985

9086
// get the host key that matches the request's host header
9187
var virtualHosts = config.get('virtualHosts')
@@ -96,7 +92,7 @@ Cache.prototype.getEndpointMatchingRequest = function (req) {
9692
}) || ''
9793

9894
// check if there is a match in the loaded routes for the current request URL
99-
var endpoint = _.find(endpoints, endpoint => {
95+
return _.find(endpoints, endpoint => {
10096
var paths = _.pluck(endpoint.page.routes, 'path')
10197
return (
10298
_.contains(paths, requestUrl) &&
@@ -105,9 +101,8 @@ Cache.prototype.getEndpointMatchingRequest = function (req) {
105101
: true)
106102
)
107103
})
108-
109-
return endpoint
110104
}
105+
111106
/**
112107
* Retrieves the page component that best matches the paths loaded in api/index.js
113108
* @param {IncomingMessage} req - the current HTTP request
@@ -130,14 +125,23 @@ Cache.prototype.getEndpointMatchingLoadedPaths = function (req) {
130125
* @param {IncomingMessage} req - the current HTTP request
131126
* @returns {string}
132127
*/
133-
Cache.prototype.getEndpointContentType = function (req) {
134-
var endpoint = this.getEndpointMatchingRequest(req)
128+
Cache.prototype.getReqContentType = function (req) {
129+
// Check for content-type in the page json
130+
var endpoint = this.getEndpoint(req)
135131

136-
if (!endpoint) {
137-
endpoint = this.getEndpointMatchingLoadedPaths(req)
138-
}
132+
return endpoint && endpoint.page && endpoint.page.contentType
133+
? endpoint.page.contentType
134+
: false
135+
}
136+
137+
/**
138+
* Adds the Cache middleware to the stack
139+
*/
140+
Cache.prototype.getEndpoint = function (req) {
141+
var endpoint = this.getEndpointMatchingRequest(req)
142+
if (!endpoint) endpoint = this.getEndpointMatchingLoadedPaths(req)
139143

140-
return endpoint.page.contentType
144+
return endpoint
141145
}
142146

143147
/**
@@ -154,9 +158,15 @@ Cache.prototype.init = function () {
154158
this.server.app.use(function cache (req, res, next) {
155159
var enabled = self.cachingEnabled(req)
156160

161+
if (!enabled) return next()
162+
157163
debug('%s%s, cache enabled: %s', req.headers.host, req.url, enabled)
158164

159-
if (!enabled) return next()
165+
// Check it's a page
166+
if (!self.getEndpoint(req)) return next()
167+
168+
// get contentType that current endpoint requires
169+
var contentType = self.getReqContentType(req)
160170

161171
// only cache GET requests
162172
if (req.method && req.method.toLowerCase() !== 'get') return next()
@@ -183,33 +193,63 @@ Cache.prototype.init = function () {
183193
var noCache =
184194
query.cache && query.cache.toString().toLowerCase() === 'false'
185195

186-
// get contentType that current endpoint requires
187-
var contentType = self.getEndpointContentType(req)
196+
// File extension for cache file
197+
var cacheExt =
198+
compressible(contentType) && help.canCompress(req.headers)
199+
? '.' + help.canCompress(req.headers)
200+
: null
201+
202+
var opts = {
203+
directory: { extension: mime.extension(contentType) + cacheExt }
204+
}
205+
206+
// Compression settings
207+
var shouldCompress = compressible(contentType)
208+
? help.canCompress(req.headers)
209+
: false
188210

189211
// attempt to get from the cache
190212
self.cache
191-
.get(filename)
213+
.get(filename, opts)
192214
.then(stream => {
193215
debug('serving %s%s from cache', req.headers.host, req.url)
194216

195-
res.setHeader('X-Cache-Lookup', 'HIT')
196-
197217
if (noCache) {
218+
res.setHeader('X-Cache-Lookup', 'HIT')
198219
res.setHeader('X-Cache', 'MISS')
199220
return next()
200221
}
201222

223+
var headers = {
224+
'X-Cache-Lookup': 'HIT',
225+
'X-Cache': 'HIT',
226+
'Content-Type': contentType,
227+
'Cache-Control':
228+
config.get('headers.cacheControl')[contentType] ||
229+
'public, max-age=86400'
230+
}
231+
232+
// Add compression headers
233+
if (shouldCompress) headers['Content-Encoding'] = shouldCompress
234+
235+
// Add extra headers
236+
stream.on('open', fd => {
237+
fs.fstat(fd, (_, stats) => {
238+
res.setHeader('Content-Length', stats.size)
239+
res.setHeader('ETag', etag(stats))
240+
})
241+
})
242+
202243
res.statusCode = 200
203-
res.setHeader('X-Cache', 'HIT')
204-
res.setHeader('Content-Type', contentType)
244+
Object.keys(headers).map(i => res.setHeader(i, headers[i]))
205245

206-
// send cached content back
207246
stream.pipe(res)
208247
})
209248
.catch(() => {
210249
// not found in cache
211250
res.setHeader('X-Cache', 'MISS')
212251
res.setHeader('X-Cache-Lookup', 'MISS')
252+
213253
return cacheResponse()
214254
})
215255

@@ -222,24 +262,31 @@ Cache.prototype.init = function () {
222262
var _end = res.end
223263
var _write = res.write
224264

225-
var data = ''
265+
var data = []
226266

227267
res.write = function (chunk) {
268+
if (chunk) data.push(chunk)
269+
228270
_write.apply(res, arguments)
229271
}
230272

231273
res.end = function (chunk) {
232274
// respond before attempting to cache
233275
_end.apply(res, arguments)
234276

235-
if (chunk) data += chunk
277+
if (chunk && !data.length) data.push(chunk)
236278

237279
// if response is not 200 don't cache
238280
if (res.statusCode !== 200) return
239281

240-
// cache the content
241-
self.cache.set(filename, data).then(() => {})
282+
// cache the content, with applicable file extension
283+
try {
284+
self.cache.set(filename, Buffer.concat(data), opts).then(() => {})
285+
} catch (e) {
286+
console.log('Could not cache content: ' + requestUrl)
287+
}
242288
}
289+
243290
return next()
244291
}
245292
})

0 commit comments

Comments
 (0)