summaryrefslogtreecommitdiff
path: root/injector.js
diff options
context:
space:
mode:
Diffstat (limited to 'injector.js')
-rw-r--r--injector.js507
1 files changed, 400 insertions, 107 deletions
diff --git a/injector.js b/injector.js
index 4d8b293..e644539 100644
--- a/injector.js
+++ b/injector.js
@@ -1,133 +1,426 @@
-/*global module*/
+/*global module, setTimeout*/
+
var Injector = (function() {
- var Injector = function() {
- this.specs = {};
- this.values = {};
- this.stack = [];
- this.usedRequestor = null;
- };
+ 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);
+ }
+ };
+ };
- Injector.prototype.register = function(name, spec) {
- if (typeof(name) == "function" || name instanceof Function) {
- this.specs[name.name] = name;
- } else if (typeof(name) == "string" || name instanceof String) {
- this.specs[name] = spec;
- } else {
- for (var key in name) {
- this.register(key, name[key]);
+ 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 this;
- };
+ return deferred.promise;
+ };
- Injector.prototype.get = function(name) {
- if (name in this.specs) {
- var oldUsedRequestor = this.usedRequestor;
- this.usedRequestor = false;
- this.stack.push(name);
- try {
- throwIfCyclic(name, this.stack);
- var result = this.invoke(this.specs[name]);
- if (this.usedRequestor) {
- return result;
+ 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 {
- delete this.specs[name];
- return (this.values[name] = result);
+ return null;
}
- } catch (e) {
- throw (e instanceof InjectorError
- ? e
- : new InjectorError("Error constructing value for " + stackString(this.stack), e));
- } finally {
- this.stack.pop();
- this.usedRequestor = oldUsedRequestor;
}
- }
- if (name in this.values) {
- return this.values[name];
- } else {
- throw new InjectorError("Dependency " + name + " not found");
- }
+ };
};
- Injector.prototype.requestor = function() {
- switch (this.stack.length) {
- case 0:
- throw new InjectorError("Cannot use requestor for invoked function - none exists");
- case 1:
- throw new InjectorError("Cannot use requestor for top-level constructor - none exists");
- default:
- if (this.usedRequestor === false)
- this.usedRequestor = true;
- return this.stack[this.stack.length - 2];
- }
- };
+ Injector.DOMPlugin = (function() {
+ var DOMPlugin = function() {
+ this.aliases = {};
+ };
- Injector.prototype.invoke = function(spec) {
- var parsed = parseSpec(spec);
- var fn = parsed[0];
- var dependencies = parsed[1];
- return fn.apply(this, dependencies.map(function(dependency) {
- return this.get(dependency);
- }, this));
- };
+ DOMPlugin.prototype.register = function(injector, name, spec) {
+ this.aliases[name] = spec;
+ return true;
+ };
- Injector.prototype.construct = function() {
- };
+ 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;
+ })();
- var parsingRegex = /^function[^(]*\(([^)]*)\)/;
- var parseFnArgs = function(fn) {
- var parts = parsingRegex.exec(fn.toString().replace(/\s+/g, ""));
- if (parts == null) {
- throw new Error("Unable to parse fn definition");
- } else {
- return parts[1] ? parts[1].split(/,/) : [];
- }
- };
+ Injector.HTTPPlugin = (function() {
+ var HTTPPlugin = function() {
+ this.aliases = {};
+ };
- 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];
- };
+ 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;
+ };
- var stackString = function(stack) {
- stack.reverse();
- var result = stack.join(" <- ");
- stack.reverse();
- return result;
- };
+ return HTTPPlugin;
+ })();
- var throwIfCyclic = function(name, stack) {
- if (stack.indexOf(name) != stack.length - 1) {
- throw new InjectorError("Cyclic dependency detected " + stackString(stack));
- }
- };
+ 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;
+ })();
- 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;
})();
-
if (typeof(module) !== "undefined")
module.exports = Injector;