├── .gitmodules ├── LICENSE ├── README ├── diffable.js ├── fileResourceManager.js ├── requestHandler.js └── resources ├── DJSBootstrap.js ├── DJSBootstrap.min.js ├── DeltaBootstrap.js ├── DeltaBootstrap.min.js ├── JsDictionaryBootstrap.js └── JsDictionaryBootstrap.min.js /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vcdiff.js"] 2 | path = vcdiff.js 3 | url = git://github.com/plotnikoff/vcdiff.js.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2010 Konstantin Plotnikov. All rights reserved. 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to 4 | deal in the Software without restriction, including without limitation the 5 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 6 | sell copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 17 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 18 | IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Diffable. 2 | "Diffable is a method for reducing page load latency by transmitting 3 | differential encodings of static files. It works by sending deltas between 4 | versions of files that reside in a browser's cache. In web applications, files 5 | such as Javascript, HTML, and CSS are often updated every week or more, but 6 | change very little between versions. This method significantly reduces the 7 | bandwidth and transmission time for these files and is implementable on all 8 | legacy browsers that support Javascript." 9 | How it works and reference Java implementation: http://code.google.com/p/diffable/ 10 | 11 | Connect-diffable. 12 | Middleware for Sencha's Connect (http://github.com/senchalabs/connect/) stack 13 | that implements algorithm described in previous section. 14 | 15 | Installation. 16 | git clone git://github.com/plotnikoff/connect-diffable.git 17 | cd connect-diffable 18 | git submodule init 19 | git submodule update 20 | 21 | Usage. 22 | require('../connect-diffable/diffable') - Module exports constructor for 23 | Diffable. 24 | 25 | Constuctor accepts configuration object: 26 | { 27 | 'diffableDir' : './diffable', 28 | 'resourceDir' : './static' 29 | } 30 | where 'diffableDir' points to directory where versions and deltas will be kept 31 | and 'resourceDir' is directory where static files reside. 32 | 33 | serve(fileName...) - method returns middleware function for Connect stack and 34 | adds files passed as parameters to diffable control, so when you change static 35 | file changes are tracked by middleware and appropriate delta and version files 36 | are created. Path is relative to 'resourceDir' 37 | 38 | Also it is possible to generate docs by running jsdoc-toolkit on diffable.js 39 | 40 | Example. 41 | Say we'd like to serve file app.js with diffable. 42 | 43 | Simple server that serves static files. 44 | 45 | server.js 46 | var Connect = require('connect'), 47 | diffable = require('../connect-diffable/diffable'), 48 | diff = new diffable({ 49 | 'diffableDir' : './diffable', 50 | 'resourceDir' : './static' 51 | }); 52 | 53 | module.exports = Connect.createServer( 54 | diff.serve('/js/app.js'), 55 | Connect.staticProvider('./static') 56 | ); 57 | 58 | 59 | Also we need to change index.html in which we use this secondary resource: 60 | 61 | index.html 62 | 63 | 64 |
65 | 66 |
18 | *
19 | * {
20 | * "diffableDir" : '/path/to/directory'
21 | * "resourceDir" : '/path/to/directory'
22 | * }
23 | *
24 | *
25 | * diffableDir
- directory where delta and version files will
26 | * be storedresourceDir
- directory where diffable can find static files.
28 | */
29 | diffable = function (config) {
30 | this.dir = fs.realpathSync(config.diffableDir);
31 | this.resourceDir = config.resourceDir;
32 | this.provider = requestHandler({
33 | 'resourceDir': config.resourceDir,
34 | 'frm': frm,
35 | 'diffableRoot': this.dir,
36 | 'logger' : config.logger
37 | });
38 | that = this;
39 | };
40 |
41 | /**
42 | * Method adds resources that should be served with diffable.
43 | * @private
44 | * @param {String} path Path to resource that should be served by diffable.
45 | * Path is relative to resourceDir
.
46 | */
47 | diffable.prototype.watch = function (path) {
48 | //resolving absolute path to resource being tracked
49 | fs.realpath(this.resourceDir + path, function (err, resolvedPath) {
50 | if (err) {
51 | throw err;
52 | }
53 |
54 | var hash = crypto.createHash('md5').update(resolvedPath),
55 | resourceDir = that.dir + '/' + hash.digest('hex');
56 |
57 | //Create resource directory (if doesn't exist) name of directory is
58 | // md5 hash of resource's absolute path
59 | fs.mkdir(resourceDir, 0755, function (err) {
60 |
61 | //create first version of resource
62 | frm.putResource(resolvedPath, resourceDir);
63 |
64 | //add callback to track file changes
65 | fs.watchFile(resolvedPath, function (curr, prev) {
66 |
67 | //if changes were made add new version of resource
68 | frm.putResource(resolvedPath, resourceDir);
69 | });
70 | });
71 | });
72 | };
73 |
74 | /**
75 | * Method adds files to diffable control, and returns Connect middleware.
76 | * Returns connect stack interface, if request contains data that is
77 | * relevant to diffable this middleware will serve appropriate version
78 | * and/or delta files.
79 | * @public
80 | * @param {String} filename... varargs names of the files to look for. Paths
81 | * are relative to resourceDir
in configuration object
82 | * @returns {Function}
83 | */
84 | diffable.prototype.serve = function () {
85 | var i = 0, len = arguments.length
86 | if (len >= 1) {
87 | for (; i < len; i += 1) {
88 | that.watch(arguments[i]);
89 | }
90 | return that.provider;
91 | } else {
92 | console.log('Diffable: There are no files under control')
93 | return function (req, res, next) {
94 | next();
95 | }
96 | }
97 | }
98 |
99 | return diffable;
100 |
101 | }());
102 |
--------------------------------------------------------------------------------
/fileResourceManager.js:
--------------------------------------------------------------------------------
1 | /*global require, module*/
2 |
3 | var fs = require('fs'),
4 | crypto = require('crypto'),
5 | vcdiff = require('./vcdiff.js/vcdiff');
6 |
7 | /**
8 | * Class keeps track of resources and thier versions/deltas.
9 | */
10 | var FileResourceManager = function () {
11 | this.resources = {};
12 | this.versions = {};
13 | this.currentVersion = null;
14 | };
15 |
16 | FileResourceManager.prototype = {
17 |
18 | /**
19 | * Method returns resource path hash if resource is tracked by
20 | * FileResourceManager
21 | * @param {String} path
22 | */
23 | getResourceHash : function (path) {
24 | return this.resources[path];
25 | },
26 |
27 | /**
28 | * Method returns most recent version hash for given resource.
29 | * @param {String} resourceHash
30 | */
31 | getVersionHash : function (resourceHash) {
32 | return this.versions[resourceHash];
33 | },
34 |
35 | /**
36 | * Method is responsible for version and delta files createion.
37 | * @param {String} resolvedPath
38 | * @param {String} resourceDir
39 | */
40 | putResource : function (resolvedPath, resourceDir) {
41 | var that = this;
42 |
43 | //read resource file
44 | fs.readFile(resolvedPath, function (err, data) {
45 | if (err) {
46 | throw err;
47 | }
48 |
49 | //generate md5 hash from resource's data
50 | var hash = crypto.createHash('md5').update(data),
51 | resourceName = hash.digest('hex') + '.version',
52 | resourceHash = resourceDir.split('/').reverse()[0];
53 |
54 | that.resources[resolvedPath] = resourceHash;
55 |
56 | //create version file
57 | fs.writeFile(resourceDir + '/' + resourceName, data, function (err) {
58 | if (err) {
59 | throw err;
60 | }
61 | that.currentVersion = resourceName;
62 | that.versions[resourceHash] = resourceName.split('.')[0];
63 |
64 | //read resource directory to fetch older versions of resource
65 | fs.readdir(resourceDir, function (err, files) {
66 | if (err) {
67 | throw err;
68 | }
69 | var i, len = files.length, vcd = new vcdiff.Vcdiff();
70 |
71 | //loop over older versions to create diffs
72 | for (i = 0;i < len; i += 1) {
73 |
74 | //check that this is not current version and isn't a diff file
75 | if (files[i] !== that.currentVersion &&
76 | files[i].split('.')[1] !== 'diff') {
77 | (function (fileName) {
78 | //read file with older version and generate diff file
79 | fs.readFile(resourceDir + '/' + fileName, 'utf8',
80 | function (err, dictData) {
81 | var diff = vcd.encode(dictData.toString(),
82 | data.toString()),
83 | diffName = fileName.split('.')[0] + '_' +
84 | that.currentVersion.split('.')[0] + '.diff';
85 | fs.writeFile(resourceDir + '/' + diffName,
86 | JSON.stringify(diff), function (err) {
87 | if (err) {
88 | throw err;
89 | }
90 | }
91 | );
92 | }
93 | );
94 | }(files[i]));
95 | }
96 | }
97 | });
98 | });
99 | });
100 | }
101 |
102 | };
103 |
104 | module.exports = FileResourceManager;
--------------------------------------------------------------------------------
/requestHandler.js:
--------------------------------------------------------------------------------
1 | /*jslint nomen:false, regexp:false*/
2 |
3 | /*global module, require, __dirname, process*/
4 |
5 | var fs = require('fs'),
6 | Path = require('path'),
7 | parseUrl = require('url').parse,
8 | queryString = require('querystring'), that;
9 | /**
10 | * Function sends response with aggresive caching
11 | * @param {Object} res
12 | * @param {Object} string
13 | */
14 | function sendForCaching(res, str) {
15 | var headers = {
16 | "Content-Type": 'text/javascript',
17 | "Content-Length": str.length,
18 | "Cache-Control": "public, max-age=63072000",
19 | "Last-Modified": new Date(2000, 1, 1).toUTCString(),
20 | "Expires": new Date().toUTCString()
21 | };
22 |
23 | res.writeHead(200, headers);
24 | res.end(str);
25 | }
26 |
27 | function sendConditional(res) {
28 | res.writeHead(304, {
29 | "Content-Type": 'text/javascript',
30 | "Last-Modified": new Date(2000, 1, 1).toUTCString(),
31 | "Expires": new Date().toUTCString(),
32 | "Cache-Control": "public, max-age=63072000"
33 | });
34 | res.end();
35 | }
36 |
37 | /**
38 | * @param {Object} options
39 | * @cfg {String} resourceDir
40 | * @cfg {FileResourceManager} frm
41 | * @cfg {String} diffableRoot
42 | */
43 | module.exports = function requestHandler(config) {
44 | var root = config.resourceDir,
45 | frm = config.frm,
46 | diffableRoot = config.diffableRoot,
47 | log = config.logger ? config.logger : function () {},
48 | suffix = process.env.NODE_ENV === 'production' ? '.min' : '',
49 | bootScript = fs.readFileSync(__dirname +
50 | '/resources/DJSBootstrap' + suffix + '.js', 'utf8'),
51 | deltaScript = fs.readFileSync(__dirname +
52 | '/resources/DeltaBootstrap' + suffix + '.js', 'utf8'),
53 | versionScript = fs.readFileSync(__dirname +
54 | '/resources/JsDictionaryBootstrap' + suffix + '.js', 'utf8');
55 |
56 | return function (req, res, next) {
57 | if (req.method !== 'GET' && req.method !== 'HEAD') {
58 | return next();
59 | }
60 |
61 | var filename, url = parseUrl(req.url), hashes, resHash, dictVerHash,
62 | targetVerHash;
63 |
64 | //Delta request handler
65 | function onDiffRead(err, diffData) {
66 | if (err) {
67 | return next();
68 | }
69 | var script = deltaScript;
70 | script = script.replace('{{DJS_RESOURCE_IDENTIFIER}}',
71 | resHash);
72 | script = script.replace('{{DJS_DIFF_CONTENT}}', diffData.toString());
73 | log({
74 | 'type' : 'delta',
75 | 'resource' : resHash,
76 | 'deltaFile' : dictVerHash + '_' + targetVerHash + '.diff'
77 | });
78 | sendForCaching(res, script);
79 | }
80 |
81 | //Version request handler
82 | function onJsRead(err, versionData) {
83 | if (err) {
84 | return next();
85 | }
86 | var script = versionScript;
87 | script = script.replace('{{DJS_RESOURCE_IDENTIFIER}}', resHash);
88 | script = script.replace('{{DJS_CODE}}',
89 | JSON.stringify(versionData.toString()));
90 | script = script.replace('{{DJS_BOOTSTRAP_VERSION}}', dictVerHash);
91 | script = script.replace('{{DJS_DIFF_URL}}',
92 | '/diffable/' + resHash + '/');
93 | log({
94 | 'type' : 'version',
95 | 'resource' : resHash,
96 | 'versionFile' : dictVerHash + '.version'
97 | });
98 | sendForCaching(res, script);
99 | }
100 |
101 | //HTML request handler
102 | function onHtmlRead(err, data) {
103 | if (err) {
104 | return next(err);
105 | }
106 | var strData = data.toString(),
107 | matches = strData.match(/\{\{DIFFABLE(.)*\}\}/gi),
108 | i, len, resource, script = "", counter = 0;
109 |
110 | //file doesn't contain diffable template. Pass through to
111 | //next middleware
112 | if (matches === null) {
113 | return next();
114 | }
115 |
116 | for (i = 0, len = matches.length; i < len; i += 1) {
117 | resource = matches[i].split('"')[1];
118 | (function (counter) {
119 | fs.realpath(root + resource, function (err, resolvedPath) {
120 | var resHash = frm.getResourceHash(resolvedPath),
121 | verHash = frm.getVersionHash(resHash), headers;
122 |
123 | if (counter === 0) {
124 | script += "";
125 | }
126 |
127 | script += '";
134 | strData = strData.replace(matches[counter], script);
135 | script = '';
136 | counter += 1;
137 | if (counter === len) {
138 | headers = {
139 | "Content-Type": 'text/html',
140 | "Content-Length": strData.length,
141 | "Cache-Control": "private, max-age=0",
142 | "Expires": "-1"
143 | };
144 | log({
145 | 'type' : 'html',
146 | 'resource' : resHash
147 | });
148 | res.writeHead(200, headers);
149 | res.end(strData);
150 | }
151 |
152 | });
153 | }(i));
154 | }
155 | }
156 |
157 | filename = Path.join(root, queryString.unescape(url.pathname));
158 |
159 | if (filename[filename.length - 1] === '/') {
160 | filename += "index.html";
161 | }
162 |
163 | if (filename.match(/\/diffable\/(.)*\.diff/gi)) {
164 | //request for delta data
165 | if (!req.headers['if-modified-since']) {
166 | hashes = filename.split('/').reverse()[0].split('_');
167 | resHash = hashes[0];
168 | dictVerHash = hashes[1];
169 | targetVerHash = hashes[2].split('.')[0];
170 | fs.readFile(diffableRoot + '/' + resHash + '/' +
171 | dictVerHash + '_' + targetVerHash + '.diff', onDiffRead);
172 | } else {
173 | sendConditional(res);
174 | }
175 | } else if (filename.match(/\/diffable\/(.)/gi)) {
176 | //request for versioned file
177 | if (!req.headers['if-modified-since']) {
178 | resHash = filename.split('/').reverse()[0];
179 | dictVerHash = frm.getVersionHash(resHash);
180 | if (dictVerHash) {
181 | fs.readFile(diffableRoot + '/' + resHash +
182 | '/' +
183 | dictVerHash +
184 | '.version', onJsRead);
185 | }
186 | else {
187 | return next();
188 | }
189 | } else {
190 | sendConditional(res);
191 | }
192 | } else if (filename.match(/(.)*\.html/gi)) {
193 | //request for html page
194 | fs.readFile(filename, onHtmlRead);
195 | } else {
196 | //pass through to next middleware
197 | return next();
198 | }
199 |
200 | };
201 | };
202 |
--------------------------------------------------------------------------------
/resources/DJSBootstrap.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2010 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | function DJSBootstrap(identifier, code) {
18 | this.code_ = code;
19 | this.identifier_ = identifier;
20 | if (window.localStorage) {
21 | if (!localStorage["diffable"]) {
22 | localStorage["diffable"] = JSON.stringify({});
23 | }
24 | this.ls_ = JSON.parse(localStorage["diffable"]);
25 | }
26 | }
27 |
28 | DJSBootstrap.prototype.bootstrap = function(bootstrap_version,
29 | diff_url) {
30 | if (window['deltajs'][this.identifier_]['cv'] == bootstrap_version) {
31 | this.applyAndExecute();
32 | } else {
33 | var me = this;
34 | window['deltajs'][this.identifier_]['load'] = function() {
35 | me.applyAndExecute.apply(me, arguments);
36 | };
37 | var diffScript = document.createElement('script');
38 | diffScript.src = diff_url + this.identifier_ + "_" +
39 | bootstrap_version + "_" +
40 | window['deltajs'][this.identifier_]['cv'] + ".diff";
41 | document.getElementsByTagName("head")[0].appendChild(diffScript);
42 | }
43 | };
44 |
45 | DJSBootstrap.prototype.applyAndExecute = function(opt_delta) {
46 | var output = this.code_;
47 | if (opt_delta) {
48 | output = DJSBootstrap.apply_(this.code_, opt_delta);
49 | }
50 | if (this.ls_) {
51 | this.ls_[this.identifier_] = {
52 | 'v': window['deltajs'][this.identifier_]['cv'],
53 | 'c': output
54 | }
55 | localStorage['diffable'] = JSON.stringify(this.ls_);
56 | }
57 | DJSBootstrap.globalEval(output);
58 | };
59 |
60 | DJSBootstrap.apply_ = function(dict, diff) {
61 | var output = [];
62 | for (var i = 0; i < diff.length; i++) {
63 | if (typeof diff[i] == 'number') {
64 | output.push(
65 | dict.substring(diff[i], diff[i] + diff[i + 1]));
66 | ++i;
67 | } else if (typeof diff[i] == 'string') {
68 | output.push(diff[i]);
69 | }
70 | }
71 | return output.join('');
72 | };
73 |
74 | DJSBootstrap.globalEval = (function() {
75 | var isIndirectEvalGlobal = (function (original, Object) {
76 | try {
77 | return (1, eval)('Object') === original;
78 | } catch(err) {
79 | return false;
80 | }
81 | }(Object, 123));
82 |
83 | if (isIndirectEvalGlobal) {
84 | return function(expression) {
85 | return (1,eval)(expression);
86 | };
87 | } else if (typeof window.execScript !== 'undefined') {
88 | return function(expression) {
89 | return window.execScript(expression);
90 | };
91 | }
92 | }());
93 |
94 | DJSBootstrap.loadVersion = function (identifier) {
95 | var script = document.createElement('script');
96 | script.src = '/diffable/' + identifier;
97 | document.getElementsByTagName('head')[0].appendChild(script);
98 | }
99 |
100 | DJSBootstrap.checkStorage = function (resHash, verHash) {
101 | if (!window.localStorage || !window.localStorage['diffable']) {
102 | DJSBootstrap.loadVersion(resHash);
103 | } else {
104 | var ls = JSON.parse(localStorage['diffable']);
105 | if (ls[resHash] && ls[resHash]['v'] === verHash) {
106 | DJSBootstrap.globalEval(ls[resHash]['c']);
107 | } else {
108 | DJSBootstrap.loadVersion(resHash);
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/resources/DJSBootstrap.min.js:
--------------------------------------------------------------------------------
1 | function DJSBootstrap(a,b){this.code_=b;this.identifier_=a;if(window.localStorage){if(!localStorage.diffable){localStorage.diffable=JSON.stringify({})}this.ls_=JSON.parse(localStorage.diffable)}}DJSBootstrap.prototype.bootstrap=function(c,a){if(window.deltajs[this.identifier_]["cv"]==c){this.applyAndExecute()}else{var b=this;window.deltajs[this.identifier_]["load"]=function(){b.applyAndExecute.apply(b,arguments)};var d=document.createElement("script");d.src=a+this.identifier_+"_"+c+"_"+window.deltajs[this.identifier_]["cv"]+".diff";document.getElementsByTagName("head")[0].appendChild(d)}};DJSBootstrap.prototype.applyAndExecute=function(b){var a=this.code_;if(b){a=DJSBootstrap.apply_(this.code_,b)}if(this.ls_){this.ls_[this.identifier_]={v:window.deltajs[this.identifier_]["cv"],c:a};localStorage.diffable=JSON.stringify(this.ls_)}DJSBootstrap.globalEval(a)};DJSBootstrap.apply_=function(d,c){var a=[];for(var b=0;b