Permalink
Browse files

Merge pull request #15 from Swaagie/master

Support both deflate and gzip
  • Loading branch information...
2 parents a04cff7 + 43b5ab3 commit 0fa17c2fc58e94d07b4dcaad8024518a57e8bd8a @3rd-Eden committed Mar 10, 2013
Showing with 117 additions and 18 deletions.
  1. +25 −10 index.js
  2. +1 −1 lib/pull.js
  3. +3 −1 package.json
  4. +88 −6 test/server.test.js
View
@@ -38,7 +38,6 @@ function Versions(options) {
// Read in the various of configurations that we want to merge in to our own
// configuration object.
- //
if (!options.cloned) {
this.read('../../node_modules/package.json'); // For version number
this.read('./versions.json'); // For our defaults
@@ -298,16 +297,21 @@ Versions.prototype.initialize = function initialize(type) {
*/
Versions.prototype.write = function write(req, res, data) {
var age = this.get('max age')
- , body = data.buffer;
+ , body = data.buffer
+ , type;
// Check if we have a GZIP version of the content.
- if ('gzip' in data) {
- if (this.allows('gzip', req)) {
- res.setHeader('Content-Encoding', 'gzip');
- body = data.gzip;
- this.metrics.incr('gzip');
+ if ('compressed' in data) {
+ // Force GZIP over deflate as it is more stable.
+ if (this.allows('deflate', req) && data.compressed.deflate) type = 'deflate';
+ if (this.allows('gzip', req) && data.compressed.gzip) type = 'gzip';
+
+ if (type) {
+ res.setHeader('Content-Encoding', type);
+ body = data.compressed[type];
+ this.metrics.incr(type);
} else {
- this.metrics.incr('gzip blocked');
+ this.metrics.incr('compression blocked');
}
}
@@ -330,7 +334,7 @@ Versions.prototype.write = function write(req, res, data) {
* @returns {Boolean}
* @api public
*/
-Versions.prototype.allows = function supports(what, req) {
+Versions.prototype.allows = function allows(what, req) {
var headers = req.headers;
switch (what) {
@@ -361,6 +365,9 @@ Versions.prototype.allows = function supports(what, req) {
return obfuscated;
+ case 'deflate':
+ return !!~(headers['accept-encoding'] || '').toLowerCase().indexOf('deflate');
+
// Do we allow this extension to be served from our server?
case 'extension':
req.extension = req.extension || path.extname(req.url);
@@ -391,9 +398,17 @@ Versions.prototype.allows = function supports(what, req) {
* @api private
*/
Versions.prototype.compress = function compress(type, data, callback) {
+ var compressed = Object.create(null);
+
+ function iterator(error, content, method) {
+ compressed[method] = !error ? content : null;
+ if (Object.keys(compressed).length === 2) callback(null, compressed);
+ }
+
// Only these types of content should be gzipped.
if (/json|text|javascript|xml/i.test(type || '') || type in this.compressTypes) {
- zlib.gzip(data, callback);
+ zlib.gzip(data, function (error, content) { iterator(error, content, 'gzip'); });
+ zlib.deflate(data, function (error, content) { iterator(error, content, 'deflate'); });
} else {
process.nextTick(callback);
}
View
@@ -65,7 +65,7 @@ module.exports = function pull(req, res, next) {
// Compress the data and pass in the origin response so our compression
// function can check if this resource needs to be compressed
self.compress(data['content-type'], body, function compiling(err, compressed) {
- if (compressed && !err) data.gzip = compressed;
+ if (compressed && !err) data.compressed = compressed;
res.setHeader('X-Cache', 'Pull');
self.write(req, res, data);
View
@@ -40,6 +40,8 @@
"devDependencies": {
"chai": "1.5.x",
"mocha": "1.8.x",
- "redis": "0.8.x"
+ "redis": "0.8.x",
+ "sinon": "1.6.x",
+ "sinon-chai": "2.3.x"
}
}
View
@@ -3,8 +3,11 @@ describe('versions()', function () {
var chai = require('chai')
, path = require('path')
+ , sinon = require('sinon')
+ , sinonChai = require('sinon-chai')
, expect = chai.expect;
+ chai.use(sinonChai);
chai.Assertion.includeStack = true;
describe('initialization', function () {
@@ -235,18 +238,87 @@ describe('versions()', function () {
});
describe('#write', function () {
- var versions;
+ var versions, req, res, data;
+
before(function () {
versions = require('../').clone();
versions.logger.notification = 8;
+ versions.initialize('server');
+ });
+
+ beforeEach(function () {
+ res = {
+ setHeader: sinon.stub()
+ , end: sinon.stub()
+ };
+
+ data = {
+ buffer: 'test'
+ , 'content-type': 'application/json'
+ , 'last-modified': 1234
+ , compressed: {
+ deflate: 'body with deflated content'
+ , gzip: 'body with gzipped content'
+ }
+ };
});
after(function (done) {
versions.end(done);
});
- it('sets the correct headers');
- it('decided which content to use');
+ it('sets the correct headers', function () {
+ var age = 86400000
+ , exp = new Date(Date.now() + age).toUTCString();
+ req = { headers: { 'accept-encoding': 'cakes' } };
+ versions.set('max age', age).write(req, res, data);
+
+ expect(res.setHeader.callCount).to.be.equal(5);
+ expect(res.setHeader).to.be.calledWith('Expires', exp);
+ expect(res.setHeader).to.be.calledWith('Cache-Control', 'max-age='+ age +', public');
+ expect(res.setHeader).to.be.calledWith('Last-Modified', data['last-modified']);
+ expect(res.setHeader).to.be.calledWith('Content-Type', data['content-type']);
+ expect(res.setHeader).to.be.calledWith('Content-Length', data.buffer.length);
+ });
+
+ it('returns uncompressed buffer if non requested', function () {
+ req = { headers: { 'accept-encoding': 'cakes' } };
+ versions.write(req, res, data);
+
+ expect(res.setHeader.callCount).to.be.equal(5);
+ expect(res.end).to.be.calledWith('test');
+ expect(res.end).to.be.calledOnce;
+ });
+
+ it('returns uncompressed buffer no compression available', function () {
+ req = { headers: { 'accept-encoding': 'gzip,deflate' } };
+ data = { buffer: 'test' };
+ versions.write(req, res, data);
+
+ expect(res.setHeader.callCount).to.be.equal(5);
+ expect(res.end).to.be.calledWith('test');
+ expect(res.end).to.be.calledOnce;
+ });
+
+ it('prefers gzip over deflate', function () {
+ req = { headers: { 'accept-encoding': 'gzip,deflate' } };
+ versions.write(req, res, data);
+
+ expect(res.setHeader).to.be.calledWith('Content-Encoding', 'gzip');
+ expect(res.setHeader.callCount).to.be.equal(6);
+ expect(res.end).to.be.calledWith('body with gzipped content');
+ expect(res.end).to.be.calledOnce;
+ });
+
+ it('returns compression type deflate if requested', function () {
+ req = { headers: { 'accept-encoding': 'deflate' } };
+ versions.write(req, res, data);
+
+ expect(res.setHeader).to.be.calledWith('Content-Encoding', 'deflate');
+ expect(res.end).to.be.calledWith('body with deflated content');
+ expect(res.setHeader.callCount).to.be.equal(6);
+ expect(res.end).to.be.calledOnce;
+ });
});
describe('#allows', function () {
@@ -280,6 +352,11 @@ describe('versions()', function () {
expect(versions.allows('gzip', decline)).to.equal(false);
});
+ it('detects deflate support', function () {
+ expect(versions.allows('deflate', accept)).to.equal(true);
+ expect(versions.allows('deflate', decline)).to.equal(false);
+ });
+
it('ignores IE6 without service pack', function () {
accept.headers['user-agent'] = 'Mozilla/5.0 (compatible; MSIE 6.0; Windows NT 5.1)';
@@ -374,9 +451,14 @@ describe('versions()', function () {
versions.compress('text/javascript', buffer, function (err, data) {
expect(err).to.equal(null);
- expect(Buffer.isBuffer(data)).to.equal(true);
- expect(data.toString()).to.not.equal(buffer.toString());
- expect(data.length).to.be.below(buffer.length);
+ expect(data).to.have.property('gzip');
+ expect(data).to.have.property('deflate');
+
+ Object.keys(data).forEach(function type (key) {
+ expect(Buffer.isBuffer(data[key])).to.equal(true);
+ expect(data[key].toString()).to.not.equal(buffer.toString());
+ expect(data[key].length).to.be.below(buffer.length);
+ });
done();
});

0 comments on commit 0fa17c2

Please sign in to comment.