From 1f850329b4a6c9bfd14b618a9d76ef802eea28ca Mon Sep 17 00:00:00 2001 From: Carlo Zancanaro Date: Mon, 29 Sep 2014 01:21:08 +1000 Subject: Lots of changes: now async, behaviour almost entirely managed by plugins --- injector.js | 507 +++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 400 insertions(+), 107 deletions(-) (limited to 'injector.js') 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; -- cgit v1.2.3