From fab0b505c608d2a553c1ca74553e6a433e453a5d Mon Sep 17 00:00:00 2001 From: Carlo Zancanaro Date: Mon, 29 Sep 2014 01:32:38 +1000 Subject: Rename to Scorpion: sounds cooler --- scorpion.js | 426 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 scorpion.js (limited to 'scorpion.js') diff --git a/scorpion.js b/scorpion.js new file mode 100644 index 0000000..6d7051c --- /dev/null +++ b/scorpion.js @@ -0,0 +1,426 @@ +/*global module, setTimeout*/ + +var Scorpion = (function() { + + var Deferred = (function() { + var defer = function(fn, value) {setTimeout(function() {fn(value);});}; + var runFn = function(fn, deferred, action) { + return function(value) { + try { + var result = fn(value); + if (result && result.then) { + result.then(function(value) { + deferred.resolve(value); + }, function(value) { + deferred.reject(value); + }); + } else { + deferred.resolve(result); + } + } catch (e) { + deferred.reject(e); + } + }; + }; + + var Promise = function() { + this.resolved = false; + this.rejected = false; + this.onSuccess = []; + this.onError = []; + }; + Promise.prototype.then = function(success, error) { + success = success || function(x) {return x;}; + error = error || function(x) {throw x;}; + var deferred = new Deferred(); + var successFn = runFn(success, deferred); + var errorFn = runFn(error, deferred); + if (this.resolved) { + defer(successFn, this.value); + } else if (this.rejected) { + defer(errorFn, this.value); + } else { + if (this.onSuccess != null) + this.onSuccess.push(successFn); + if (this.onError != null) + this.onError.push(errorFn); + } + return deferred.promise; + }; + + var Deferred = function() { + this.promise = new Promise(); + }; + Deferred.prototype.resolve = function(value) { + if (this.promise.resolved) { + throw new Error("Cannot re-resolve already resolved promise"); + } else if (this.promise.rejected) { + throw new Error("Cannot resolve a rejected promise"); + } else { + this.promise.resolved = true; + this.promise.value = value; + var handlers = this.promise.onSuccess; + this.promise.onSuccess = null; + this.promise.onError = null; + setTimeout(function() { + handlers.forEach(function(handler) { + handler(value); + }); + }); + } + }; + Deferred.prototype.reject = function(value) { + if (this.promise.resolved) { + throw new Error("Cannot reject an already resolved promise"); + } else if (this.promise.rejected) { + throw new Error("Cannot re-reject a rejected promise"); + } else { + this.promise.rejected = true; + this.promise.value = value; + var handlers = this.promise.onError; + this.promise.onSuccess = null; + this.promise.onError = null; + setTimeout(function() { + handlers.forEach(function(handler) { + handler(value); + }); + }); + } + }; + + Deferred.waitForAll = function(promises) { + var deferred = new Deferred(); + var successes = 0, errors = 0; + var errorResults = {}, successResults = {}; + var expected = promises.length; + promises.forEach(function(promise, i) { + promise.then(function(value) { + successResults[i] = value; + successes++; + maybeFinish(); + }, function(error) { + errorResults[i] = error; + errors++; + maybeFinish(); + }); + }); + var maybeFinish = function() { + if (successes == expected) { + var array = []; + for (var i = 0, l = expected; i < l; ++i) + array.push(successResults[i]); + deferred.resolve(array); + } else if (successes + errors == expected) { + deferred.reject({ + errors: errorResults, + values: successResults + }); + } + }; + maybeFinish(); + return deferred.promise; + }; + + return Deferred; + })(); + + var DepsParser = (function() { + var fnArgRegex = /^function[^(]*\(([^)]*)\)/; + var parseFnArgs = function(fn) { + var parts = fnArgRegex.exec(fn.toString().replace(/\s+/g, "")); + if (parts == null) { + throw new Error("Unable to parse fn definition"); + } else { + return parts[1] ? parts[1].split(/,/) : []; + } + }; + + var parseSpec = function(spec) { + var fn, dependencies; + if (typeof(spec) == "function" || spec instanceof Function) { + dependencies = parseFnArgs(spec); + fn = spec; + } else { + fn = spec[spec.length - 1]; + dependencies = spec.slice(0, spec.length - 1); + } + return [fn, dependencies]; + }; + + var parseDep = function(dep) { + var parts = dep.split("!"); + if (parts.length == 0) { + throw new Error("Invalid dependency: " + dep); + } else if (parts.length == 1) { + return {prefix: "", name: parts[0]}; + } else { + return {prefix: parts[0], name: parts.slice(1).join("!")}; + } + }; + + return { + spec: parseSpec, + dep: parseDep + }; + })(); + + var Scorpion = (function(Deferred, DepsParser) { + + var Scorpion = function(plugins) { + // plugins are a list to be executed in order + this.plugins = plugins; + }; + + Scorpion.rejected = function(message, cause) { + var deferred = this.defer(); + deferred.reject(this.error(message, cause)); + return deferred.promise; + }; + Scorpion.prototype.rejected = Scorpion.rejected; + + Scorpion.resolved = function(value) { + var deferred = this.defer(); + deferred.resolve(value); + return deferred.promise; + }; + Scorpion.prototype.resolved = Scorpion.resolved; + + Scorpion.prototype.get = function(dep, stack) { + stack = [dep].concat(stack || []); + if (stack && stack.lastIndexOf(dep) != 0) { + return this.rejected("Cyclic dependency: " + stack.join(" <- ")); + } + for (var i = 0, l = this.plugins.length; i < l; ++i) { + var value = this.plugins[i].get(this, dep, stack); + if (value) { + if (!value.destroy) + value.destroy = function(){return Scorpion.resolved(true);}; + return value; + } + } + return this.rejected("Unknown dependency: " + stack.join(" <- ")); + }; + + Scorpion.prototype.register = function(name, value) { + for (var i = 0, l = this.plugins.length; i < l; ++i) { + if (this.plugins[i].register(this, name, value)) + return this; + } + throw this.error("No plugin handled registration of: " + name); + return this; + }; + + Scorpion.prototype.invoke = function(spec) { + var injector = this; + var parsed = this.parseSpec(spec); + var fn = parsed[0]; + var dependencies = parsed[1]; + var depPromises = dependencies.map(function(dep) { + return injector.get(dep, []); + }); + var depPromise = this.waitForAll(depPromises); + var result = depPromise.then(function(results) { + return fn.apply(injector, results); + }); + result.destroy = function() { + return Scorpion.waitForAll(depPromises.map(function(promise) { + return promise.destroy(); + })).then(function() { + return true; + }); + }; + return result; + }; + + + + /* static utility functions */ + + Scorpion.error = function(message, cause) {return new ScorpionError(message, cause);}; + Scorpion.prototype.error = Scorpion.error; + + Scorpion.defer = function() {return new Deferred();}; + Scorpion.prototype.defer = Scorpion.defer; + + Scorpion.parseSpec = DepsParser.spec; + Scorpion.prototype.parseSpec = DepsParser.spec; + + Scorpion.waitForAll = Deferred.waitForAll; + Scorpion.prototype.waitForAll = Deferred.waitForAll; + + /* the injector error type */ + + var ScorpionError = function(message, cause) { + this.name = "ScorpionError"; + this.message = message; + this.cause = cause; + }; + ScorpionError.prototype = new Error(); + ScorpionError.prototype.constructor = ScorpionError; + ScorpionError.prototype.toString = function() { + return "ScorpionError: " + this.message + (this.cause ? " [caused by " + this.cause + "]" : ""); + }; + + return Scorpion; + + })(Deferred, DepsParser); + + Scorpion.prefixPlugin = function(prefix, plugin) { + return { + register: function(injector, name, spec) { + if (name.indexOf(prefix) == 0) { + return plugin.register(injector, + name.substr(prefix.length), + spec); + } else { + return false; + } + }, + get: function(injector, name, stack) { + if (name.indexOf(prefix) == 0) { + return plugin.get(injector, + name.substr(prefix.length), + stack); + } else { + return null; + } + } + }; + }; + + Scorpion.DOMPlugin = (function() { + var DOMPlugin = function() { + this.aliases = {}; + }; + + DOMPlugin.prototype.register = function(injector, name, spec) { + this.aliases[name] = spec; + return true; + }; + + DOMPlugin.prototype.get = function(injector, name, stack) { + var deferred = injector.defer(); + var interval = setInterval(function() { + var obj = $(this.aliases[name] || name); + if (obj.length) { + clearInterval(interval); + deferred.resolve(obj); + } + }.bind(this), 100); + return deferred.promise; + }; + + return DOMPlugin; + })(); + + Scorpion.HTTPPlugin = (function() { + var HTTPPlugin = function() { + this.aliases = {}; + }; + + HTTPPlugin.prototype.register = function(injector, name, spec) { + this.aliases[name] = spec; + return true; + }; + + HTTPPlugin.prototype.get = function(injector, name, stack) { + var deferred = injector.defer(); + $.ajax(this.aliases[name] || name).then(function(result) { + deferred.resolve(result); + // deferred.resolve.bind(deferred); + }, deferred.reject.bind(deferred)); + deferred.promise.destroy = function() { + return Scorpion.resolved(true); + }; + return deferred.promise; + }; + + return HTTPPlugin; + })(); + + Scorpion.ValuePlugin = (function() { + var ValuePlugin = function() { + this.specs = {}; + this.values = {}; + }; + + ValuePlugin.prototype.register = function(injector, name, spec) { + this.specs[name] = spec; + return true; + }; + + ValuePlugin.prototype.get = function(injector, name, stack) { + if (name in this.values) { + this.values[name].references++; + return this.values[name]; + } else if (name in this.specs) { + var spec = this.specs[name]; + var parsed = injector.parseSpec(spec); + var constructor = parsed[0]; + var dependencies = parsed[1]; + var depPromises = dependencies.map(function(dep) { + return injector.get(dep, [name].concat(stack)); + }); + var depPromise = injector.waitForAll(depPromises); + + var onDestroy = null; + var readRequestor = false; + var deferred = injector.defer(); + var result = deferred.promise; + depPromise.then(function(results) { + var wrappedScorpion = Object.create(injector); + wrappedScorpion.requestor = function() { + readRequestor = true; + return stack[1]; + }; + try { + deferred.resolve(constructor.apply(wrappedScorpion, results)); + } catch (e) { + deferred.reject(e); + } finally { + onDestroy = wrappedScorpion.onDestroy; + if (result.references <= 0 && onDestroy) + onDestroy(); + } + }, function(e) { + deferred.reject(injector.error("Error constructing " + name)); + }); + + result.references = 1; + var values = this.values; + result.destroy = function() { + this.references--; + if (this.references <= 0) { + if (onDestroy) + onDestroy(); + delete values[name]; + if (!result.resolved && !result.rejected) + deferred.reject(injector.error("Promise destroyed before value completed construction")); + return injector.waitForAll(depPromises.map(function(promise) { + return promise.destroy(); + })).then(function() { + return true; + }); + } else { + return injector.resolved(true); + } + }; + if (readRequestor) { + return result; + } else { + return this.values[name] = result; + } + } else { + return null; + } + }; + + return ValuePlugin; + })(); + + + return Scorpion; +})(); + +if (typeof(module) !== "undefined") + module.exports = Scorpion; -- cgit v1.2.3