Last active
December 16, 2015 07:59
-
-
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* 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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * 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