/*global module, setTimeout*/ var Injector = (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 Injector = (function(Deferred, DepsParser) { var Injector = function(plugins) { // plugins are a list to be executed in order this.plugins = plugins; }; Injector.rejected = function(message, cause) { var deferred = this.defer(); deferred.reject(this.error(message, cause)); return deferred.promise; }; Injector.prototype.rejected = Injector.rejected; Injector.resolved = function(value) { var deferred = this.defer(); deferred.resolve(value); return deferred.promise; }; Injector.prototype.resolved = Injector.resolved; Injector.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 Injector.resolved(true);}; return value; } } return this.rejected("Unknown dependency: " + stack.join(" <- ")); }; Injector.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; }; Injector.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 Injector.waitForAll(depPromises.map(function(promise) { return promise.destroy(); })).then(function() { return true; }); }; return result; }; /* static utility functions */ Injector.error = function(message, cause) {return new InjectorError(message, cause);}; Injector.prototype.error = Injector.error; Injector.defer = function() {return new Deferred();}; Injector.prototype.defer = Injector.defer; Injector.parseSpec = DepsParser.spec; Injector.prototype.parseSpec = DepsParser.spec; Injector.waitForAll = Deferred.waitForAll; Injector.prototype.waitForAll = Deferred.waitForAll; /* the injector error type */ var InjectorError = function(message, cause) { this.name = "InjectorError"; this.message = message; this.cause = cause; }; InjectorError.prototype = new Error(); InjectorError.prototype.constructor = InjectorError; InjectorError.prototype.toString = function() { return "InjectorError: " + this.message + (this.cause ? " [caused by " + this.cause + "]" : ""); }; return Injector; })(Deferred, DepsParser); Injector.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; } } }; }; Injector.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; })(); Injector.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 Injector.resolved(true); }; return deferred.promise; }; return HTTPPlugin; })(); Injector.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 wrappedInjector = Object.create(injector); wrappedInjector.requestor = function() { readRequestor = true; return stack[1]; }; try { deferred.resolve(constructor.apply(wrappedInjector, results)); } catch (e) { deferred.reject(e); } finally { onDestroy = wrappedInjector.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 Injector; })(); if (typeof(module) !== "undefined") module.exports = Injector;