I have a proof-of-concept working for a JavaScript autoloader, but it currently suffers from a major flaw: it requires the code to be re-executed in its entirety rather than simply trying again from the line that failed.
Here is the prototype:
<!doctype html>
<html>
<head>
</head>
<body>
<script type="text/javascript">
var app = function(){
console.log('Initialize App');
var test = new Test();
var foo = new Foo();
var bar = new Bar();
};
var autoload = function(app){
var recover = function(error){
var name = error.message.split(' ')[0];
console.log('Loading '+name);
//A file could be synchronously loaded here instead
this[name] = function(){
console.log(name+' has been dynamically created');
};
load(app);
};
var load = function(app){
try {
app();
} catch (error){
if (error.name == "ReferenceError"){
console.log(error.message);
recover(error, app);
}
}
};
load(app);
};
autoload(app);
</script>
</body>
</html>
How It's Supposed To Work
The idea is that all of your application code would get executed within the app function. Eventually, if I can get it working properly, you could also pass in a dependency map to autoloader with the app function to synchronously load dependencies when a function is not defined. The dependency map would simply be an object mapping function names to file names.
How It Currently Works
If you don't feel like trying it out, the above code outputs the following to the console:
Initialize App
Test is not defined
Loading Test
Initialize App
Test has been dynamically created
Foo is not defined
Loading Foo
Initialize App
Test has been dynamically created
Foo has been dynamically created
Bar is not defined
Loading Bar
Initialize App
Test has been dynamically created
Foo has been dynamically created
Bar has been dynamically created
The complete app function is re-executed each time the autoloader catches an error. Obviously, this is less than ideal for a number of reasons.
Recovering From The Error
To move to the next step for making this work, I need to find a way to recover from the error without re-executing the entire app function. The error object from the catch block does provide both the line number and file name where the error occurred, but so far, I haven't been able to find a way to take advantage of that information. There are three general approaches that I can think of:
Restart script execution at the given line
Restart script execution at the beginning, but skip all lines until the given line
Grab the file as a string, split it into an array by line number, and eval the remaining lines.
Unfortunately, I wasn't able to find information on either of the first two approaches. Of the three, #1 seems like it would be more ideal, but I would certainly be open to other creative suggestions as well. As far as I can tell, JavaScript doesn't provide a way to start script execution at an arbitrary line number. #3 might work, but I'm not sure it would be very performant. The only way I can think of doing it would be to require an extra request each time to load the file text into a string.
The Questions
This is admittedly pushing the boundaries of how dependencies could be loaded in JavaScript. I'm not even sure if it is possible because I don't know if JavaScript allows for this type of error recovery. That said, I'm interested in exploring it further until I find out it's absolutely impossible.
In the interests of getting this working:
Is there a way to start script execution at an arbitrary line of JavaScript?
Are there other approaches to this that might be more fruitful? Feel free to be creative!
Taking a step back to look at the bigger picture (assuming I can get this to work):
Is a JavaScript autoloader something that people would even want?
What are the advantages/disadvantages to an autoloader like this vs. an approach like AMD?
What kind of performance issues would be encountered with this approach? Would the performance hit be too much to make it worth it?
It's kind of complicated to make, throwing and catching is kind of expensive as well. You could use typeof window["Test"]!=="function" and then create it instead of using the try catch like this.
But for a general recover and continue approach the following code would do the trick.
var app = (function(i){
var objects=new Array(3),
fnNames=["Test","Foo","Bar"];
return function(){
var len=fnNames.length
,test,foo,bar;
//check if closure vars have been reset, if so
// this has ran successfully once so don't do anything?
if(objects===false){
console.log("nothing to do, initialized already");
return;
}
while(i<len){
try{
objects[i] = new window[fnNames[i]]();
i++;
}catch(e){
if (e.name == "TypeError"){//different syntax different error
throw {"fnName":fnNames[i]};
}
}
}
//store instances in the variables
test = objects[0];
foo = objects[1];
bar = objects[2];
//reset closure vars assuming you only call app once
// when it's successful
i=null;objects=false;
console.log("Got all the instances",test,foo,bar);
};
}(0));
var autoload = function(app){
var recover = function(name){
console.log('Loading '+name);
//A file could be synchronously loaded here instead
this[name] = function(){
this.name=name;
console.log(this.name+' has been dynamically created');
};
load(app);
};
var load = function(app){
try {
app();
} catch (error){
//if statement here no longer needed
// object thrown has fnName (function name)
recover(error.fnName, app);
}
};
load(app);
};
autoload(app);
autoload(app);
Related
I have a project for work where I am basically creating a form of CMS to which we will add applications as time moves forward.
The issue we're having is getting those applications loaded in (and more specifically modified) on run-time within the server.
The reason we're requiring this form of "hot loading" is because we don't want the server to restart whenever a change has been made, and more specifically, we'd like to add the new applications through an admin panel.
Nodemon is a useful tool for development, but for our production environment we want to be able to replace an existing application (or module/plugin if you will) without having to restart the server (whether it's manually or through nodemon, the server needs to be running at all time).
You could compare this to how CMS' like Drupal, Yoomla, or Wordpress do things, but for our needs, we decided that Node was the better way to go for many reasons.
Code wise, I am looking for something like this, but that will work:
let applications = []
//add a new application through the web interface calling the appropiate class method, within the method the following code runs:
applications.push(require('path/to/application');
//when an application gets modified:
applications.splice(index,1);
applications.push('path/to/application');
But I require existing instances of said application to be adjusted as well.
Example:
// file location: ./Applications/application/index.js
class application {
greet() {
console.log("Hello");
}
}
module.exports = application;
the app loader would load in said application:
class appLoader {
constructor() {
this.List = new Object();
}
Add(appname) {
this.List[appname] = require(`./Applications/${appname}/index`);
}
Remove(appname) {
delete require.cache[require.resolve(`./Applications/${appname}/index`)]
delete this.List[appname];
}
Reload(appname) {
this.Remove(appname);
this.Add(appname);
}
}
The running code:
const AppLoader = require('appLoader');
const applications = new AppLoader();
applications.add('application'); // adds the application created above
var app = new applications.List['application']();
app.greet();
// Change is made to the application file, .greet() now outputs "Hello World" instead of "Hello"
//do something to know it has to reload, either by fs.watch, or manual trigger
applications.Reload('application');
app.greet();
The expected behavior is:
Hello
Hello World
In reality, I'm getting:
Hello
Hello
If anyone can help me figure out a way to dynamically load in applications like this, but also remove/reload them during run-time, it would be greatly appreciated!
Edit: if there is a way to run my application code without the use of require that would allow a dynamic load/reload/remove, that is also a welcome solution
Ok, thanks to #jfriend00 I realized I need to fix something else with my code, so his comments can still be useful for other people. As to my issue of unloading required modules or reloading them without a server restart, I figured out a relatively elegant way of making it happen.
Let me start by showing you all my test class and app.js and I'll explain what I did and how it works.
Class.js:
"use strict";
class Class {
constructor() {
// this.file will be put in comments post run-time, and this.Output = "Hey" will be uncommented to change the source file.
var date = new Date()
this.Output = date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds() + "." + date.getMilliseconds();
this.file = global.require.fs.readFileSync('./file.mov');
//this.Output = "Hey";
}
}
module.exports = Class;
app.js:
'use strict';
global.require = {
fs: require('fs')
};
const arr = [];
const mod = './class.js'
let Class = [null];
Class[0] = require(mod);
let c = [];
c.push(new Class[0]());
console.log(c[0].Output);
console.log(process.memoryUsage());
setTimeout(() => {
delete require.cache[require.resolve(mod)];
delete Class[0];
Class[0] = require(mod);
console.log(Class)
delete c[0];
c[0] = new Class[0]();
console.log(c[0].Output);
console.log(process.memoryUsage());
}, 10000);
Now let me explain here for a bit, and mind you, this is testing code so the naming is just horrid.
This is how I went to work:
Step 1
I needed a way to separate required modules (like fs, or websocket, express, etc.) so it wouldn't mess with the whole delete require_cache() part of the code, my solution was making those globally required:
global.required = {
fs: require('fs')
}
Step 2
Figure out a way to make sure the Garbage Collector removes the unloaded code, I achieved this by putting my requires and class declarations inside of a variable so that I could use the delete functionality within Node/Javascript. (I used let in my test code because I was testing another method beforehand, haven't tested if const would work again).
I also made a variable that contains the path string for the file (in this case './Class.js' but for my explanation below I'll just write it in as is)
let Class = [null] //this declares an array that has an index '0'
Class[0] = require('./Class');
let c = [new Class[0]()] // this declares an array that has the class instantiated inside of index '0'
As for the garbage collection, I'm simply able to do the following:
delete Class[0];
delete c[0];
After this I am able to redo the declaration of the required class and subsequently the class itself and keep my code working without requiring a restart.
Take in mind that his takes a lot of work to implement in an actual project, but you could split it up by adding an unload() method to a class to unload underlying custom classes. But my initial testing shows that this works like a charm!
Edit: I feel required to note that without jfriend00's comments I'd never have figured out this solution
Output
When the project start, it outputs the current time and the process.memoryUsage()
13:49:13.540
{ rss: 50343936,
heapTotal: 7061504,
heapUsed: 4270696,
external: 29814377 }
during the 10 second wait, I change the Class.js file to not read the file.mov and say "Hey" instead of the time, after the 10s timout this is the output:
Hey
{ rss: 48439296,
heapTotal: 7585792,
heapUsed: 4435408,
external: 8680 }
I'm running against a wall here, maybe it's just a small problem where I can't see the solution due to my inexperience with NodeJS.
Right now I'm constructing a BT device which will be controlled by a master application and I have settled for the prototyping on a Raspberry PI 3 with NodeJS using the Bleno module.
So far everything worked fine, the device gets found and I can set and get values over Bluetooth. But to separate the different "programs" which the device could execute from the Bluetooth logic (because of loops etc.) I have opted to extract these into external NodeJS files.
The idea here was to use the NodeJS fork module and control the starting and stoppping of those processes through the main process.
But herein my problems start. I can fork the different JavaScript files without problem and these get executed, but I can't get them to stop and I don't know how to fix it.
Here's the code (simplified):
var util = require('util');
var events = require('events');
var cp = require('child_process');
...
var ProgramTypeOne = {
NONE: 0,
ProgramOne: 1,
...
};
...
var currentProgram=null;
...
function BLEDevice() {
events.EventEmitter.call(this);
...
this.currentProgram=null;
...
}
util.inherits(BLELamp, events.EventEmitter);
BLELamp.prototype.setProgram = function(programType, programNumber) {
var self = this;
var result=0;
if(programType=="ProgramTypeOne "){
if(programNumber==1){
killProgram();
this.currentProgram=cp.fork('./programs/programOne');
result=1;
}else if(programNumber==2){
...
}
self.emit('ready', result);
};
...
module.exports.currentProgram = currentProgram;
...
function killProgram(){
if(this.currentProgram!=null){
this.currentProgram.kill('SIGTERM');
}
}
There seems to be a problem with the variable currentProgram which, seemingly, never gets the childprocess from the fork call.
As I have never worked extensivley with JavaScript, except some small scripts on websites, I have no idea where exactly my error lies.
I think it has something to do with the handling of class variables.
The starting point for me was the Pizza example of Bleno.
Hope the information is enough and that someone can help me out.
Thanks in advance!
Since killProgram() is a standalone function outside of the scope of BLELamp, you need to call killProgram with the correct scope by binding BLELamp as this. Using apply (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply) should resolve it. The following I would expect would fix it (the only line change is the one invoking killProgram):
BLELamp.prototype.setProgram = function(programType, programNumber) {
var self = this;
var result=0;
if(programType=="ProgramTypeOne "){
if(programNumber==1){
killProgram.apply(this);
this.currentProgram=cp.fork('./programs/programOne');
result=1;
}else if(programNumber==2){
...
}
self.emit('ready', result);
};
As a side note, your code is kind of confusing because you have a standalone var currentProgram then a couple prototypes with their own bound this.currentPrograms. I would change the names to prevent confusion.
I'm creating a plugin system using the following:
function Plugin(thingy, code)
{
var GLOBAL = null;
var arguments = null;
var process = null;
var require = null;
eval(code);
};
plugins.push(new Plugin(thingy, code));
Please don't get too excited about the eval(), using ('vm') or a sandbox is not an option as this will be a long running object until the user unloads it. It will also be running in it's own nodeJS instance so they can't affect other users. I'd still have the same problem passing in this object reference to a sandbox system anyway.
What I am concerned about is someone seeing the code of the thingy object that has functions they need to use e.g shoot()
console.log(thingy.shoot.toString());
A way around this was the following:
function thingy()
{
// They can't see this
var _shoot = function(someone)
{
// Load weapon
// Aim
// Fire
};
// They can see this
this.shoot = function(someone)
{
_shoot(someone);
};
};
This way if they console.log(thingy.shoot.toString()) they'll only see _shoot(someone); and not the actual code that handles the shooting process.
Please could someone help me with the following:
Is there an easier way to limit access to a passed in variables code?
I'm setting GLOBAL, arguments, process and require to null; are there others I need to worry about?
I have a Factory class that I use in JavaScript to dynamically load a class file over AJAX and then return an object. I have run into a VERY peculiar bug in the system though that throws errors in EVERY browser, but under a condition that is beyond my ability to explain.
Here is a simplified version of my Factory class (I removed a lot of type checking and error handling to cut it down to bare minimum).
function Factory(){
// This holds which files have already been loaded
var loaded=new Object();
// Returns a new object
this.getObject=function(className,methodName,methodData){
if(loadFile('class.'+className+'.js')){
// Making sure that the object name is defined
if(window[className]!=null){
// Has to be an object
if(typeof(window[className])=='function'){
// Creating a temporary object
return new window[className];
}
}
}
}
// Loads a file over AJAX
var loadFile=function(address){
// Loads as long as the file has not already been loaded
if(loaded[address]==null){
// Required for AJAX connections (without ASYNC)
var XMLHttp=new XMLHttpRequest();
XMLHttp.open('GET',address,false);
XMLHttp.send(null);
// Result based on response status
if(XMLHttp.status===200 || XMLHttp.status===304){
// Getting the contents of the script
var data=XMLHttp.responseText;
// Loading the script contents to the browser
(window.execScript || function(data){
window['eval'].call(window,data);
})(data);
// makes sure that file is loaded only once
loaded[address]=true;
}
}
}
This is what the user does:
var Factory=new Factory();
var alpha=Factory.getObject('example');
alpha.set(32);
alpha.get();
var beta=Factory.getObject('example');
beta.set(64);
alpha.get();
beta.get();
This fails, it says 'object is not a function' when the function is run for the second time (at the return new window[className]; line). I understand if I am missing something here, BUT here's the kicker:
If I prefix className in my window[] calls, then it works. For example, if I change my 'example' class filename to 'test_example' and then have these lines:
... if(window['test_'+className]!=null){ ...
... if(typeof(window['test_'+className])=='function'){ ...
... return new window['test_'+className]; ...
Then it works and both alpha and beta objects work as expected. When I refer to them purely through a variable, they fail. I tried things like className.toString() without success and even this fails:
className+''
This is really weird, I don't know where to look and what to try out anymore, does anyone know why this happens?
EDIT: Here is an example of the 'example.js' script that is being loaded:
function example(){
var myVar=16;
this.set=function(value){
myVar=value;
}
this.get=function(){
alert(myVar);
}
}
(and if I rename this to test_example() and load the functions as shown above with constructed strings, then it again works)
I figured out where the error was, which my above, cut-down version does not show. Apparently I named my new variable the same that the name of the class itself was, thus after first initialization it got overwritten.
I'm having a problem with the following Javascript code (Phonegap in Eclipse):
function FileStore(onsuccess, onfail){
//chain of Phonegap File API handlers to get certain directories
function onGetSupportDirectorySuccess(dir){
//stuff
onsuccess();
}
function getDirectory(dir){
return "something" + dir;
}
}
var onFileStoreOpened = function(){
if (window.file_store instanceof FileStore){
console.log('window.file_store is a FileStore');
console.log(window.file_store.getDirectory('something'));
}
}
var onDeviceReady = function(){
window.file_store = new FileStore(onFileStoreOpened, onFileStoreFailure);
}
Here, I want to do some things to initialize file services for the app, and then use them in my initialization from the callback. I get the following error messages in LogCat:
07-03 06:26:54.942: D/CordovaLog(223): file:///android_asset/www/index.html: Line 40 : window.file_store is a FileStore
07-03 06:26:55.053: D/CordovaLog(223): file:///android_asset/www/cordova-1.8.1.js: Line 254 : Error in success callback: File7 = TypeError: Result of expression 'window.file_store.getDirectory' [undefined] is not a function.
After moving the code around and stripping out everything in getDirectory() to make sure it was valid, I'm not even sure I understand the error message, which suggested to me that getDirectory() is not seen as a member function of window.file_store, even though window.file_store is recognized as a FileStore object. That makes no sense to me, so I guess that interpretation is incorrect. Any enlightenment will be greatly appreciated.
I've since tried the following:
window.file_store = {
app_data_dir : null,
Init: function(onsuccess, onfail){
//chain of Phonegap File API handlers to get directories
function onGetSupportDirectorySuccess(dir){
window.file_store.app_data_dir = dir;
console.log("opened dir " + dir.name);
onsuccess();
}
},
GetDirectory : function(){
return window.file_store.app_data_dir; //simplified
}
}
var onFileStoreOpened = function(){
var docs = window.file_store.getDirectory();
console.log('APPDATA: ' + docs.fullPath);
}
var onDeviceReady = function() {
window.file_store.Init(onFileStoreOpened, onFileStoreFailure);
}
and I get
D/CordovaLog(224): file:///android_asset/www/base/device.js: Line 81 : opened dir AppData
D/CordovaLog(224): file:///android_asset/www/cordova-1.8.1.js: Line 254 : Error in success callback: File7 = TypeError: Result of expression 'docs' [null] is not an object.
All I want to do here is make sure certain directories exist (I've removed all but one) when I start, save the directory object for future use, and then retrieve and use it after all initialization is done, and I don't want everything in the global namespace. Of course I would like to be able to use specific instances when necessary, and I'm disturbed that I can't make it work that way since it demonstrates there is a problem with my understanding, but I can't even get this to work with a single, global one. Is this a Javascript problem or a Phonegap problem?
As it stands, your getDirectory function is a private function within FileStore. If you wanted to make it a 'member' or 'property' of FileStore, you would need to alter it a little within FileStore to make it like this:
this.getDirectory = function(dir){ };
or leave it how it is and then set a property....
this.getDirectory = getDirectory();
this way when new FileStore is called it will have getDirectory as a property because the 'this' keyword is always returned when calling a function with 'new'
Hope this quick answer helps. There's lots of stuff on the goog about constructor functions.
You understand it correctly. The getDirectory as it stands is a private function and cannot be called using the file_store instance.
Try this in the browser.
function FileStore(onsuccess, onfail){
function onGetSupportDirectorySuccess(dir){
//stuff
onsuccess();
}
this.getDirectory = function (dir){
return "something" + dir;
}
}
window.file_store = new FileStore('', ''); //the empty strings are just placeholders.
if (window.file_store instanceof FileStore){
console.log('window.file_store is a FileStore');
console.log(window.file_store.getDirectory('something'));
}
This will prove that the basic js code is working fine. If there still is a problem while using it in PhoneGap, comment.