Skip to content

Instantly share code, notes, and snippets.

@mmthomas
Created November 16, 2012 17:12
Show Gist options
  • Save mmthomas/4089076 to your computer and use it in GitHub Desktop.
Save mmthomas/4089076 to your computer and use it in GitHub Desktop.
A simple JavaScript dependency injection container
/*
*
* 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;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment