diff --git a/lib/App/etag.js b/lib/App/etag.js
new file mode 100644
index 0000000..613ef2f
--- /dev/null
+++ b/lib/App/etag.js
@@ -0,0 +1,5 @@
+const crypto = require('crypto')
+
+module.exports = function(data) {
+ return crypto.createHash('sha1').update(data).digest('hex')
+}
\ No newline at end of file
diff --git a/lib/App/onFileRequest.js b/lib/App/onFileRequest.js
index 5785234..2f0b440 100644
--- a/lib/App/onFileRequest.js
+++ b/lib/App/onFileRequest.js
@@ -1,5 +1,4 @@
const fs = require('fs')
-const xxhash = require('xxhash')
const path = require('path')
const getMIMEType = require('mime-types').lookup
@@ -52,7 +51,7 @@ module.exports = function(request, response) {
const headers = {
'Content-Length': stats.size,
- 'ETag': xxhash.hash(Buffer.from(stats.mtime.toString()), 0),
+ 'ETag': this.etag(stats.mtime.toString()),
'Cache-Control': 'max-age=864000'
}
diff --git a/lib/App/routePage.js b/lib/App/routePage.js
index ed4d5b6..af07d68 100644
--- a/lib/App/routePage.js
+++ b/lib/App/routePage.js
@@ -1,5 +1,6 @@
-const xxhash = require('xxhash')
const zlib = require('zlib')
+const crypto = require('crypto')
+const NodeCache = require('node-cache')
// This should be close to the MTU size of a TCP packet.
// Regarding performance it makes no sense to compress smaller files.
@@ -9,6 +10,15 @@ const zlib = require('zlib')
// we're trying to optimize for performance, not bandwidth.
const gzipThreshold = 1450
+// We cache the gzipped response using the ETag as a key
+// for the in-memory cache. By default we save the cached
+// responses only for 10 minutes to prevent using a lot of memory.
+const gzipCache = new NodeCache({
+ stdTTL: 600,
+ checkperiod: 610
+})
+
+// GZip compression options
const bestCompressionOptions = {
level: zlib.Z_BEST_COMPRESSION
}
@@ -17,14 +27,19 @@ const fastCompressionOptions = {
level: zlib.Z_BEST_SPEED
}
+// Boilerplate HTML content
const mobileMetaTag = ''
const manifestTag = ''
const scriptTag = ''
+// Hash a UTF-8 string using SHA-1
+function hash(code) {
+ return crypto.createHash('sha1').update(code, 'utf8').digest('hex')
+}
+
// Generic respond functionality
-const respond = function(code, headers, request, response) {
- let codeBuffer = Buffer.from(code)
- let etag = xxhash.hash(codeBuffer, 0).toString()
+function respond(code, headers, request, response) {
+ let etag = hash(code)
if(request.headers['if-none-match'] === etag) {
response.writeHead(304)
@@ -42,24 +57,36 @@ const respond = function(code, headers, request, response) {
localHeaders.ETag = etag
// Send response
- if(!request.fromProxy && code.length >= gzipThreshold) {
+ if(code.length >= gzipThreshold) {
localHeaders['Content-Encoding'] = 'gzip'
+ let gzippedCode = gzipCache.get(etag)
+
+ if(gzippedCode !== undefined) {
+ localHeaders['Content-Length'] = gzippedCode.length
+
+ response.writeHead(response.statusCode || 200, localHeaders)
+ response.end(gzippedCode)
+ return
+ }
+
zlib.gzip(code, fastCompressionOptions, function(error, gzippedCode) {
localHeaders['Content-Length'] = gzippedCode.length
response.writeHead(response.statusCode || 200, localHeaders)
response.end(gzippedCode)
+
+ gzipCache.set(etag, gzippedCode)
})
} else {
- localHeaders['Content-Length'] = Buffer.byteLength(code, 'utf8')
+ localHeaders['Content-Length'] = Buffer.byteLength(code, 'utf8') //codeBuffer.length
response.writeHead(response.statusCode || 200, localHeaders)
response.end(code)
}
}
-const respondStatic = function(code, baseHeaders) {
+function respondStatic(code, baseHeaders) {
const headers = Object.assign({}, baseHeaders)
headers['Cache-Control'] = 'no-cache'
@@ -72,7 +99,7 @@ const respondStatic = function(code, baseHeaders) {
const gzippedCode = zlib.gzipSync(code, bestCompressionOptions)
headers['Content-Length'] = gzippedCode.length
- headers.ETag = xxhash.hash(Buffer.from(gzippedCode), 0).toString()
+ headers.ETag = hash(gzippedCode)
return function(request, response) {
if(request.headers['if-none-match'] === headers.ETag) {
@@ -89,7 +116,7 @@ const respondStatic = function(code, baseHeaders) {
// Keep in mind that the client needs to uncompress and that takes time as well.
// Therefore we send an uncompressed version.
headers['Content-Length'] = Buffer.byteLength(code, 'utf8')
- headers.ETag = xxhash.hash(Buffer.from(code), 0).toString()
+ headers.ETag = hash(code)
return function(request, response) {
if(request.headers['if-none-match'] === headers.ETag) {
@@ -188,6 +215,9 @@ module.exports = function(page) {
return
}
+ // So we can avoid the bind calls
+ const app = this
+
// Routing
if(page.controller) {
if(page.template) {
@@ -203,24 +233,24 @@ module.exports = function(page) {
page.httpVerbs.forEach(method => {
const next = page.controller[method].bind(page.controller)
- this[method](page.url, (request, response) => {
- renderLayout(request, layoutControllerParams => {
- response.render = params => {
- const code = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params)))
+ this[method](page.url, function requestHandler(request, response) {
+ renderLayout(request, function handleLayout(layoutControllerParams) {
+ response.render = function handlePage(params) {
+ const content = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params)))
if(layoutControllerParams) {
if(layoutData || request.globals)
Object.assign(layoutControllerParams, request.globals, layoutData)
- layoutControllerParams.content = code
- layoutControllerParams.app = this
+ layoutControllerParams.content = content
+ layoutControllerParams.app = app
layoutControllerParams.page = page
respond(renderLayoutTemplate(layoutControllerParams), headers, request, response)
} else {
respond(renderLayoutTemplate(Object.assign({
- content: code,
- app: this,
+ content,
+ app,
page
},
request.globals,
@@ -242,13 +272,13 @@ module.exports = function(page) {
page.httpVerbs.forEach(method => {
const runPageController = page.controller[method].bind(page.controller)
- this[method](page.url, (request, response) => {
- response.render = params => {
- const code = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params)))
+ this[method](page.url, function requestHandler(request, response) {
+ response.render = function handlePage(params) {
+ const content = page.wrap(renderPageTemplate(Object.assign({}, page.defaultParams, request.globals, page.json, params)))
const layoutParams = {
- content: code,
- app: this,
+ content,
+ app,
page
}
@@ -280,8 +310,8 @@ module.exports = function(page) {
const renderLayout = this.layout.controller.render.bind(this.layout.controller)
const layoutData = this.layout.json
- this.get(page.url, (request, response) => {
- renderLayout(request, layoutControllerParams => {
+ this.get(page.url, function(request, response) {
+ renderLayout(request, function(layoutControllerParams) {
if(!layoutControllerParams)
layoutControllerParams = {}
@@ -289,7 +319,7 @@ module.exports = function(page) {
Object.assign(layoutControllerParams, request.globals, layoutData)
layoutControllerParams.content = page.code
- layoutControllerParams.app = this
+ layoutControllerParams.app = app
layoutControllerParams.page = page
respond(renderLayoutTemplate(layoutControllerParams), headers, request, response)
diff --git a/lib/App/routeScripts.js b/lib/App/routeScripts.js
index d2d7831..714b310 100644
--- a/lib/App/routeScripts.js
+++ b/lib/App/routeScripts.js
@@ -1,5 +1,4 @@
const compress = require('brotli').compress
-const xxhash = require('xxhash')
module.exports = function() {
const combinedJS = '"use strict";' + (this.liveReload ? this.liveReload.script : '') + this.pluginScripts.join(';') + this.js.map(script => script.code).join(';')
@@ -27,7 +26,7 @@ module.exports = function() {
compressedScriptsHeaders['Content-Length'] = Buffer.byteLength(compressedScripts, 'utf8')
}
- compressedScriptsHeaders.ETag = xxhash.hash(Buffer.from(compressedScripts), 0)
+ compressedScriptsHeaders.ETag = this.etag(compressedScripts)
// Route
this.server.routes.GET['scripts.js'] = (request, response) => {
diff --git a/lib/App/routeStyles.js b/lib/App/routeStyles.js
index 5f293f6..da63447 100644
--- a/lib/App/routeStyles.js
+++ b/lib/App/routeStyles.js
@@ -1,5 +1,4 @@
const compress = require('brotli').compress
-const xxhash = require('xxhash')
module.exports = function() {
this.combinedCSS = this.pluginStyles.join(' ') + this.css.map(style => style.code).join(' ')
@@ -27,7 +26,7 @@ module.exports = function() {
compressedStylesHeaders['Content-Length'] = Buffer.byteLength(compressedStyles, 'utf8')
}
- compressedStylesHeaders.ETag = xxhash.hash(Buffer.from(compressedStyles), 0)
+ compressedStylesHeaders.ETag = this.etag(compressedStyles)
// Route
this.server.routes.GET['styles.css'] = (request, response) => {
diff --git a/lib/Server/run.js b/lib/Server/run.js
index 516b92c..27d1776 100644
--- a/lib/Server/run.js
+++ b/lib/Server/run.js
@@ -25,19 +25,15 @@ let run = function(app) {
this.redirectServer = require('http').createServer((request, response) => {
// If there is a proxy sending our HTTP contents to an HTTPS client we allow
// accessing the contents directly via HTTP without redirecting to HTTPS.
- if(request.headers['x-forwarded-proto'] === 'https') {
- request.fromProxy = true
+ if(request.headers['x-forwarded-proto'] === 'https')
return this.onRequest(request, response)
- }
// In production mode we allow local proxy servers to access HTTP contents.
if(app.production) {
let remoteAddress = request.connection.remoteAddress
- if(remoteAddress === '::ffff:127.0.0.1' || remoteAddress === '127.0.0.1') {
- request.fromProxy = true
+ if(remoteAddress === '::ffff:127.0.0.1' || remoteAddress === '127.0.0.1')
return this.onRequest(request, response)
- }
}
// If there's no proxy (e.g. direct browser access to HTTP) we redirect
diff --git a/package.json b/package.json
index 528cf91..4355009 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "aero",
- "version": "2.0.3",
+ "version": "2.0.4",
"description": "The fastest node.js framework.",
"repository": "aerojs/aero",
"homepage": "https://github.com/aerojs/aero",
@@ -17,6 +17,7 @@
"markdown-it": "8.x",
"mime-types": "2.x",
"mkdirp": "0.x",
+ "node-cache": "4.x",
"node-watch": "0.x",
"pem": "1.x",
"pug": "2.0.0-beta6",
@@ -27,8 +28,7 @@
"stylus": ">=0.54.0",
"uglify-js-harmony": ">=2.6",
"uws": "0.x",
- "ws": ">=1.1.1",
- "xxhash": "0.x"
+ "ws": ">=1.1.1"
},
"devDependencies": {
"aero-ajax": "*",
diff --git a/test/index.js b/test/index.js
index 51ff323..318252b 100644
--- a/test/index.js
+++ b/test/index.js
@@ -79,7 +79,6 @@ require('strict-mode')(function () {
assert(app.config.ports)
assert(app.config.ports.http)
assert(app.config.ports.https)
- assert(app.config.ports.liveReload)
assert(app.package)
assert(app.package.name)
diff --git a/yarn.lock b/yarn.lock
index d5eafd9..f39e958 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -210,8 +210,8 @@ character-parser@^2.1.1:
is-regex "^1.0.3"
clean-css@^3.2.10, clean-css@^3.3.0:
- version "3.4.20"
- resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.20.tgz#c0d8963b5448e030f0bcd3ddd0dac4dfe3dea501"
+ version "3.4.21"
+ resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-3.4.21.tgz#2101d5dbd19d63dbc16a75ebd570e7c33948f65b"
dependencies:
commander "2.8.x"
source-map "0.4.x"
@@ -232,6 +232,10 @@ cliui@^3.0.3:
strip-ansi "^3.0.1"
wrap-ansi "^2.0.0"
+clone@1.0.x:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
+
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
@@ -816,6 +820,10 @@ load-class@1.x:
dependencies:
bluebird "*"
+lodash@4.x:
+ version "4.17.2"
+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.2.tgz#34a3055babe04ce42467b607d700072c7ff6bf42"
+
log-driver@1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.5.tgz#7ae4ec257302fd790d557cb10c97100d857b0056"
@@ -886,9 +894,12 @@ multi-stage-sourcemap@0.2.1:
dependencies:
source-map "^0.1.34"
-nan@^2.4.0:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/nan/-/nan-2.4.0.tgz#fb3c59d45fe4effe215f0b890f8adf6eb32d2232"
+node-cache@4.x:
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-4.1.0.tgz#2a6a66460bf063781138206988237ec02c135157"
+ dependencies:
+ clone "1.0.x"
+ lodash "4.x"
node-uuid@~1.4.7:
version "1.4.7"
@@ -1611,12 +1622,6 @@ xtend@^4.0.0, xtend@~4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
-xxhash@0.x:
- version "0.2.4"
- resolved "https://registry.yarnpkg.com/xxhash/-/xxhash-0.2.4.tgz#8b8a48162cfccc21b920fa500261187d40216c39"
- dependencies:
- nan "^2.4.0"
-
y18n@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"