Hot Code Push NodeJS

I tried to find out this "Hot Code Push" on Node.js. Basically, my main file (which starts when node app.js ) consists of some settings, configurations, and initializations. In this file, I have a file watcher using chokidar. When I added the file, I just require file. If the file was modified or updated, I would delete the cache delete require.cache[path] and then re-requested it. All of these modules do not export anything, but simply work with a single global Storm object.

 Storm.watch = function() { var chokidar, directories, self = this; chokidar = require('chokidar'); directories = ['server/', 'app/server', 'app/server/config', 'public']; clientPath = new RegExp(_.regexpEscape(path.join('app', 'client'))); watcher = chokidar.watch(directories, { ignored: function(_path) { if (_path.match(/\./)) { !_path.match(/\.(js|coffee|iced|styl)$/); } else { !_path.match(/(app|config|public)/); } }, persistent: true }); watcher.on('add', function(_path){ self.fileCreated(path.resolve(Storm.root, _path)); //Storm.logger.log(Storm.cliColor.green("File Added: ", _path)); //_console.info("File Updated"); console.log(Storm.css.compile(' {name}: {file}', "" + "name" + "{" + "color: white;" + "font-weight:bold;" + "}" + "hr {" + "background: grey" + "}")({name: "File Added", file: _path.replace(Storm.root, ""), hr: "=================================================="})); }); watcher.on('change', function(_path){ _path = path.resolve(Storm.root, _path); if (fs.existsSync(_path)) { if (_path.match(/\.styl$/)) { self.clientFileUpdated(_path); } else { self.fileUpdated(_path); } } else { self.fileDeleted(_path); } //Storm.logger.log(Storm.cliColor.green("File Changed: ", _path)); console.log(Storm.css.compile(' {name}: {file}', "" + "name" + "{" + "color: yellow;" + "font-weight:bold;" + "}" + "hr {" + "background: grey" + "}")({name: "File Changed", file: _path.replace(Storm.root, ""), hr: "=================================================="})); }); watcher.on('unlink', function(_path){ self.fileDeleted(path.resolve(Storm.root, _path)); //Storm.logger.log(Storm.cliColor.green("File Deleted: ", _path)); console.log(Storm.css.compile(' {name}: {file}', "" + "name" + "{" + "color: red;" + "font-weight:bold;" + "}" + "hr {" + "background: grey" + "}")({name: "File Deleted", file: _path.replace(Storm.root, ""), hr: "=================================================="})); }); watcher.on('error', function(error){ console.log(error); }); }; Storm.watch.prototype.fileCreated = function(_path) { if (_path.match('views')) { return; } try { require.resolve(_path); } catch (error) { require(_path); } }; Storm.watch.prototype.fileDeleted = function(_path) { delete require.cache[require.resolve(_path)]; }; Storm.watch.prototype.fileUpdated = function(_path) { var self = this; pattern = function(string) { return new RegExp(_.regexpEscape(string)); }; if (_path.match(pattern(path.join('app', 'templates')))) { Storm.View.cache = {}; } else if (_path.match(pattern(path.join('app', 'helpers')))) { self.reloadPath(path, function(){ self.reloadPaths(path.join(Storm.root, 'app', 'controllers')); }); } else if (_path.match(pattern(path.join('config', 'assets.coffee')))) { self.reloadPath(_path, function(error, config) { //Storm.config.assets = config || {}; }); } else if (_path.match(/app\/server\/(models|controllers)\/.+\.(?:coffee|js|iced)/)) { var isController, directory, klassName, klass; self.reloadPath(_path, function(error, config) { if (error) { throw new Error(error); } }); Storm.serverRefresh(); isController = RegExp.$1 == 'controllers'; directory = 'app/' + RegExp.$1; klassName = _path.split('/'); klassName = klassName[klassName.length - 1]; klassName = klassName.split('.'); klassName.pop(); klassName = klassName.join('.'); klassName = _.camelize(klassName); if (!klass) { require(_path); } else { console.log(_path); self.reloadPath(_path) } } else if (_path.match(/config\/routes\.(?:coffee|js|iced)/)) { self.reloadPath(_path); } else { this.reloadPath(_path); } }; Storm.watch.prototype.reloadPath = function(_path, cb) { _path = require.resolve(path.resolve(Storm.root, path.relative(Storm.root, _path))); delete require.cache[_path]; delete require.cache[path.resolve(path.join(Storm.root, "server", "application", "server.js"))]; //console.log(require.cache[path.resolve(path.join(Storm.root, "server", "application", "server.js"))]); require("./server.js"); Storm.App.use(Storm.router); process.nextTick(function(){ Storm.serverRefresh(); var result = require(_path); if (cb) { cb(null, result); } }); }; Storm.watch.prototype.reloadPaths = function(directory, cb) { }; 

Some of the code is incomplete / not used as I try to use many different methods.

What works:

For code like the following:

 function run() { console.log(123); } 

Works great. But any asynchronous code is not updated.

Problem = Asynchronous Code

 app.get('/', function(req, res){ // code here.. }); 

If I then update the file when the nodejs process starts, nothing happens, although it goes through the file watcher and the cache is deleted and then restored. Another example in which it does not work:

 // middleware.js function hello(req, res, next) { // code here... } // another file: app.use(hello); 

Since app.use will still use the old version of this method.

Question:

How can I fix the problem? Is something missing?

Please do not offer to use third-party modules, as forever. I am trying to include functionality in one instance.

EDIT:

After learning meteors codebase (surprisingly few resources on โ€œHot Code Pushโ€ in Node.js or in a browser.) And messing around with my own implementation, I successfully made a working solution. https://github.com/TheHydroImpulse/Refresh.js . It is still at an early stage of development, but now it seems solid. I will also implement a browser solution, just to complete.

+6
source share
2 answers

Removing the require cache does not actually โ€œdumpโ€ your old code and does not undo what this code did.

Take, for example, the following function:

 var callbacks=[]; registerCallback = function(cb) { callbacks.push(cb); }; 

Now let's say that you have a module that calls this global function.

 registerCallback(function() { console.log('foo'); }); 

After starting your application, callbacks will have one element. Now we are modifying the module.

 registerCallback(function() { console.log('bar'); }); 

Your "hot patching" code is executed, the require.cache d version is removed, and the module reloads.

What you need to understand is that now callbacks have two . Firstly, it refers to a function that writes foo (which was added when the application started) and a link to a function that writes the panel (which was just added).

Even if you deleted the cached reference to the exports module , you cannot actually remove the module. . As for the JavaScript runtime, you just deleted one link from many. Any other part of your application can still be linked to a link to something in the old module.

This is exactly what happens with your HTTP application. When the application starts, your modules attach anonymous callbacks to routes. When these modules change, they attach a new callback to the same routes; old callbacks are not deleted. I assume that you are using Express, and it calls the route handlers in the order in which they were added. So the new callback will never work.


To be honest, I would not use this approach to reload your application upon modification. Most people write application initialization code assuming a clean environment; you violate this assumption by running the initialization code in a dirty environment - that is, one that is already running and working.

Trying to clean the environment so that your initialization code works, almost certainly has more problems than it costs. I would just restart the whole application when your base files have changed.

+3
source

Meteor solves this problem by allowing modules to "register" as part of the push code process.

They implement this in their reload package:

https://github.com/meteor/meteor/blob/master/packages/reload/reload.js#L105-L109

I saw that the Meteor.reload API used in some plugins in GitHub, but they also use it in the session package:

https://github.com/meteor/meteor/blob/master/packages/session/session.js#L103-L115

 if (Meteor._reload) { Meteor._reload.onMigrate('session', function () { return [true, {keys: Session.keys}]; }); (function () { var migrationData = Meteor._reload.migrationData('session'); if (migrationData && migrationData.keys) { Session.keys = migrationData.keys; } })(); } 

Thus, basically, when the page / window is loaded, the meteorite performs a โ€œmigrationโ€, and this is before the package for determining data / methods / etc. which get recounted by pressing the hot code.

It is also used by their package> (search reload ).

Between updates, they maintain a "state" using window.sessionStorage .

+2
source

All Articles