/* * * A simple JavaScript dependency injection container * By Monroe Thomas http://blog.coolmuse.com * * http://blog.coolmuse.com/2012/11/11/a-simple-javascript-dependency-injection-container/ * * MIT Licensed. * * Unit tests can be found at https://gist.github.com/4270523 * */ /** * Defines a service by annotating a service constructor function with an array of * service identities * @param {Function|String|Array} identitiesOrConstructor The identities of service dependencies, * or the service constructor if no dependencies exist. * @param {Function} [serviceConstructor] The service constructor. * @return {Function} The annotated service constructor function. */ function defineService(identitiesOrConstructor, serviceConstructor) { if (typeof identitiesOrConstructor === "function") { serviceConstructor = identitiesOrConstructor; if (typeof serviceConstructor.dependencyIdentities !== "undefined") { return serviceConstructor; } identitiesOrConstructor = []; } else if (typeof identitiesOrConstructor === "string") { identitiesOrConstructor = [identitiesOrConstructor]; // wrap in an array } if (!Array.isArray(identitiesOrConstructor)) throw new Error("identitiesOrConstructor must be an array."); if (typeof serviceConstructor !== "function") throw new Error("serviceConstructor must be a function."); // annotate the constructor with the dependency identity array serviceConstructor.dependencyIdentities = identitiesOrConstructor; return serviceConstructor; } /** * Returns a service kernel. * @constructor */ function ServiceKernel() { var instances = {}; var definitions = {}; var beingResolved = {}; var pendingCallbacks = []; /** * Returns the service instance corresponding to the specified service identity. * @param {String} identity * @return {*} The service instance; or undefined if the service is being resolved. */ function getInstance(identity) { if (identity in beingResolved) return undefined; if (identity in instances) return instances[identity]; if (identity in definitions) return resolveInstance(identity); throw new Error("The service '" + identity + "' has not been defined.", "identity"); } /** * Resolves the service instance corresponding to the specified service identity. * @param {String} identity * @return {*} The service instance. */ function resolveInstance(identity) { if (identity in beingResolved) { throw new Error("resolveInstance is already being called for the service '" + identity + "'."); } var instance; try { beingResolved[identity] = true; var definition = definitions[identity]; // gather the service constructor arguments var dependencies = []; if (definition.dependencyIdentities && Array.isArray(definition.dependencyIdentities)) { for (var i = 0; i < definition.dependencyIdentities.length; i++) { // recursively resolve service dependency; // may be undefined in case of a circular dependency instance = getInstance(definition.dependencyIdentities[i]); dependencies.push(instance); } } // call the service constructor function ConstructorThunk() { return definition.apply(this, arguments[0]); } ConstructorThunk.prototype = definition.prototype; instance = new ConstructorThunk(dependencies); instances[identity] = instance; } finally { delete beingResolved[identity]; } // resolve any pending require calls that may need this instance resolvePending(identity, instance); return instance; } /** * Checks if any pending require callbacks can be completed with the specified service. * @param {String} identity The resolved service identity. * @param {*} instance The resolved service instance. */ function resolvePending(identity, instance) { if (pendingCallbacks.length === 0) return; var resolved, i; for (i = 0; i < pendingCallbacks.length; i++) { if (pendingCallbacks[i].resolve(identity, instance)) { resolved = resolved || []; resolved.push(i); } } if (resolved) { for (i = 0; i < resolved.length; i++) { pendingCallbacks.splice(resolved[i], 1); } } } /** * Returns an object with a resolve function that can be called when a new service instance is created; * the resolve function will return true if all dependencies have been satisfied * and the callback method has been invoked; otherwise it will return false * @param {Array} identities An array of service identity strings. * @param {Array} dependencies An array of dependencies; unresolved dependencies have a value of undefined. * @param {Number} pending The number of unresolved dependencies. * @param {Function} callback The callback to invoke when all dependencies are resolved. * @return {Object} An object containing a resolve function. * @constructor */ function PendingCallback(identities, dependencies, pending, callback) { if (!Array.isArray(identities)) throw new Error("identities must be an array."); if (!Array.isArray(dependencies)) throw new Error("dependencies must be an array."); if (typeof pending !== "number") throw new Error("pending must be a number."); if (typeof callback !== "function") throw new Error("callback must be a function."); if (pending <= 0) throw new Error("pending must be positive."); /** * Checks if the specified service resolves the callback criteria. * @param {String} identity The service identity to resolve. * @param {*} instance The resolved service instance. * @return {Boolean} True if all dependencies are resolved; otherwise false. */ function resolve (identity, instance) { var index = identities.indexOf(identity); if (index === -1) return false; dependencies[index] = instance; if (0 === --pending) { callback.apply({}, dependencies); return true; } return false; } return { /** * Checks if the specified service resolves the callback criteria. * @param {String} identity The service identity to resolve. * @param {*} instance The resolved service instance. * @return {Boolean} True if all dependencies are resolved; otherwise false. */ resolve : resolve } } /** * Defines a service within the kernel. * @param {String} identity The service identity. * @param {Function|String|Array} dependencyIdentitiesOrConstructor * The identities of service dependencies, * or the service constructor if no dependencies exist. * @param {Function} [serviceConstructor] The service constructor. */ function define(identity, dependencyIdentitiesOrConstructor, serviceConstructor) { if (typeof identity !== "string") throw new Error("identity must be a string."); if (identity.length === 0) throw new Error("The identity string may not be empty."); if (identity in definitions) { throw new Error("The service '" + identity + "' has already been defined."); } var definition = defineService(dependencyIdentitiesOrConstructor, serviceConstructor); definitions[identity] = definition; } /** * Defines a service within the kernel based on an existing instance. * Equivalent to calling define(instance, function() { return instance; }); * @param {String} identity The service identity. * @param {*} instance The service instance. */ function defineInstance(identity, instance) { this.define(identity, function() { return instance; }); } /** * Undefines a service. * @param {String} identity The service identity. * @return {Boolean} Returns true if the service was undefined; false otherwise. */ function undefine(identity) { if (typeof identity !== "string") throw new Error("identity must be a string."); if (identity in definitions) { delete definitions[identity]; if (identity in instances) { delete instances[identity]; } return true; } return false; } /** * Returns one or more services. Has similar semantics to the AMD require() method. * @param {String|Array} identities The identities of the services required by the callback. * If callback is not specified, then this must be a string. * @param {Function} [callback] The callback to invoke with the required service instances. * If this is not specified, then identities must be a string, * and the required instance is returned. * @return {*} Returns the specified service instance if no callback is specified; * otherwise returns void. */ function require(identities, callback) { // synchronous version if (typeof callback === "undefined") { if (typeof identities !== "string") throw new Error("identities must be a string when no callback is specified."); var instance = getInstance(identities); if (typeof instance === "undefined") { throw new Error("The service '" + identities + "' has not been defined."); } return instance; } if (typeof identities === "string") { identities = [identities]; // wrap in an array } if (!Array.isArray(identities)) throw new Error("identities must be an array."); if (typeof callback !== "function") throw new Error("callback must be a function."); // gather callback arguments var dependencies = []; var pending = 0; for (var i = 0; i < identities.length; i++) { var instance = getInstance(identities[i]); dependencies.push(instance); if (typeof instance === "undefined") { pending++; } } if (pending > 0) { pendingCallbacks.push(PendingCallback(identities, dependencies, pending, callback)); } else { callback.apply({}, dependencies); } } // create the object that contains the kernel methods var kernel = { /** * Defines a service within the kernel. * @param {String} identity The service identity. * @param {Function|String|Array} dependencyIdentitiesOrConstructor * The identities of service dependencies, * or the service constructor if no dependencies exist. * @param {Function} [serviceConstructor] The service constructor. */ define: define, /** * Defines a service within the kernel based on an existing instance. * Equivalent to calling define(instance, function() { return instance; }); * @param {String} identity The service identity. * @param {*} instance The service instance. */ defineInstance: defineInstance, /** * Undefines a service. * @param {String} identity The service identity. * @return {Boolean} Returns true if the service was undefined; false otherwise. */ undefine : undefine, /** * Returns one or more services. Has similar semantics to the AMD require() method. * @param {String|Array} identities The identities of the services required by the callback. * If callback is not specified, then this must be a string. * @param {Function} [callback] The callback to invoke with the required service instances. * If this is not specified, then identities must be a string, * and the required instance is returned. * @return {*} Returns the specified service instance if no callback is specified; * otherwise returns void. */ require : require } // define the kernel itself and its require method as services kernel.defineInstance("kernel", kernel); kernel.defineInstance("require", require.bind(this)); return kernel; }