/*global module, setTimeout*/ var Scorpion = (function() { var Deferred = (function() { var defer = function(fn, value) {setTimeout(function() {fn(value);});}; var runFn = function(fn, deferred) { 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; }; Deferred.collapseErrorObject = function(obj) { var errors = []; for (var key in obj.errors) errors = errors.concat(obj.errors[key]); throw errors; }; 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) { if (typeof(name) == "function" || name instanceof Function) { value = name; name = value.name; } if (typeof(name) == "string" || name instanceof String) { 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; } else { for (var key in name) { this.register(key, name[key]); } 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); }, Deferred.collapseErrorObject); result.destroy = function() { return Scorpion.waitForAll(depPromises.map(function(promise) { return promise.destroy(); })).then(function() { return true; }, Deferred.collapseErrorObject); }; 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(); if (typeof($) !== "undefined" && $) { var intervalFn = function() { var obj = $(this.aliases[name] || name); if (obj.length) { clearInterval(interval); deferred.resolve(obj); } }.bind(this); var interval = setInterval(intervalFn, 100); intervalFn(); } else { deferred.reject(new Error("jQuery was not found " + stack.join(" <- "))); } 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(); if (typeof($) !== "undefined" && $) { $.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); }; } else { deferred.reject(new Error("jQuery was not found " + stack.join(" <- "))); } 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 readRequestor = false; var depPromises = dependencies.map(function(dep) { if (dep == "__requestor__") { readRequestor = true; return injector.resolved(stack[1]); } else { return injector.get(dep, stack); } }); var depPromise = injector.waitForAll(depPromises); var onDestroy = null; var deferred = injector.defer(); var result = deferred.promise; depPromise.then(function(results) { var wrappedScorpion = Object.create(injector); try { deferred.resolve(constructor.apply(wrappedScorpion, results)); } catch (e) { deferred.reject(e); } finally { onDestroy = wrappedScorpion.onDestroy; if (result.references <= 0 && onDestroy) onDestroy(); } }, function(e) { try { Deferred.collapseErrorObject(e); } catch (ex) { deferred.reject(ex); } }); 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; }, Deferred.collapseErrorObject); } 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;