Skip to content

Instantly share code, notes, and snippets.

@pyokagan
Last active December 16, 2015 07:59
Show Gist options
  • Select an option

  • Save pyokagan/5402721 to your computer and use it in GitHub Desktop.

Select an option

Save pyokagan/5402721 to your computer and use it in GitHub Desktop.
fun.js -- injecting 100% more fun and excitement into nodejs programming by implementing module reloading, code swapping, classes with C3 MRO, state saving, loading and replacing.
/* This fun module implements 100% more fun and excitement in
* nodejs coding.
*
* Implements module reloading, code swapping, classes with C3 MRO,
* state saving (pickling) and loading (unpickling) and replacing.
*
* This is extracted from the bb4 source code, licensed under the MIT License.
* bb4 is available at <https://github.com/pyokagan/bb4>
*
* Requires node-weak to be installed. (npm install weak)
*
*/
var moduleId = module.id,
moduleFilename = module.filename;
var module = require("module"),
weak = require("weak"),
singletonCtr = 0,
singletons = {}, //Mapping of singleton names to weakrefs of singleton objects
instanceCtr = 0,
instances = {}, //Mapping of instance IDs to weakrefs of instance objects
deps = {}, //Mapping of filename -> set of filenames of dependent modules
_require = module.prototype.require;
var has = function(obj, key) {
return hasOwnProperty.call(obj, key);
};
var breaker = {};
var each = function(obj, iterator, context) {
if (obj == null) return;
if (obj.length === +obj.length) {
for (var i = 0, l = obj.length; i < l; i++) {
if (iterator.call(context, obj[i], i, obj) === breaker) return;
}
} else {
for (var key in obj) {
if (has(obj, key)) {
if (iterator.call(context, obj[key], key, obj) === breaker) return;
}
}
}
};
var any = function(obj, iterator, context) {
var result = false;
if (obj == null) return result;
each(obj, function(value, index, list) {
if (result || (result = iterator.call(context, value, index, list))) return breaker;
});
return !!result;
};
var map = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
return results;
};
var filter = function(obj, iterator, context) {
var results = [];
if (obj == null) return results;
each(obj, function(value, index, list) {
if (iterator.call(context, value, index, list)) results[results.length] = value;
});
return results;
};
var contains = function(obj, target) {
if (obj == null) return false;
return any(obj, function(value) {
return value === target;
});
};
var singleton = exports.singleton = function (obj) {
var name = obj.__name__;
if (!name) throw "Object does not have name set";
if (singletons[name] && !weak.isDead(singletons[name])) {
var dest = weak.get(singletons[name]);
if (dest.__ns__ == singletonCtr) {
throw "Singleton " + name + " already registered! This usually indicates a name conflict.";
} else {
// Merge
for (var prop in obj) {
if (!obj.hasOwnProperty(prop)) continue;
dest[prop] = obj[prop];
}
for (var prop in dest) {
if (!dest.hasOwnProperty(prop)) continue;
if (typeof obj[prop] == "undefined") {
delete dest[prop];
}
}
dest.__proto__ = obj.__proto__;
dest.__ns__ = singletonCtr;
return dest;
}
} else {
// Copy over directly
obj.__ns__ = singletonCtr;
singletons[name] = weak(obj);
return obj;
}
}
// Registers instance
var instance = exports.instance = function (obj) {
if (typeof obj.__id__ != "undefined")
return;
obj.__id__ = instanceCtr++;
instances[obj.__id__] = weak(obj);
}
var reloadInstances = exports.reloadInstances = function() {
for (var instanceId in instances) {
if (!instances.hasOwnProperty(instanceId)) continue;
var instance = weak.get(instances[instanceId]);
if (typeof instance == "undefined") {
// Instance probably has been garbage collected
delete instances[instanceId];
continue;
}
// Reload by calling the instance's constructor again
if (instance.__init__) instance.__init__.call(instance, instance);
}
}
module.prototype.require = function(path) {
var filename = module._resolveFilename(path, this);
var x = _require.apply(this, arguments);
x.__name__ = filename;
if (module._cache[filename]) {
module._cache[filename].exports = singleton(x);
if (!deps[filename]) deps[filename] = {};
deps[filename][this.filename] = true;
}
return x;
}
function _reload(path) {
var d = deps[path];
// Ensure that we do not reload this module!
if (path != moduleFilename && !(module._cache[path] && module._cache[path].id == ".")) {
// Clear cached module
delete module._cache[path];
// Clear deps
delete deps[path];
// Load module
module._load(path, null);
}
// Recurse into modules that depend on it
if (d) {
for (var depPath in d) {
if (!d.hasOwnProperty(depPath)) continue;
_reload(depPath);
}
}
}
module.prototype.reload = exports.reload = function(path) {
singletonCtr++;
var filename = module._resolveFilename(path, this);
_reload(filename); //Reload modules
reloadInstances(); //Reload instances
}
var getSingleton = exports.getSingleton = function(name) {
if (singletons[name]) return weak.get(singletons[name]);
}
var getInstance = exports.getInstance = function(id) {
if (instances[id]) return weak.get(instances[id]);
}
var getConstructorFromClassName = exports.getConstructorFromClassName = function(name) {
var comp = name.split("/");
var singletonName = name + "::constructor";
comp.pop();
var moduleId = comp.join("/");
var module = require(moduleId);
return getSingleton(singletonName);
}
var getPrototypeFromClassName = exports.getPrototypeFromClassName = function(name) {
var comp = name.split("/");
var singletonName = name;
comp.pop();
var moduleId = comp.join("/");
var module = require(moduleId);
return getSingleton(singletonName);
}
function merge(seqs) {
// seqs = [[MRO1], [MRO2], [MRO3]...]
// output = [classA, classB, classC...]
function pythonIdentity(x) {
return x.length;
}
var res = []; // Output
while(true) {
var nonEmptySeqs = filter(seqs, pythonIdentity);
if (!nonEmptySeqs.length) {
return res;
}
var cand;
for(var i = 0; i < nonEmptySeqs.length; ++i) {
cand = nonEmptySeqs[i][0];
var filtContainBody = function(nonemptyseq) {
return contains(nonemptyseq.slice(1, nonemptyseq.length), cand)
}
var nothead = any(nonEmptySeqs, filtContainBody);
if (nothead) {
cand = null; //Reject candidate, move on to next one
} else {
break; // Accept candidate
}
}
if (!cand) {
// Candidate not found in the end
throw "Inconsistent Hierarchy";
}
res.push(cand);
// Remove cand
for (var i = 0; i < nonEmptySeqs.length; ++i) {
if (nonEmptySeqs[i][0] === cand)
nonEmptySeqs[i].splice(0, 1);
}
} //while(true)
}
function bases(c) {
// Returns bases of a class c as an array
// If c is a PYK class, then we return its __bases__
// Else, we return the __proto__ of c.prototype (if any)
// If c has no prototype, then we return empty array
if (c.__bases__) {
return c.__bases__;
} else if (c.prototype) {
var x = Object.getPrototypeOf(c.prototype);
return x ? [x] : [];
} else {
return [];
}
}
function mro(c) {
// Calculates the MRO of function f and caches it
// where c is a function
if (c.__mro__) {
return c.__mro__.slice(0);
} else {
c.__mro__ = merge([[c]].concat(map(bases(c), mro), [bases(c)]));
return c.__mro__;
}
}
var superCall = exports.superCall = function superCall(f) {
f.__supercall__ = f;
return f;
}
var cls = exports.cls = function (name, bases, props) {
// Returns function c where c.prototype is the new generated prototype
// Also, we register the prototype as a singleton so that it can be updated.
// 1. Calculate MRO of the new class
var _mro = mro({__bases__: bases.slice(0), prototype: props});
var prototype = {};
// 2. Copy over methods to new prototype
for (var i = _mro.length - 1; i >= 0; --i) {
var x = _mro[i].prototype;
for (var propName in x) {
if (!x.hasOwnProperty(propName)) continue;
if (x[propName].__supercall__) {
// Handle superCalls
var f = prototype[propName] ? prototype[propName] : function() {},
y = x[propName].__supercall__.call(this, f);
y.__superfunc__ = x[propName].__supercall__;
prototype[propName] = y;
} else {
// Handle ordinary functions and properties
prototype[propName] = x[propName];
}
}
}
prototype.__name__ = name;
// 3. Create a constructor that calls the real constructor
var that = prototype.__init__ ? prototype.__init__ : function() {};
var constructor = function() {instance(this); return that.apply(this, arguments); }
_mro[0] = constructor;
constructor.prototype = prototype;
constructor.__mro__ = _mro;
constructor.__bases__ = bases.slice(0);
constructor.__name__ = name + "::constructor";
constructor.inspect = function() {
return "<class " + JSON.stringify(name) + ">";
};
// Register prototype as singleton
singleton(prototype);
singleton(constructor);
return constructor;
}
var pickleId = 0; //Pickle session number
function _pickle(obj, loc, pickleProp) {
if (obj == null) return null; //Special case null because it cannot be detected with typeof
if (Object.prototype.toString.call(obj) === "[object Array]") {
obj = obj.slice(0); //Shallow copy of array
for (var i = 0; i < obj.length; ++i) {
var val = _pickle(obj[i], loc.concat(i), pickleProp);
obj[i] = typeof val == "undefined" ? null : val;
}
return obj;
}
switch(typeof obj) {
case "object":
// Detect circular reference
if (obj[pickleProp]) return {"$ref": obj[pickleProp]};
// Create copy of object
var out = {};
for (var prop in obj) {
if (!obj.hasOwnProperty(prop)) continue;
var val = _pickle(obj[prop], loc.concat(prop), pickleProp);
if (typeof val != "undefined") out[prop] = val;
}
// If the prototype of the object has a name we save it as well
// so we can reconstruct the object
if (obj.__proto__ && obj.__proto__.__name__)
out.__class__ = obj.__proto__.__name__;
// Save our location
obj[pickleProp] = loc;
return out;
case "boolean":
case "number":
case "string":
return obj;
case "undefined":
case "function":
case "xml": //TODO: Support XML?
default:
return; //Return undefined;
}
}
var pickle = exports.pickle = function(obj) {
var pickleProp = "__pickle" + (pickleId++) + "__";
return _pickle(obj, [], pickleProp);
}
// Fetches loc from obj
function _fetch(obj, loc) {
if (loc.length == 0) return obj;
else {
var x = loc.shift();
return _fetch(obj[x], loc);
}
}
function _newobjfunc(out) {
// In this case we create a new object
var prototype = getPrototypeFromClassName(out.__class__);
var inst = Object.create(prototype);
instance(inst); //Register instance
for (var prop in out) {
if (!out.hasOwnProperty(prop)) continue;
inst[prop] = out[prop];
}
//Reload the class by calling its constructor
if (prototype.__init__) prototype.__init__.call(inst, inst);
return inst;
}
function _replaceobjfunc(out) {
var instance = getInstance(out.__id__);
for (var prop in out) {
if (!out.hasOwnProperty(prop)) continue;
instance[prop] = out[prop];
}
for (var prop in instance) {
if (!instance.hasOwnProperty(prop)) continue;
if (!out[prop] || !out.hasOwnProperty(prop)) delete instance[prop];
}
var proto = Object.getPrototypeOf(instance);
// Reload by calling the instance's constructor
if (proto.__init__) proto.__init__.call(instance, instance);
delete instance["__class__"];
return instance;
}
function _unpickle(obj, master, objFunc) {
if (obj == null) return null;
if (Object.prototype.toString.call(obj) === "[object Array]") {
for (var i = 0; i < obj.length; ++i) {
obj[i] = _unpickle(obj[i], master, objFunc);
}
return obj;
}
switch(typeof obj) {
case "object":
if (obj["$ref"]) {
return _unpickle(_fetch(master, obj["$ref"]), master, objFunc);
}
for (var prop in obj) {
if (!obj.hasOwnProperty(prop)) continue;
obj[prop] = _unpickle(obj[prop], master, objFunc);
}
if (obj["__class__"]) obj = objFunc(obj);
return obj;
case "boolean":
case "number":
case "string":
return obj;
case "undefined":
case "function":
case "xml": //TODO: Support XML?
default:
return; //Return undefined;
}
}
// Reconstruct original objects from JSON obj
var unpickle = exports.unpickle = function(json) {
json = JSON.parse(JSON.stringify(json)); //Deep copy of JSON object
return _unpickle(json, json, _newobjfunc);
}
/* Like unpickle, but instead of constructing new objects,
* replace existing objects instead. */
var replace = exports.replace = function(json) {
json = JSON.parse(JSON.stringify(json)); //Deep copy of JSON object
return _unpickle(json, json, _replaceobjfunc);
}
// vim: set expandtab tabstop=2 shiftwidth=2:
/*
* This example shows fun.js's super fun OOP implementation
*
* For more information on Python's C3 MRO see
* <http://www.python.org/download/releases/2.3/mro/>
*/
var fun = require("./fun"),
fs = require("fs"),
moduleId = module.filename + "/";
/*
* We define a few classes. The first argument is
* the class name **which must follow a certain pattern**.
* The class name follows the template
* module.filename + "/" + unique_class_name
* The unique_class_name must not contain slashes and
* the string segment before the slash MUST be the
* absolute path of the module (available in module.filename)
* fun.js uses the path of the module to load classes
* when unpickling pickles.
*
* The second argument is a list of bases to
* inherit from. The third agument is the class
* definition.
*/
var O = fun.cls(moduleId + "O", [], {});
var A = fun.cls(moduleId + "A", [O], {});
var B = fun.cls(moduleId + "B", [O], {});
var C = fun.cls(moduleId + "C", [O], {});
var D = fun.cls(moduleId + "D", [O], {});
var E = fun.cls(moduleId + "E", [O], {});
var K1 = fun.cls(moduleId + "K1", [A, B, C], {});
var K2 = fun.cls(moduleId + "K2", [D, B, E], {});
var K3 = fun.cls(moduleId + "K3", [D, A], {});
var Z = fun.cls(moduleId + "Z", [K1, K2, K3], {});
// Note that Multiple inheritance using C3 MRO is
// handled the same as Python, allowing nice things
// like mixins.
console.log(Z.__mro__);
/*
* Module loading and reloading.
*
* You can reload modules, and all modules that depend
* on that module directly or indirectly will be reloaded
* as well, except for the root module to prevent infinite
* recursion.
*
* As a bonus, all instances of classes inside the
* reloaded module(s) will be updated as well.
*/
var test1 = [
"var fun = require('./fun'),",
" moduleId = module.filename + '/';",
"exports.Robot = fun.cls(moduleId + 'Robot', [], {",
" __init__: function(args) {",
" console.log('Robot constructor called with args = ', args);",
" this.local = 'Local var';",
" },",
" sayHi: function() {",
" console.log('Hello!');",
" }",
"});"
].join("\n");
var test2 = [
"var fun = require('./fun'),",
" moduleId = module.filename + '/';",
"exports.Robot = fun.cls(moduleId + 'Robot', [], {",
" __init__: function(args) {",
" console.log('Robot constructor called with args = ', args);",
" this.local = 'Local var';",
" },",
" sayHi: function() {",
" console.log('No I hate you');",
" }",
"});"
].join("\n");
fs.writeFileSync("test.js", test1); //Write source code
var test = require("./test.js"); //Load module
var hiRobot = new test.Robot({}); //Create new instance
hiRobot.sayHi(); //Hello!
fs.writeFileSync("test.js", test2); //Change the source code of the module
// Reload module. Note that the Robot constructor will
// be called again with the first argument set
// to the hiRobot instance. This allows classes to
// update existing instances to add/modify local variables
// in order to adapt to changes in the source code.
fun.reload("./test.js");
hiRobot.sayHi(); //No I hate you
/*
* Pickling and Unpickling
*
* fun.pickle(obj) allows you to convert anything into a
* JSON representation. As a bonus, circular references
* and instance/class information is handled as well.
*
* fun.js allows you to unpickle anything in 2 ways.
* fun.unpickle(json) does the reverse of fun.pickle.
* fun.replace(json) is special, it *replaces* the state
* of instances of the pickle. Demo below,
*/
// An empty class for testing purposes
var TestClass = fun.cls(moduleId + "TestClass", [], {});
var inst = new TestClass();
inst.text = "Before";
console.log("inst.text = ", inst.text); //Before
var pickle = fun.pickle(inst); //Pickle inst, saving its state
inst.text = "After"; //Change the text
console.log("inst.text = ", inst.text); //After
var inst2 = fun.unpickle(pickle);
console.log("inst2.text = ", inst2.text); //Before
console.log("inst.text = ", inst.text); //After
fun.replace(pickle);
console.log("inst.text = ", inst.text); //Before
// vim: set expandtab tabstop=2 shiftwidth=2:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment