knifecycle
Advanced tools
Comparing version 1.3.1 to 1.4.0
@@ -0,1 +1,21 @@ | ||
<a name="1.4.0"></a> | ||
# [1.4.0](https://github.com/nfroidure/knifecycle/compare/v1.3.1...v1.4.0) (2017-05-22) | ||
### Bug Fixes | ||
* **Providers:** Allow services to have options too ([75bffcf](https://github.com/nfroidure/knifecycle/commit/75bffcf)) | ||
* **Singletons:** Ensure singletons aren't shut down ([1de26d6](https://github.com/nfroidure/knifecycle/commit/1de26d6)) | ||
### Features | ||
* **Bad usage:** Fail on dependencies declaration for constant ([ab57c18](https://github.com/nfroidure/knifecycle/commit/ab57c18)) | ||
* **Dependencies declarations:** Allow to make some dependencies optional ([0944709](https://github.com/nfroidure/knifecycle/commit/0944709)), closes [#23](https://github.com/nfroidure/knifecycle/issues/23) | ||
* **Providers:** Allow to declare providers as singletons ([dad9006](https://github.com/nfroidure/knifecycle/commit/dad9006)), closes [#3](https://github.com/nfroidure/knifecycle/issues/3) | ||
* **Shutdown:** Allow to shutdown all silos ([7af87de](https://github.com/nfroidure/knifecycle/commit/7af87de)) | ||
* **Singletons:** Shutdown singletons when not used per any silo ([f953851](https://github.com/nfroidure/knifecycle/commit/f953851)) | ||
<a name="1.3.1"></a> | ||
@@ -2,0 +22,0 @@ ## [1.3.1](https://github.com/nfroidure/knifecycle/compare/v1.3.0...v1.3.1) (2017-03-14) |
@@ -23,2 +23,4 @@ 'use strict'; | ||
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } | ||
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } | ||
@@ -29,3 +31,5 @@ | ||
var SHUTDOWN = '$shutdown'; | ||
var SHUTDOWN_ALL = '$shutdownAll'; | ||
var INJECT = '$inject'; | ||
var SILO_CONTEXT = '$siloContext'; | ||
var FATAL_ERROR = '$fatalError'; | ||
@@ -37,3 +41,5 @@ var E_UNMATCHED_DEPENDENCY = 'E_UNMATCHED_DEPENDENCY'; | ||
var E_BAD_INJECTION = 'E_BAD_INJECTION'; | ||
var E_CONSTANT_INJECTION = 'E_CONSTANT_INJECTION'; | ||
var DECLARATION_SEPARATOR = ':'; | ||
var OPTIONAL_FLAG = '?'; | ||
@@ -43,3 +49,30 @@ // Constants that should use Symbol whenever possible | ||
var DEPENDENCIES = '__dependencies'; | ||
var OPTIONS = '__options'; | ||
/* Architecture Note #1: Knifecycle | ||
The `knifecycle` project is intended to be a [dependency | ||
injection](https://en.wikipedia.org/wiki/Dependency_injection) | ||
and [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control) | ||
tool. It will always be tied to this goal since I prefer | ||
composing software instead of using frameworks. | ||
It is designed to have a low footprint on services code. | ||
There is nothing worse than having to write specific code for | ||
a given tool. With `knifecycle`, services can be either constants, | ||
functions or object created synchronously or asynchronously. They | ||
can be reused elsewhere with no changes at all. | ||
*/ | ||
/* Architecture Note #1.1: OOP | ||
The `knifecycle` use case is one of the rare use case where | ||
[OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) | ||
principles are a good fit. | ||
A service provider is full of state since its concern is | ||
precisely to | ||
[encapsulate](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)) | ||
your application global states. | ||
*/ | ||
var Knifecycle = function () { | ||
@@ -56,5 +89,37 @@ /** | ||
function Knifecycle() { | ||
var _this2 = this; | ||
_classCallCheck(this, Knifecycle); | ||
this._silosCounter = 0; | ||
this._silosContexts = new Set(); | ||
this._servicesProviders = new Map(); | ||
this._singletonsServicesHandles = new Map(); | ||
this._singletonsServicesDescriptors = new Map(); | ||
this._singletonsServicesShutdownsPromises = new Map(); | ||
this.provider(INJECT, this.depends([SILO_CONTEXT], function (_ref) { | ||
var $siloContext = _ref.$siloContext; | ||
return { | ||
servicePromise: Promise.resolve(function (dependenciesDeclarations) { | ||
return _this2._initializeDependencies($siloContext, $siloContext.name, dependenciesDeclarations, true); | ||
}) | ||
}; | ||
})); | ||
this.provider(SHUTDOWN_ALL, function () { | ||
return { | ||
servicePromise: Promise.resolve(function () { | ||
_this2.shutdownPromise = _this2.shutdownPromise || Promise.all([].concat(_toConsumableArray(_this2._silosContexts)).map(function (siloContext) { | ||
return siloContext.servicesDescriptors.get(SHUTDOWN).servicePromise.then(function ($shutdown) { | ||
return $shutdown(); | ||
}); | ||
})); | ||
debug('Shutting down Knifecycle instance.'); | ||
return _this2.shutdownPromise; | ||
}) | ||
}; | ||
}, { | ||
singleton: true | ||
}); | ||
} | ||
@@ -77,2 +142,25 @@ | ||
/* Architecture Note #1.3: Declaring services | ||
The first step to use `knifecycle` is to declare | ||
services. There are three kinds of services: | ||
- constants: a constant is a simple value that will | ||
never change. It can be literal values, objects | ||
or even functions. | ||
- services: services are asynchronous functions | ||
resolving to objects, functions or complexer | ||
objects. Those one just need an initialization | ||
phase that must be done asynchronously. | ||
- providers: they are very similar to services | ||
except they have an additional layer of | ||
complexity. Indeed, they have to be hooked | ||
to the process life cycle to allow graceful | ||
shutdown of the applications build on top of | ||
`knifecycle`. | ||
In addition to this, services and providers can | ||
be declared as singletons. This means that they | ||
will be instanciated once for all for each | ||
executions silos using them (we will cover this | ||
topic later on). | ||
*/ | ||
/** | ||
@@ -94,6 +182,11 @@ * Register a constant service | ||
debug('Registered a new constant:', constantName); | ||
if (constantValue instanceof Function && constantValue[DEPENDENCIES]) { | ||
throw new _yerror2.default(E_CONSTANT_INJECTION, constantValue[DEPENDENCIES]); | ||
} | ||
return this.provider(constantName, Promise.resolve.bind(Promise, { | ||
servicePromise: Promise.resolve(constantValue), | ||
shutdownProvider: Promise.resolve.bind(Promise) | ||
})); | ||
}), { singleton: true }); | ||
} | ||
@@ -105,2 +198,3 @@ | ||
* @param {Function|Promise} service The service promise or a function returning it | ||
* @param {Object} options Options passed to the provider method | ||
* @return {Function} The created service provider | ||
@@ -135,3 +229,3 @@ * @example | ||
key: 'service', | ||
value: function service(serviceName, _service) { | ||
value: function service(serviceName, _service, options) { | ||
function serviceProvider(hash) { | ||
@@ -143,3 +237,3 @@ return { | ||
serviceProvider[DEPENDENCIES] = _service[DEPENDENCIES] || []; | ||
this.provider(serviceName, serviceProvider); | ||
this.provider(serviceName, serviceProvider, options); | ||
debug('Registered a new service:', serviceName); | ||
@@ -153,2 +247,5 @@ return serviceProvider; | ||
* @param {Function} serviceProvider Service provider or a service provider promise | ||
* @param {Object} options Options for the provider | ||
* @param {Object} options.singleton Define the provider as a singleton | ||
* (one instance for several runs) | ||
* @return {Promise} The actual service descriptor promise | ||
@@ -187,18 +284,17 @@ * @example | ||
value: function provider(serviceName, serviceProvider) { | ||
var _this = this; | ||
var _this3 = this; | ||
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; | ||
var uniqueServiceProvider = serviceProvider.bind(); | ||
uniqueServiceProvider[DEPENDENCIES] = serviceProvider[DEPENDENCIES] || []; | ||
uniqueServiceProvider[OPTIONS] = options; | ||
if (uniqueServiceProvider[DEPENDENCIES].map(_pickServiceNameFromDeclaration).includes(serviceName)) { | ||
throw new _yerror2.default(E_CIRCULAR_DEPENDENCY, serviceName); | ||
} | ||
uniqueServiceProvider[DEPENDENCIES].forEach(function (dependencyDeclaration) { | ||
var serviceName = _pickServiceNameFromDeclaration(dependencyDeclaration); | ||
var dependencyProvider = _this._servicesProviders.get(serviceName); | ||
if (dependencyProvider && dependencyProvider[DEPENDENCIES].some(function (childDependencyDeclaration) { | ||
var childServiceName = _pickServiceNameFromDeclaration(childDependencyDeclaration); | ||
return childServiceName === serviceName; | ||
})) { | ||
throw new _yerror2.default(E_CIRCULAR_DEPENDENCY, dependencyDeclaration, serviceName); | ||
} | ||
_this3._lookupCircularDependencies(serviceName, dependencyDeclaration); | ||
}); | ||
@@ -210,3 +306,27 @@ | ||
} | ||
}, { | ||
key: '_lookupCircularDependencies', | ||
value: function _lookupCircularDependencies(rootServiceName, dependencyDeclaration) { | ||
var _this4 = this; | ||
var declarationsStacks = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; | ||
var serviceName = _pickMappedNameFromDeclaration(dependencyDeclaration); | ||
var dependencyProvider = this._servicesProviders.get(serviceName); | ||
if (!dependencyProvider) { | ||
return; | ||
} | ||
declarationsStacks = declarationsStacks.concat(dependencyDeclaration); | ||
dependencyProvider[DEPENDENCIES].forEach(function (childDependencyDeclaration) { | ||
var childServiceName = _pickMappedNameFromDeclaration(childDependencyDeclaration); | ||
if (rootServiceName === childServiceName) { | ||
throw new (Function.prototype.bind.apply(_yerror2.default, [null].concat(_toConsumableArray([E_CIRCULAR_DEPENDENCY, rootServiceName].concat(declarationsStacks).concat(childDependencyDeclaration)))))(); | ||
} | ||
_this4._lookupCircularDependencies(rootServiceName, childDependencyDeclaration, declarationsStacks); | ||
}); | ||
} | ||
/** | ||
@@ -285,12 +405,14 @@ * Decorator to claim that a service depends on others ones. | ||
value: function toMermaidGraph() { | ||
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, | ||
_ref$shapes = _ref.shapes, | ||
shapes = _ref$shapes === undefined ? [] : _ref$shapes, | ||
_ref$styles = _ref.styles, | ||
styles = _ref$styles === undefined ? [] : _ref$styles, | ||
_ref$classes = _ref.classes, | ||
classes = _ref$classes === undefined ? {} : _ref$classes; | ||
var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, | ||
_ref2$shapes = _ref2.shapes, | ||
shapes = _ref2$shapes === undefined ? [] : _ref2$shapes, | ||
_ref2$styles = _ref2.styles, | ||
styles = _ref2$styles === undefined ? [] : _ref2$styles, | ||
_ref2$classes = _ref2.classes, | ||
classes = _ref2$classes === undefined ? {} : _ref2$classes; | ||
var servicesProviders = this._servicesProviders; | ||
var links = Array.from(servicesProviders.keys()).reduce(function (links, serviceName) { | ||
var links = Array.from(servicesProviders.keys()).filter(function (provider) { | ||
return !provider.startsWith('$'); | ||
}).reduce(function (links, serviceName) { | ||
var serviceProvider = servicesProviders.get(serviceName); | ||
@@ -313,5 +435,5 @@ | ||
return ['graph TD'].concat(links.map(function (_ref2) { | ||
var serviceName = _ref2.serviceName, | ||
dependedServiceName = _ref2.dependedServiceName; | ||
return ['graph TD'].concat(links.map(function (_ref3) { | ||
var serviceName = _ref3.serviceName, | ||
dependedServiceName = _ref3.dependedServiceName; | ||
return ' ' + (_applyShapes(shapes, serviceName) || serviceName) + '-->' + (_applyShapes(shapes, dependedServiceName) || dependedServiceName); | ||
@@ -325,2 +447,13 @@ })).concat(Object.keys(classes).map(function (className) { | ||
/* Architecture Note #1.4: Execution silos | ||
Once all the services are declared, we need a way to bring | ||
them to life. Execution silos are where the magic happen. | ||
For each call of the `run` method with given dependencies, | ||
a new silo is created and the required environment to | ||
run the actual code is leveraged. | ||
Depending of your application design, you could run it | ||
in only one execution silo or into several ones | ||
according to the isolation level your wish to reach. | ||
*/ | ||
/** | ||
@@ -346,6 +479,8 @@ * Creates a new execution silo | ||
value: function run(dependenciesDeclarations) { | ||
var _this2 = this; | ||
var _this5 = this; | ||
var _this = this; | ||
var internalDependencies = [].concat(_toConsumableArray(new Set(dependenciesDeclarations.concat(SHUTDOWN)))); | ||
var siloContext = { | ||
name: 'silo-' + Date.now(), | ||
name: 'silo-' + this._silosCounter++, | ||
servicesDescriptors: new Map(), | ||
@@ -357,2 +492,6 @@ servicesSequence: [], | ||
if (this.shutdownPromise) { | ||
throw new _yerror2.default('E_INSTANCE_SHUTDOWN'); | ||
} | ||
// Create a provider for the special fatal error service | ||
@@ -370,10 +509,16 @@ siloContext.servicesDescriptors.set(FATAL_ERROR, { | ||
// Make the siloContext available for internal injections | ||
siloContext.servicesDescriptors.set(SILO_CONTEXT, { | ||
servicePromise: Promise.resolve(siloContext) | ||
}); | ||
// Create a provider for the shutdown special dependency | ||
siloContext.servicesDescriptors.set(SHUTDOWN, { | ||
servicePromise: Promise.resolve(function () { | ||
var shutdownPromise = _shutdownNextServices(siloContext.servicesSequence); | ||
siloContext.shutdownPromise = siloContext.shutdownPromise || _shutdownNextServices(siloContext.servicesSequence); | ||
debug('Shutting down services'); | ||
return shutdownPromise; | ||
return siloContext.shutdownPromise.then(function () { | ||
_this5._silosContexts.delete(siloContext); | ||
}); | ||
@@ -386,4 +531,5 @@ // Shutdown services in their instanciation order | ||
return Promise.all(reversedServiceSequence.pop().map(function (serviceName) { | ||
var serviceDescriptor = siloContext.servicesDescriptors.get(serviceName); | ||
var serviceShutdownPromise = siloContext.servicesShutdownsPromises.get(serviceName); | ||
var singletonServiceDescriptor = _this._singletonsServicesDescriptors.get(serviceName); | ||
var serviceDescriptor = singletonServiceDescriptor || siloContext.servicesDescriptors.get(serviceName); | ||
var serviceShutdownPromise = _this._singletonsServicesShutdownsPromises.get(serviceName) || siloContext.servicesShutdownsPromises.get(serviceName); | ||
@@ -394,2 +540,3 @@ if (serviceShutdownPromise) { | ||
} | ||
if (reversedServiceSequence.some(function (servicesDeclarations) { | ||
@@ -401,4 +548,17 @@ return servicesDeclarations.includes(serviceName); | ||
} | ||
if (singletonServiceDescriptor) { | ||
var handleSet = _this._singletonsServicesHandles.get(serviceName); | ||
handleSet.delete(siloContext.name); | ||
if (handleSet.size) { | ||
debug('Singleton is used elsewhere:', serviceName, handleSet); | ||
return Promise.resolve(); | ||
} | ||
_this._singletonsServicesDescriptors.delete(serviceName); | ||
} | ||
debug('Shutting down a service:', serviceName); | ||
serviceShutdownPromise = serviceDescriptor.shutdownProvider ? serviceDescriptor.shutdownProvider() : Promise.resolve(); | ||
if (singletonServiceDescriptor) { | ||
_this._singletonsServicesShutdownsPromises.set(serviceName, serviceShutdownPromise); | ||
} | ||
siloContext.servicesShutdownsPromises.set(serviceName, serviceShutdownPromise); | ||
@@ -412,13 +572,13 @@ return serviceShutdownPromise; | ||
// Create a provider for the special inject service | ||
siloContext.servicesDescriptors.set(INJECT, { | ||
servicePromise: Promise.resolve(function (dependenciesDeclarations) { | ||
return _this2._initializeDependencies(siloContext, siloContext.name, dependenciesDeclarations, true); | ||
}) | ||
}); | ||
this._silosContexts.add(siloContext); | ||
return this._initializeDependencies(siloContext, siloContext.name, dependenciesDeclarations).then(function (servicesHash) { | ||
return this._initializeDependencies(siloContext, siloContext.name, internalDependencies).then(function (servicesHash) { | ||
debug('Handling fatal errors:', siloContext.errorsPromises); | ||
Promise.all(siloContext.errorsPromises).catch(siloContext.throwFatalError); | ||
return servicesHash; | ||
return dependenciesDeclarations.reduce(function (finalHash, dependencyDeclaration) { | ||
var serviceName = _pickServiceNameFromDeclaration(dependencyDeclaration); | ||
finalHash[serviceName] = servicesHash[serviceName]; | ||
return finalHash; | ||
}, {}); | ||
}); | ||
@@ -439,5 +599,11 @@ } | ||
value: function _getServiceDescriptor(siloContext, injectOnly, serviceName) { | ||
var serviceDescriptor = siloContext.servicesDescriptors.get(serviceName); | ||
var serviceDescriptor = this._singletonsServicesDescriptors.get(serviceName); | ||
if (serviceDescriptor) { | ||
this._singletonsServicesHandles.get(serviceName).add(siloContext.name); | ||
} else { | ||
serviceDescriptor = siloContext.servicesDescriptors.get(serviceName); | ||
} | ||
if (serviceDescriptor) { | ||
return Promise.resolve(serviceDescriptor); | ||
@@ -467,2 +633,4 @@ } | ||
value: function _initializeServiceDescriptor(siloContext, serviceName) { | ||
var _this6 = this; | ||
var serviceProvider = this._servicesProviders.get(serviceName); | ||
@@ -480,3 +648,13 @@ var serviceDescriptorPromise = void 0; | ||
serviceDescriptorPromise = this._initializeDependencies(siloContext, serviceName, serviceProvider[DEPENDENCIES]); | ||
// A singleton service may use a reserved resource | ||
// like a TCP socket. This is why we have to be aware | ||
// of singleton services full shutdown before creating | ||
// a new one | ||
serviceDescriptorPromise = (this._singletonsServicesShutdownsPromises.get(serviceName) || Promise.resolve()). | ||
// Anyway delete any shutdown promise before instanciating | ||
// a new service | ||
then(function () { | ||
_this6._singletonsServicesShutdownsPromises.delete(serviceName); | ||
siloContext.servicesShutdownsPromises.delete(serviceName); | ||
}).then(this._initializeDependencies.bind(this, siloContext, serviceName, serviceProvider[DEPENDENCIES])); | ||
@@ -501,7 +679,14 @@ serviceDescriptorPromise = serviceDescriptorPromise.then(function (deps) { | ||
if (E_UNMATCHED_DEPENDENCY === err.code) { | ||
throw _yerror2.default.wrap.apply(_yerror2.default, [err, E_UNMATCHED_DEPENDENCY, serviceName].concat(err.params)); | ||
throw _yerror2.default.wrap.apply(_yerror2.default, _toConsumableArray([err, E_UNMATCHED_DEPENDENCY, serviceName].concat(err.params))); | ||
} | ||
throw err; | ||
}); | ||
siloContext.servicesDescriptors.set(serviceName, serviceDescriptorPromise); | ||
if (serviceProvider[OPTIONS].singleton) { | ||
var handlesSet = new Set(); | ||
handlesSet.add(siloContext.name); | ||
this._singletonsServicesHandles.set(serviceName, handlesSet); | ||
this._singletonsServicesDescriptors.set(serviceName, serviceDescriptorPromise); | ||
} else { | ||
siloContext.servicesDescriptors.set(serviceName, serviceDescriptorPromise); | ||
} | ||
return serviceDescriptorPromise; | ||
@@ -514,3 +699,3 @@ } | ||
* @param {String} serviceName Service name. | ||
* @param {String} servicesDeclarations Dependencies names. | ||
* @param {String} servicesDeclarations Dependencies declarations. | ||
* @param {Boolean} injectOnly Flag indicating if existing services only should be used | ||
@@ -523,3 +708,3 @@ * @return {Promise} Service dependencies hash promise. | ||
value: function _initializeDependencies(siloContext, serviceName, servicesDeclarations) { | ||
var _this3 = this; | ||
var _this7 = this; | ||
@@ -530,6 +715,20 @@ var injectOnly = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; | ||
return Promise.resolve().then(function () { | ||
return Promise.all(servicesDeclarations.map(_pickMappedNameFromDeclaration).map(_this3._getServiceDescriptor.bind(_this3, siloContext, injectOnly))).then(function (servicesDescriptors) { | ||
return Promise.all(servicesDeclarations.map(function (serviceDeclaration) { | ||
var _parseDependencyDecla = _parseDependencyDeclaration(serviceDeclaration), | ||
mappedName = _parseDependencyDecla.mappedName, | ||
optional = _parseDependencyDecla.optional; | ||
return _this7._getServiceDescriptor(siloContext, injectOnly, mappedName).catch(function (err) { | ||
if (optional) { | ||
return Promise.resolve(); | ||
} | ||
throw err; | ||
}); | ||
})).then(function (servicesDescriptors) { | ||
debug('Initialized dependencies descriptors:', serviceName, servicesDeclarations); | ||
siloContext.servicesSequence.push(servicesDeclarations.map(_pickMappedNameFromDeclaration)); | ||
return Promise.all(servicesDescriptors.map(function (serviceDescriptor, index) { | ||
if (!serviceDescriptor) { | ||
return {}.undef; | ||
} | ||
if (!serviceDescriptor.servicePromise || !serviceDescriptor.servicePromise.then) { | ||
@@ -545,2 +744,3 @@ return Promise.reject(new _yerror2.default(E_BAD_SERVICE_PROMISE, servicesDeclarations[index])); | ||
var serviceName = _pickServiceNameFromDeclaration(servicesDeclarations[index]); | ||
hash[serviceName] = service; | ||
@@ -567,6 +767,5 @@ return hash; | ||
function _pickServiceNameFromDeclaration(serviceDeclaration) { | ||
var _serviceDeclaration$s = serviceDeclaration.split(DECLARATION_SEPARATOR), | ||
_serviceDeclaration$s2 = _slicedToArray(_serviceDeclaration$s, 1), | ||
serviceName = _serviceDeclaration$s2[0]; | ||
function _pickServiceNameFromDeclaration(dependencyDeclaration) { | ||
var _parseDependencyDecla2 = _parseDependencyDeclaration(dependencyDeclaration), | ||
serviceName = _parseDependencyDecla2.serviceName; | ||
@@ -576,7 +775,6 @@ return serviceName; | ||
function _pickMappedNameFromDeclaration(serviceDeclaration) { | ||
var _serviceDeclaration$s3 = serviceDeclaration.split(DECLARATION_SEPARATOR), | ||
_serviceDeclaration$s4 = _slicedToArray(_serviceDeclaration$s3, 2), | ||
serviceName = _serviceDeclaration$s4[0], | ||
mappedName = _serviceDeclaration$s4[1]; | ||
function _pickMappedNameFromDeclaration(dependencyDeclaration) { | ||
var _parseDependencyDecla3 = _parseDependencyDeclaration(dependencyDeclaration), | ||
serviceName = _parseDependencyDecla3.serviceName, | ||
mappedName = _parseDependencyDecla3.mappedName; | ||
@@ -586,2 +784,27 @@ return mappedName || serviceName; | ||
/* Architecture Note #1.3.1: Dependencies declaration syntax | ||
The dependencies syntax is of the following form: | ||
`?serviceName:mappedName` | ||
The `?` flag indicates an optionnal dependencies. | ||
`:mappedName` is optional and says to the container to | ||
inject `serviceName` but to rename it to `mappedName`. | ||
It allows to write generic services with fixed | ||
dependencies and remap their name at injection time. | ||
*/ | ||
function _parseDependencyDeclaration(dependencyDeclaration) { | ||
var optional = dependencyDeclaration.startsWith(OPTIONAL_FLAG); | ||
var _split = (optional ? dependencyDeclaration.slice(1) : dependencyDeclaration).split(DECLARATION_SEPARATOR), | ||
_split2 = _slicedToArray(_split, 2), | ||
serviceName = _split2[0], | ||
mappedName = _split2[1]; | ||
return { | ||
serviceName: serviceName, | ||
mappedName: mappedName || serviceName, | ||
optional: optional | ||
}; | ||
} | ||
function _applyShapes(shapes, serviceName) { | ||
@@ -610,5 +833,5 @@ return shapes.reduce(function (shapedService, shape) { | ||
function _applyStyles(classes, styles, _ref3) { | ||
var serviceName = _ref3.serviceName, | ||
dependedServiceName = _ref3.dependedServiceName; | ||
function _applyStyles(classes, styles, _ref4) { | ||
var serviceName = _ref4.serviceName, | ||
dependedServiceName = _ref4.dependedServiceName; | ||
@@ -615,0 +838,0 @@ return styles.reduce(function (classesApplications, style) { |
'use strict'; | ||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; | ||
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); | ||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; /* eslint max-nested-callbacks:0 */ | ||
var _assert = require('assert'); | ||
@@ -60,2 +62,8 @@ | ||
}); | ||
it('should fail with dependencies since it makes no sense', function () { | ||
_assert2.default.throws(function () { | ||
$.constant('time', $.depends(['hash3'], time)); | ||
}, 'E_CONSTANT_INJECTION'); | ||
}); | ||
}); | ||
@@ -73,7 +81,27 @@ | ||
it('should register provider', function () { | ||
$.service('hash', hashProvider); | ||
$.provider('hash', hashProvider); | ||
}); | ||
it('should fail with direct circular dependencies', function () { | ||
_assert2.default.throws(function () { | ||
$.provider('hash', $.depends(['hash'], hashProvider)); | ||
}, function (err) { | ||
_assert2.default.deepEqual(err.code, 'E_CIRCULAR_DEPENDENCY'); | ||
_assert2.default.deepEqual(err.params, ['hash']); | ||
return true; | ||
}); | ||
}); | ||
it('should fail with direct circular dependencies on mapped services', function () { | ||
_assert2.default.throws(function () { | ||
$.provider('hash', $.depends(['hash:lol'], hashProvider)); | ||
}, function (err) { | ||
_assert2.default.deepEqual(err.code, 'E_CIRCULAR_DEPENDENCY'); | ||
_assert2.default.deepEqual(err.params, ['hash']); | ||
return true; | ||
}); | ||
}); | ||
it('should fail with circular dependencies', function () { | ||
try { | ||
_assert2.default.throws(function () { | ||
$.provider('hash', $.depends(['hash3'], hashProvider)); | ||
@@ -83,10 +111,24 @@ $.provider('hash1', $.depends(['hash'], hashProvider)); | ||
$.provider('hash3', $.depends(['hash'], hashProvider)); | ||
} catch (err) { | ||
}, function (err) { | ||
_assert2.default.deepEqual(err.code, 'E_CIRCULAR_DEPENDENCY'); | ||
_assert2.default.deepEqual(err.params, ['hash', 'hash3']); | ||
} | ||
_assert2.default.deepEqual(err.params, ['hash3', 'hash', 'hash3']); | ||
return true; | ||
}); | ||
}); | ||
it('should fail with deeper circular dependencies', function () { | ||
_assert2.default.throws(function () { | ||
$.provider('hash', $.depends(['hash1'], hashProvider)); | ||
$.provider('hash1', $.depends(['hash2'], hashProvider)); | ||
$.provider('hash2', $.depends(['hash3'], hashProvider)); | ||
$.provider('hash3', $.depends(['hash'], hashProvider)); | ||
}, function (err) { | ||
_assert2.default.deepEqual(err.code, 'E_CIRCULAR_DEPENDENCY'); | ||
_assert2.default.deepEqual(err.params, ['hash3', 'hash', 'hash1', 'hash2', 'hash3']); | ||
return true; | ||
}); | ||
}); | ||
it('should fail with circular dependencies on mapped services', function () { | ||
try { | ||
_assert2.default.throws(function () { | ||
$.provider('aHash', $.depends(['hash3:aHash3'], hashProvider)); | ||
@@ -96,6 +138,7 @@ $.provider('aHash1', $.depends(['hash:aHash'], hashProvider)); | ||
$.provider('aHash3', $.depends(['hash:aHash'], hashProvider)); | ||
} catch (err) { | ||
}, function (err) { | ||
_assert2.default.deepEqual(err.code, 'E_CIRCULAR_DEPENDENCY'); | ||
_assert2.default.deepEqual(err.params, ['hash', 'hash3']); | ||
} | ||
_assert2.default.deepEqual(err.params, ['aHash3', 'hash:aHash', 'hash3:aHash3']); | ||
return true; | ||
}); | ||
}); | ||
@@ -120,3 +163,4 @@ }); | ||
_assert2.default.deepEqual(dependencies, {}); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -135,3 +179,4 @@ }); | ||
}); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -153,3 +198,4 @@ }); | ||
}); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -169,6 +215,40 @@ }); | ||
}); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should work with given optional dependencies', function (done) { | ||
$.constant('ENV', ENV); | ||
$.constant('DEBUG', {}); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV', '?DEBUG'], hashProvider)); | ||
$.run(['time', 'hash']).then(function (dependencies) { | ||
_assert2.default.deepEqual(Object.keys(dependencies), ['time', 'hash']); | ||
_assert2.default.deepEqual(dependencies, { | ||
hash: { ENV: ENV, DEBUG: {} }, | ||
time: time | ||
}); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should work with lacking optional dependencies', function (done) { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV', '?DEBUG'], hashProvider)); | ||
$.run(['time', 'hash']).then(function (dependencies) { | ||
_assert2.default.deepEqual(Object.keys(dependencies), ['time', 'hash']); | ||
_assert2.default.deepEqual(dependencies, { | ||
hash: { ENV: ENV, DEBUG: {}.undef }, | ||
time: time | ||
}); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should work with deeper dependencies', function (done) { | ||
@@ -186,3 +266,4 @@ $.constant('ENV', ENV); | ||
_assert2.default.deepEqual(Object.keys(dependencies), ['hash5', 'time']); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -203,3 +284,4 @@ }); | ||
_assert2.default.deepEqual(timeServiceStub.args, [[{}]]); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -220,3 +302,4 @@ }); | ||
_assert2.default.deepEqual(timeServiceStub.args, [[{}]]); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -232,3 +315,4 @@ }); | ||
_assert2.default.deepEqual(err.params, ['lol']); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -244,3 +328,4 @@ }); | ||
_assert2.default.deepEqual(err.params, ['lol']); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -258,3 +343,4 @@ }); | ||
_assert2.default.deepEqual(err.params, ['lol']); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -269,3 +355,4 @@ }); | ||
_assert2.default.deepEqual(err.params, ['lol']); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -285,3 +372,4 @@ }); | ||
_assert2.default.deepEqual(err.params, ['hash', 'hash2', 'lol']); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -335,3 +423,4 @@ }); | ||
_assert2.default.deepEqual(err.message, 'E_DB_ERROR'); | ||
done(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -355,5 +444,5 @@ db.reject(new Error('E_DB_ERROR')); | ||
_assert2.default.deepEqual(injectDependencies, {}); | ||
done(); | ||
}); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -375,5 +464,5 @@ }); | ||
}); | ||
done(); | ||
}); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -391,10 +480,141 @@ }); | ||
_assert2.default.equal(err.code, 'E_BAD_INJECTION'); | ||
done(); | ||
}); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should create dependencies when not declared as singletons', function (done) { | ||
$.constant('ENV', ENV); | ||
$.provider('hash', $.depends(['ENV'], hashProvider)); | ||
Promise.all([$.run(['hash']), $.run(['hash'])]).then(function (_ref5) { | ||
var _ref6 = _slicedToArray(_ref5, 2), | ||
hash = _ref6[0].hash, | ||
sameHash = _ref6[1].hash; | ||
_assert2.default.notEqual(hash, sameHash); | ||
return $.run(['hash']).then(function (_ref7) { | ||
var yaSameHash = _ref7.hash; | ||
_assert2.default.notEqual(hash, yaSameHash); | ||
}); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should reuse dependencies when declared as singletons', function (done) { | ||
$.constant('ENV', ENV); | ||
$.provider('hash', $.depends(['ENV'], hashProvider), { | ||
singleton: true | ||
}); | ||
Promise.all([$.run(['hash']), $.run(['hash'])]).then(function (_ref8) { | ||
var _ref9 = _slicedToArray(_ref8, 2), | ||
hash = _ref9[0].hash, | ||
sameHash = _ref9[1].hash; | ||
_assert2.default.equal(hash, sameHash); | ||
return $.run(['hash']).then(function (_ref10) { | ||
var yaSameHash = _ref10.hash; | ||
_assert2.default.equal(hash, yaSameHash); | ||
}); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
}); | ||
describe('shutdown', function () { | ||
describe('$shutdownAll', function () { | ||
it('should work even with one silo and no dependencies', function (done) { | ||
$.run(['$shutdownAll']).then(function (dependencies) { | ||
_assert2.default.equal(_typeof(dependencies.$shutdownAll), 'function'); | ||
return dependencies.$shutdownAll(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should work with several silos and dependencies', function (done) { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider), { singleton: true }); | ||
$.provider('hash1', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash2', $.depends(['ENV'], hashProvider)); | ||
Promise.all([$.run(['$shutdownAll']), $.run(['ENV', 'hash', 'hash1', 'time']), $.run(['ENV', 'hash', 'hash2'])]).then(function (_ref11) { | ||
var _ref12 = _slicedToArray(_ref11, 1), | ||
dependencies = _ref12[0]; | ||
_assert2.default.equal(_typeof(dependencies.$shutdownAll), 'function'); | ||
return dependencies.$shutdownAll(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should work when trigered from several silos simultaneously', function (done) { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash1', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash2', $.depends(['ENV'], hashProvider)); | ||
Promise.all([$.run(['$shutdownAll']), $.run(['$shutdownAll', 'ENV', 'hash', 'hash1', 'time']), $.run(['$shutdownAll', 'ENV', 'hash', 'hash2'])]).then(function (dependenciesBuckets) { | ||
return Promise.all(dependenciesBuckets.map(function (dependencies) { | ||
return dependencies.$shutdownAll(); | ||
})); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should work when a silo shutdown is in progress', function (done) { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash1', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash2', $.depends(['ENV'], hashProvider)); | ||
Promise.all([$.run(['$shutdownAll']), $.run(['$shutdown', 'ENV', 'hash', 'hash1', 'time']), $.run(['ENV', 'hash', 'hash2'])]).then(function (_ref13) { | ||
var _ref14 = _slicedToArray(_ref13, 2), | ||
dependencies1 = _ref14[0], | ||
dependencies2 = _ref14[1]; | ||
return Promise.all([dependencies2.$shutdown(), dependencies1.$shutdownAll()]); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should disallow new runs', function (done) { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash1', $.depends(['ENV'], hashProvider)); | ||
$.run(['$shutdownAll']).then(function (dependencies) { | ||
_assert2.default.equal(_typeof(dependencies.$shutdownAll), 'function'); | ||
return dependencies.$shutdownAll(); | ||
}).then(function () { | ||
_assert2.default.throws(function () { | ||
return $.run(['ENV', 'hash', 'hash1']); | ||
}, function (err) { | ||
_assert2.default.equal(err.code, 'E_INSTANCE_SHUTDOWN'); | ||
return true; | ||
}); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
}); | ||
describe('$shutdown', function () { | ||
it('should work with no dependencies', function (done) { | ||
@@ -404,3 +624,5 @@ $.run(['$shutdown']).then(function (dependencies) { | ||
dependencies.$shutdown().then(done).catch(done); | ||
return dependencies.$shutdown(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -416,3 +638,5 @@ }); | ||
dependencies.$shutdown().then(done).catch(done); | ||
return dependencies.$shutdown(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -429,3 +653,5 @@ }); | ||
dependencies.$shutdown().then(done).catch(done); | ||
return dependencies.$shutdown(); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
@@ -513,3 +739,5 @@ }); | ||
return dependencies.$shutdown(); | ||
}).then(done).catch(done); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
@@ -546,4 +774,51 @@ | ||
_assert2.default.deepEqual(servicesShutdownCalls.args, [['hash2'], ['hash1'], ['hash']]); | ||
}).then(done).catch(done); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should not shutdown singleton dependencies if used elsewhere', function (done) { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider), { | ||
singleton: true | ||
}); | ||
$.run(['time', 'hash']).then(function (dependencies) { | ||
var hash = dependencies.hash; | ||
return $.run(['time', 'hash', '$shutdown']).then(function (dependencies) { | ||
_assert2.default.equal(dependencies.hash, hash); | ||
return dependencies.$shutdown().then(function () { | ||
return $.run(['time', 'hash']).then(function (dependencies) { | ||
_assert2.default.equal(dependencies.hash, hash); | ||
}); | ||
}); | ||
}); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
it('should shutdown singleton dependencies if not used elsewhere', function (done) { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider), { | ||
singleton: true | ||
}); | ||
$.run(['time', 'hash', '$shutdown']).then(function (dependencies) { | ||
var hash = dependencies.hash; | ||
return dependencies.$shutdown().then(function () { | ||
return $.run(['time', 'hash']).then(function (dependencies) { | ||
_assert2.default.notEqual(dependencies.hash, hash); | ||
}); | ||
}); | ||
}).then(function () { | ||
return done(); | ||
}).catch(done); | ||
}); | ||
}); | ||
@@ -550,0 +825,0 @@ |
@@ -13,4 +13,14 @@ 'use strict'; | ||
var $ = _index2.default.getInstance(); // Needing several Lifecycle instances is a rare usecase so we are providing | ||
// a singleton to simplify developpers usage | ||
var $ = _index2.default.getInstance(); /* Architecture Note #1.2: One instance to rule them all | ||
We almost never need to use several Knifecycle instances. | ||
This is why we are providing the `knifecycle/instance` | ||
module that give a direct access to a lazy instanciated | ||
`Knifecycle` instance. | ||
At the same time, I prefer choosing when instantiating a | ||
singleton this is why I decided to not do it on the behalf | ||
of the developers by instead providing an opt-in interface | ||
to this singleton. | ||
*/ | ||
exports.default = $; |
{ | ||
"name": "knifecycle", | ||
"version": "1.3.1", | ||
"version": "1.4.0", | ||
"description": "Manage your NodeJS processes's lifecycle.", | ||
@@ -17,2 +17,3 @@ "main": "dist/index.js", | ||
"jsdocs", | ||
"jsarch", | ||
"readme" | ||
@@ -26,2 +27,3 @@ ] | ||
"scripts": { | ||
"architecture": "jsarch src/*.js > ARCHITECTURE.md", | ||
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", | ||
@@ -66,8 +68,9 @@ "cli": "env NODE_ENV=${NODE_ENV:-cli}", | ||
"istanbul": "^1.0.0-alpha.2", | ||
"jsarch": "1.2.1", | ||
"jsdoc-to-markdown": "^3.0.0", | ||
"metapak": "0.0.18", | ||
"metapak-nfroidure": "0.4.1", | ||
"metapak": "0.0.20", | ||
"metapak-nfroidure": "0.5.2", | ||
"mocha": "3.2.0", | ||
"mocha-lcov-reporter": "1.3.0", | ||
"sinon": "^1.16.1" | ||
"sinon": "^2.0.0" | ||
}, | ||
@@ -90,2 +93,3 @@ "dependencies": { | ||
"conventional-changelog-cli", | ||
"jsarch", | ||
"jsdoc-to-markdown" | ||
@@ -92,0 +96,0 @@ ] |
118
README.md
@@ -20,29 +20,46 @@ <!-- | ||
**The code dependencies** are fully covered by require/system modules in a | ||
testable manner (with `mockery` or `System` directly). There is no need for | ||
another dependency management system if those libraries are pure functions | ||
(involve no global states at all). | ||
**The code dependencies** are fully covered by require/system | ||
modules in a testable manner (with `mockery` or `System` | ||
directly). There is no need for another dependency management | ||
system if those libraries are pure functions (involve no | ||
global states at all). | ||
Unfortunately, applications often rely on **global states** where the JavaScript | ||
module system shows its limits. This is where `knifecycle` enters the game. | ||
Unfortunately, applications often rely on **global states** | ||
where the JavaScript module system shows its limits. This | ||
is where `knifecycle` enters the game. | ||
It is largely inspired by the Angular service system except it should not | ||
provide code but access to global states (time, filesystem, db). It also | ||
have an important additional feature to shutdown processes which is really | ||
useful for back-end servers and doesn't exists in Angular. | ||
It is largely inspired by the Angular service system except | ||
it should not provide code but access to global states | ||
(time, filesystem, db). It also have an important additional | ||
feature to shutdown processes which is really useful for | ||
back-end servers and doesn't exists in Angular. | ||
You may want to look at the | ||
[architecture notes](./ARCHITECTURE.md) to better handle the | ||
reasonning behind `knifecycle` and its implementation. | ||
At this point you may think that a DI system is useless. My | ||
advice is that it depends. But at least, you should not | ||
make a definitive choice and allow both approaches. See | ||
[this Stack Overflow anser](http://stackoverflow.com/questions/9250851/do-i-need-dependency-injection-in-nodejs-or-how-to-deal-with/44084729#44084729) | ||
for more context about this statement. | ||
## Features | ||
- services management: start services taking their dependencies in count and | ||
shut them down the same way for graceful exits (namely dependency injection | ||
with inverted control); | ||
- easy end to end testing: just replace your services per your own mocks and | ||
stubs while ensuring your application integrity between testing and production; | ||
- services management: start services taking their dependencies | ||
in count and shut them down the same way for graceful exits | ||
(namely dependency injection with inverted control); | ||
- singleton: maintain singleton services across several running | ||
execution silos. | ||
- easy end to end testing: just replace your services per your | ||
own mocks and stubs while ensuring your application integrity | ||
between testing and production; | ||
- isolation: isolate processing in a clean manner, per concerns; | ||
- functional programming ready: encapsulate global states allowing the rest of | ||
your application to be purely functional; | ||
- no circular dependencies for services: while circular dependencies are not a | ||
problem within purely functional libraries (require allows it), it may be | ||
harmful for your services, `knifecycle` impeach that while providing an `$inject` | ||
service à la Angular to allow accessing existing services references if you | ||
really need to; | ||
- functional programming ready: encapsulate global states | ||
allowing the rest of your application to be purely functional; | ||
- no circular dependencies for services: while circular | ||
dependencies are not a problem within purely functional | ||
libraries (require allows it), it may be harmful for your | ||
services, `knifecycle` impeach that while providing an | ||
`$inject` service à la Angular to allow accessing existing | ||
services references if you really need to; | ||
- generate Mermaid graphs of the dependency tree. | ||
@@ -52,4 +69,4 @@ | ||
Using Knifecycle is all about declaring the services our application needs. | ||
Some of them are simple constants: | ||
Using Knifecycle is all about declaring the services our | ||
application needs. Some of them are simple constants: | ||
```js | ||
@@ -82,5 +99,4 @@ // services/core.js | ||
While others are services that may depend on higher level ones. By example a | ||
logger. | ||
While others are services that are asynchronously built | ||
or may depends on other services. By example a logger. | ||
```js | ||
@@ -95,7 +111,11 @@ // services/logger.js | ||
service('logger', | ||
// Declare the service dependencies with the depends decorator | ||
depends(['ENV'], | ||
function logService({ ENV }) { | ||
// Declare the service dependencies with the depends | ||
// decorator. Note that the LOGGER_CONFIG dependency | ||
// is optional | ||
depends(['?LOGGER_CONFIG', 'ENV'], | ||
function logService({ LOGGER_CONFIG, ENV }) { | ||
let logger = new Logger({ | ||
logFile: ENV.LOGFILE, | ||
logFile: LOGGER_CONFIG && LOGGER_CONFIG.LOGFILE ? | ||
LOGGER_CONFIG.LOGFILE : | ||
ENV.LOGFILE : , | ||
}); | ||
@@ -197,3 +217,3 @@ | ||
let fatalErrorPromise = new Promise((resolve, reject) { | ||
db.once('error', reject); | ||
app.once('error', reject); | ||
}); | ||
@@ -248,7 +268,7 @@ | ||
// graceful shutdown was successful let's exit in peace | ||
process.exit(0); | ||
exit(0); | ||
}) | ||
.catch((err) => { | ||
console.error('Could not exit gracefully:', err); | ||
process.exit(1); | ||
exit(1); | ||
}); | ||
@@ -261,3 +281,4 @@ | ||
Simply use the DEBUG env var by setting it to 'knifecycle': | ||
Simply use the DEBUG environment variable by setting it to | ||
'knifecycle': | ||
```sh | ||
@@ -269,14 +290,8 @@ DEBUG=knifecycle npm t | ||
This library is already used by the microservices i am working on at 7Digital | ||
but I plan to use it with the | ||
[Trip Story](https://github.com/nfroidure/TripStory) toy project in order to | ||
illustrate its usage on an open-source project. I think i will also use it for | ||
front-end projects too. | ||
The scope of this library won't change. However the plan is: | ||
- improve performances | ||
- [allow to declare singleton services](https://github.com/nfroidure/knifecycle/issues/3) | ||
- evolve with Node. You will never have to transpile this library to use it with Node. | ||
- evolve with Node. I may not need to transpile this library at | ||
some point. | ||
- `depends`, `constant`, `service`, `provider` may become decorators; | ||
- track bugs ;) | ||
- track bugs ;). | ||
@@ -296,6 +311,6 @@ I'll also share most of my own services/providers and their stubs/mocks in order | ||
</dd> | ||
<dt><a href="#service">service(serviceName, service)</a> ⇒ <code>function</code></dt> | ||
<dt><a href="#service">service(serviceName, service, options)</a> ⇒ <code>function</code></dt> | ||
<dd><p>Register a service</p> | ||
</dd> | ||
<dt><a href="#provider">provider(serviceName, serviceProvider)</a> ⇒ <code>Promise</code></dt> | ||
<dt><a href="#provider">provider(serviceName, serviceProvider, options)</a> ⇒ <code>Promise</code></dt> | ||
<dd><p>Register a service provider</p> | ||
@@ -361,3 +376,3 @@ </dd> | ||
## service(serviceName, service) ⇒ <code>function</code> | ||
## service(serviceName, service, options) ⇒ <code>function</code> | ||
Register a service | ||
@@ -371,3 +386,4 @@ | ||
| serviceName | <code>String</code> | Service name | | ||
| service | <code>function</code> | <code>Promise</code> | The service promise or a function returning it | | ||
| service | <code>function</code> \| <code>Promise</code> | The service promise or a function returning it | | ||
| options | <code>Object</code> | Options passed to the provider method | | ||
@@ -401,3 +417,3 @@ **Example** | ||
## provider(serviceName, serviceProvider) ⇒ <code>Promise</code> | ||
## provider(serviceName, serviceProvider, options) ⇒ <code>Promise</code> | ||
Register a service provider | ||
@@ -412,2 +428,4 @@ | ||
| serviceProvider | <code>function</code> | Service provider or a service provider promise | | ||
| options | <code>Object</code> | Options for the provider | | ||
| options.singleton | <code>Object</code> | Define the provider as a singleton (one instance for several runs) | | ||
@@ -578,3 +596,3 @@ **Example** | ||
| serviceName | <code>String</code> | | Service name. | | ||
| servicesDeclarations | <code>String</code> | | Dependencies names. | | ||
| servicesDeclarations | <code>String</code> | | Dependencies declarations. | | ||
| injectOnly | <code>Boolean</code> | <code>false</code> | Flag indicating if existing services only should be used | | ||
@@ -581,0 +599,0 @@ |
435
src/index.js
@@ -8,3 +8,5 @@ /* eslint max-len: ["warn", { "ignoreComments": true }] */ | ||
const SHUTDOWN = '$shutdown'; | ||
const SHUTDOWN_ALL = '$shutdownAll'; | ||
const INJECT = '$inject'; | ||
const SILO_CONTEXT = '$siloContext'; | ||
const FATAL_ERROR = '$fatalError'; | ||
@@ -16,3 +18,5 @@ const E_UNMATCHED_DEPENDENCY = 'E_UNMATCHED_DEPENDENCY'; | ||
const E_BAD_INJECTION = 'E_BAD_INJECTION'; | ||
const E_CONSTANT_INJECTION = 'E_CONSTANT_INJECTION'; | ||
const DECLARATION_SEPARATOR = ':'; | ||
const OPTIONAL_FLAG = '?'; | ||
@@ -22,3 +26,29 @@ // Constants that should use Symbol whenever possible | ||
const DEPENDENCIES = '__dependencies'; | ||
const OPTIONS = '__options'; | ||
/* Architecture Note #1: Knifecycle | ||
The `knifecycle` project is intended to be a [dependency | ||
injection](https://en.wikipedia.org/wiki/Dependency_injection) | ||
and [inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control) | ||
tool. It will always be tied to this goal since I prefer | ||
composing software instead of using frameworks. | ||
It is designed to have a low footprint on services code. | ||
There is nothing worse than having to write specific code for | ||
a given tool. With `knifecycle`, services can be either constants, | ||
functions or object created synchronously or asynchronously. They | ||
can be reused elsewhere with no changes at all. | ||
*/ | ||
/* Architecture Note #1.1: OOP | ||
The `knifecycle` use case is one of the rare use case where | ||
[OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) | ||
principles are a good fit. | ||
A service provider is full of state since its concern is | ||
precisely to | ||
[encapsulate](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)) | ||
your application global states. | ||
*/ | ||
export default class Knifecycle { | ||
@@ -35,3 +65,37 @@ /** | ||
constructor() { | ||
this._silosCounter = 0; | ||
this._silosContexts = new Set(); | ||
this._servicesProviders = new Map(); | ||
this._singletonsServicesHandles = new Map(); | ||
this._singletonsServicesDescriptors = new Map(); | ||
this._singletonsServicesShutdownsPromises = new Map(); | ||
this.provider(INJECT, this.depends([SILO_CONTEXT], ({ $siloContext }) => ({ | ||
servicePromise: Promise.resolve(dependenciesDeclarations => | ||
this._initializeDependencies( | ||
$siloContext, | ||
$siloContext.name, | ||
dependenciesDeclarations, | ||
true | ||
) | ||
), | ||
}))); | ||
this.provider(SHUTDOWN_ALL, () => ({ | ||
servicePromise: Promise.resolve(() => { | ||
this.shutdownPromise = this.shutdownPromise || | ||
Promise.all( | ||
[...this._silosContexts].map( | ||
siloContext => | ||
siloContext.servicesDescriptors.get(SHUTDOWN) | ||
.servicePromise | ||
.then($shutdown => $shutdown()) | ||
) | ||
); | ||
debug('Shutting down Knifecycle instance.'); | ||
return this.shutdownPromise; | ||
}), | ||
}), { | ||
singleton: true, | ||
}); | ||
} | ||
@@ -54,2 +118,27 @@ | ||
/* Architecture Note #1.3: Declaring services | ||
The first step to use `knifecycle` is to declare | ||
services. There are three kinds of services: | ||
- constants: a constant is a simple value that will | ||
never change. It can be literal values, objects | ||
or even functions. | ||
- services: services are asynchronous functions | ||
resolving to objects, functions or complexer | ||
objects. Those one just need an initialization | ||
phase that must be done asynchronously. | ||
- providers: they are very similar to services | ||
except they have an additional layer of | ||
complexity. Indeed, they have to be hooked | ||
to the process life cycle to allow graceful | ||
shutdown of the applications build on top of | ||
`knifecycle`. | ||
In addition to this, services and providers can | ||
be declared as singletons. This means that they | ||
will be instanciated once for all for each | ||
executions silos using them (we will cover this | ||
topic later on). | ||
*/ | ||
/** | ||
@@ -71,6 +160,14 @@ * Register a constant service | ||
debug('Registered a new constant:', constantName); | ||
if( | ||
constantValue instanceof Function && | ||
constantValue[DEPENDENCIES] | ||
) { | ||
throw new YError(E_CONSTANT_INJECTION, constantValue[DEPENDENCIES]); | ||
} | ||
return this.provider(constantName, Promise.resolve.bind(Promise, { | ||
servicePromise: Promise.resolve(constantValue), | ||
shutdownProvider: Promise.resolve.bind(Promise), | ||
})); | ||
}), { singleton: true }); | ||
} | ||
@@ -82,2 +179,3 @@ | ||
* @param {Function|Promise} service The service promise or a function returning it | ||
* @param {Object} options Options passed to the provider method | ||
* @return {Function} The created service provider | ||
@@ -109,3 +207,3 @@ * @example | ||
*/ | ||
service(serviceName, service) { | ||
service(serviceName, service, options) { | ||
function serviceProvider(hash) { | ||
@@ -119,3 +217,3 @@ return { | ||
serviceProvider[DEPENDENCIES] = service[DEPENDENCIES] || []; | ||
this.provider(serviceName, serviceProvider); | ||
this.provider(serviceName, serviceProvider, options); | ||
debug('Registered a new service:', serviceName); | ||
@@ -129,2 +227,5 @@ return serviceProvider; | ||
* @param {Function} serviceProvider Service provider or a service provider promise | ||
* @param {Object} options Options for the provider | ||
* @param {Object} options.singleton Define the provider as a singleton | ||
* (one instance for several runs) | ||
* @return {Promise} The actual service descriptor promise | ||
@@ -159,21 +260,21 @@ * @example | ||
*/ | ||
provider(serviceName, serviceProvider) { | ||
provider(serviceName, serviceProvider, options = {}) { | ||
const uniqueServiceProvider = serviceProvider.bind(); | ||
uniqueServiceProvider[DEPENDENCIES] = serviceProvider[DEPENDENCIES] || []; | ||
uniqueServiceProvider[OPTIONS] = options; | ||
if( | ||
uniqueServiceProvider[DEPENDENCIES] | ||
.map(_pickServiceNameFromDeclaration) | ||
.includes(serviceName) | ||
) { | ||
throw new YError(E_CIRCULAR_DEPENDENCY, serviceName); | ||
} | ||
uniqueServiceProvider[DEPENDENCIES].forEach((dependencyDeclaration) => { | ||
const serviceName = _pickServiceNameFromDeclaration(dependencyDeclaration); | ||
const dependencyProvider = this._servicesProviders.get(serviceName); | ||
if( | ||
dependencyProvider && | ||
dependencyProvider[DEPENDENCIES] | ||
.some((childDependencyDeclaration) => { | ||
const childServiceName = _pickServiceNameFromDeclaration(childDependencyDeclaration); | ||
return childServiceName === serviceName; | ||
}) | ||
) { | ||
throw new YError(E_CIRCULAR_DEPENDENCY, dependencyDeclaration, serviceName); | ||
} | ||
this._lookupCircularDependencies( | ||
serviceName, | ||
dependencyDeclaration | ||
); | ||
}); | ||
@@ -186,2 +287,38 @@ | ||
_lookupCircularDependencies( | ||
rootServiceName, | ||
dependencyDeclaration, | ||
declarationsStacks = [] | ||
) { | ||
const serviceName = _pickMappedNameFromDeclaration( | ||
dependencyDeclaration | ||
); | ||
const dependencyProvider = this._servicesProviders.get(serviceName); | ||
if(!dependencyProvider) { | ||
return; | ||
} | ||
declarationsStacks = declarationsStacks.concat(dependencyDeclaration); | ||
dependencyProvider[DEPENDENCIES] | ||
.forEach((childDependencyDeclaration) => { | ||
const childServiceName = _pickMappedNameFromDeclaration( | ||
childDependencyDeclaration | ||
); | ||
if(rootServiceName === childServiceName) { | ||
throw new YError( | ||
...[E_CIRCULAR_DEPENDENCY, rootServiceName] | ||
.concat(declarationsStacks) | ||
.concat(childDependencyDeclaration) | ||
); | ||
} | ||
this._lookupCircularDependencies( | ||
rootServiceName, | ||
childDependencyDeclaration, | ||
declarationsStacks | ||
); | ||
}); | ||
} | ||
/** | ||
@@ -226,3 +363,6 @@ * Decorator to claim that a service depends on others ones. | ||
debug('Wrapped a service provider with dependencies:', dependenciesDeclarations); | ||
debug( | ||
'Wrapped a service provider with dependencies:', | ||
dependenciesDeclarations | ||
); | ||
@@ -259,2 +399,3 @@ return uniqueServiceProvider; | ||
const links = Array.from(servicesProviders.keys()) | ||
.filter(provider => !provider.startsWith('$')) | ||
.reduce((links, serviceName) => { | ||
@@ -284,8 +425,13 @@ const serviceProvider = servicesProviders.get(serviceName); | ||
({ serviceName, dependedServiceName }) => | ||
' ' + (_applyShapes(shapes, serviceName) || serviceName) + '-->' + | ||
(_applyShapes(shapes, dependedServiceName) || dependedServiceName) | ||
` ${ | ||
_applyShapes(shapes, serviceName) || | ||
serviceName | ||
}-->${ | ||
_applyShapes(shapes, dependedServiceName) || | ||
dependedServiceName | ||
}` | ||
) | ||
) | ||
.concat(Object.keys(classes).map( | ||
className => ' classDef ' + className + ' ' + classes[className] | ||
className => ` classDef ${className} ${classes[className]}` | ||
)) | ||
@@ -295,3 +441,3 @@ .concat( | ||
serviceName => | ||
' class ' + serviceName + ' ' + classesApplications[serviceName] + ';' | ||
` class ${serviceName} ${classesApplications[serviceName]};` | ||
) | ||
@@ -302,2 +448,14 @@ ) | ||
/* Architecture Note #1.4: Execution silos | ||
Once all the services are declared, we need a way to bring | ||
them to life. Execution silos are where the magic happen. | ||
For each call of the `run` method with given dependencies, | ||
a new silo is created and the required environment to | ||
run the actual code is leveraged. | ||
Depending of your application design, you could run it | ||
in only one execution silo or into several ones | ||
according to the isolation level your wish to reach. | ||
*/ | ||
/** | ||
@@ -320,4 +478,8 @@ * Creates a new execution silo | ||
run(dependenciesDeclarations) { | ||
const _this = this; | ||
const internalDependencies = [...new Set( | ||
dependenciesDeclarations.concat(SHUTDOWN) | ||
)]; | ||
const siloContext = { | ||
name: 'silo-' + Date.now(), | ||
name: `silo-${this._silosCounter++}`, | ||
servicesDescriptors: new Map(), | ||
@@ -329,2 +491,6 @@ servicesSequence: [], | ||
if(this.shutdownPromise) { | ||
throw new YError('E_INSTANCE_SHUTDOWN'); | ||
} | ||
// Create a provider for the special fatal error service | ||
@@ -342,10 +508,20 @@ siloContext.servicesDescriptors.set(FATAL_ERROR, { | ||
// Make the siloContext available for internal injections | ||
siloContext.servicesDescriptors.set(SILO_CONTEXT, { | ||
servicePromise: Promise.resolve(siloContext), | ||
}); | ||
// Create a provider for the shutdown special dependency | ||
siloContext.servicesDescriptors.set(SHUTDOWN, { | ||
servicePromise: Promise.resolve(() => { | ||
const shutdownPromise = _shutdownNextServices(siloContext.servicesSequence); | ||
siloContext.shutdownPromise = siloContext.shutdownPromise || | ||
_shutdownNextServices( | ||
siloContext.servicesSequence | ||
); | ||
debug('Shutting down services'); | ||
return shutdownPromise; | ||
return siloContext.shutdownPromise | ||
.then(() => { | ||
this._silosContexts.delete(siloContext); | ||
}); | ||
@@ -359,4 +535,9 @@ // Shutdown services in their instanciation order | ||
reversedServiceSequence.pop().map((serviceName) => { | ||
const serviceDescriptor = siloContext.servicesDescriptors.get(serviceName); | ||
let serviceShutdownPromise = siloContext.servicesShutdownsPromises.get(serviceName); | ||
const singletonServiceDescriptor = | ||
_this._singletonsServicesDescriptors.get(serviceName); | ||
const serviceDescriptor = singletonServiceDescriptor || | ||
siloContext.servicesDescriptors.get(serviceName); | ||
let serviceShutdownPromise = | ||
_this._singletonsServicesShutdownsPromises.get(serviceName) || | ||
siloContext.servicesShutdownsPromises.get(serviceName); | ||
@@ -367,4 +548,6 @@ if(serviceShutdownPromise) { | ||
} | ||
if(reversedServiceSequence.some( | ||
servicesDeclarations => servicesDeclarations.includes(serviceName) | ||
servicesDeclarations => | ||
servicesDeclarations.includes(serviceName) | ||
)) { | ||
@@ -374,2 +557,13 @@ debug('Delaying service shutdown:', serviceName); | ||
} | ||
if(singletonServiceDescriptor) { | ||
const handleSet = | ||
_this._singletonsServicesHandles.get(serviceName); | ||
handleSet.delete(siloContext.name); | ||
if(handleSet.size) { | ||
debug('Singleton is used elsewhere:', serviceName, handleSet); | ||
return Promise.resolve(); | ||
} | ||
_this._singletonsServicesDescriptors.delete(serviceName); | ||
} | ||
debug('Shutting down a service:', serviceName); | ||
@@ -379,3 +573,12 @@ serviceShutdownPromise = serviceDescriptor.shutdownProvider ? | ||
Promise.resolve(); | ||
siloContext.servicesShutdownsPromises.set(serviceName, serviceShutdownPromise); | ||
if(singletonServiceDescriptor) { | ||
_this._singletonsServicesShutdownsPromises.set( | ||
serviceName, | ||
serviceShutdownPromise | ||
); | ||
} | ||
siloContext.servicesShutdownsPromises.set( | ||
serviceName, | ||
serviceShutdownPromise | ||
); | ||
return serviceShutdownPromise; | ||
@@ -390,14 +593,22 @@ }) | ||
// Create a provider for the special inject service | ||
siloContext.servicesDescriptors.set(INJECT, { | ||
servicePromise: Promise.resolve(dependenciesDeclarations => | ||
this._initializeDependencies(siloContext, siloContext.name, dependenciesDeclarations, true) | ||
), | ||
}); | ||
this._silosContexts.add(siloContext); | ||
return this._initializeDependencies(siloContext, siloContext.name, dependenciesDeclarations) | ||
return this._initializeDependencies( | ||
siloContext, | ||
siloContext.name, | ||
internalDependencies | ||
) | ||
.then((servicesHash) => { | ||
debug('Handling fatal errors:', siloContext.errorsPromises); | ||
Promise.all(siloContext.errorsPromises).catch(siloContext.throwFatalError); | ||
return servicesHash; | ||
Promise.all(siloContext.errorsPromises) | ||
.catch(siloContext.throwFatalError); | ||
return dependenciesDeclarations.reduce( | ||
(finalHash, dependencyDeclaration) => { | ||
const serviceName = | ||
_pickServiceNameFromDeclaration(dependencyDeclaration); | ||
finalHash[serviceName] = servicesHash[serviceName]; | ||
return finalHash; | ||
}, {} | ||
); | ||
}); | ||
@@ -415,5 +626,14 @@ } | ||
_getServiceDescriptor(siloContext, injectOnly, serviceName) { | ||
const serviceDescriptor = siloContext.servicesDescriptors.get(serviceName); | ||
let serviceDescriptor = | ||
this._singletonsServicesDescriptors.get(serviceName); | ||
if(serviceDescriptor) { | ||
this._singletonsServicesHandles.get(serviceName) | ||
.add(siloContext.name); | ||
} else { | ||
serviceDescriptor = | ||
siloContext.servicesDescriptors.get(serviceName); | ||
} | ||
if(serviceDescriptor) { | ||
return Promise.resolve(serviceDescriptor); | ||
@@ -447,12 +667,32 @@ } | ||
debug('No service provider:', serviceName); | ||
serviceDescriptorPromise = Promise.reject(new YError(E_UNMATCHED_DEPENDENCY, serviceName)); | ||
siloContext.servicesDescriptors.set(serviceName, serviceDescriptorPromise); | ||
serviceDescriptorPromise = Promise.reject( | ||
new YError(E_UNMATCHED_DEPENDENCY, serviceName) | ||
); | ||
siloContext.servicesDescriptors.set( | ||
serviceName, | ||
serviceDescriptorPromise | ||
); | ||
return serviceDescriptorPromise; | ||
} | ||
serviceDescriptorPromise = this._initializeDependencies( | ||
// A singleton service may use a reserved resource | ||
// like a TCP socket. This is why we have to be aware | ||
// of singleton services full shutdown before creating | ||
// a new one | ||
serviceDescriptorPromise = ( | ||
this._singletonsServicesShutdownsPromises.get(serviceName) || | ||
Promise.resolve() | ||
) | ||
// Anyway delete any shutdown promise before instanciating | ||
// a new service | ||
.then(() => { | ||
this._singletonsServicesShutdownsPromises.delete(serviceName); | ||
siloContext.servicesShutdownsPromises.delete(serviceName); | ||
}) | ||
.then(this._initializeDependencies.bind( | ||
this, | ||
siloContext, | ||
serviceName, | ||
serviceProvider[DEPENDENCIES] | ||
); | ||
)); | ||
@@ -481,10 +721,22 @@ serviceDescriptorPromise = serviceDescriptorPromise | ||
if(E_UNMATCHED_DEPENDENCY === err.code) { | ||
throw YError.wrap.apply(YError, [ | ||
throw YError.wrap(...[ | ||
err, E_UNMATCHED_DEPENDENCY, serviceName, | ||
].concat(err.params) | ||
); | ||
].concat(err.params)); | ||
} | ||
throw err; | ||
}); | ||
siloContext.servicesDescriptors.set(serviceName, serviceDescriptorPromise); | ||
if(serviceProvider[OPTIONS].singleton) { | ||
const handlesSet = new Set(); | ||
handlesSet.add(siloContext.name); | ||
this._singletonsServicesHandles.set(serviceName, handlesSet); | ||
this._singletonsServicesDescriptors.set( | ||
serviceName, | ||
serviceDescriptorPromise | ||
); | ||
} else { | ||
siloContext.servicesDescriptors.set( | ||
serviceName, | ||
serviceDescriptorPromise | ||
); | ||
} | ||
return serviceDescriptorPromise; | ||
@@ -497,7 +749,9 @@ } | ||
* @param {String} serviceName Service name. | ||
* @param {String} servicesDeclarations Dependencies names. | ||
* @param {String} servicesDeclarations Dependencies declarations. | ||
* @param {Boolean} injectOnly Flag indicating if existing services only should be used | ||
* @return {Promise} Service dependencies hash promise. | ||
*/ | ||
_initializeDependencies(siloContext, serviceName, servicesDeclarations, injectOnly = false) { | ||
_initializeDependencies( | ||
siloContext, serviceName, servicesDeclarations, injectOnly = false | ||
) { | ||
debug('Initializing dependencies:', serviceName, servicesDeclarations); | ||
@@ -508,13 +762,42 @@ return Promise.resolve() | ||
servicesDeclarations | ||
.map(_pickMappedNameFromDeclaration) | ||
.map(this._getServiceDescriptor.bind(this, siloContext, injectOnly)) | ||
.map((serviceDeclaration) => { | ||
const { | ||
mappedName, | ||
optional, | ||
} = _parseDependencyDeclaration(serviceDeclaration); | ||
return this._getServiceDescriptor(siloContext, injectOnly, mappedName) | ||
.catch((err) => { | ||
if(optional) { | ||
return Promise.resolve(); | ||
} | ||
throw err; | ||
}); | ||
}) | ||
) | ||
.then((servicesDescriptors) => { | ||
debug('Initialized dependencies descriptors:', serviceName, servicesDeclarations); | ||
siloContext.servicesSequence.push(servicesDeclarations.map(_pickMappedNameFromDeclaration)); | ||
debug( | ||
'Initialized dependencies descriptors:', | ||
serviceName, | ||
servicesDeclarations | ||
); | ||
siloContext.servicesSequence.push( | ||
servicesDeclarations.map(_pickMappedNameFromDeclaration) | ||
); | ||
return Promise.all(servicesDescriptors.map( | ||
(serviceDescriptor, index) => { | ||
if((!serviceDescriptor.servicePromise) || !serviceDescriptor.servicePromise.then) { | ||
return Promise.reject(new YError(E_BAD_SERVICE_PROMISE, servicesDeclarations[index])); | ||
if(!serviceDescriptor) { | ||
return {}.undef; | ||
} | ||
if( | ||
(!serviceDescriptor.servicePromise) || | ||
!serviceDescriptor.servicePromise.then | ||
) { | ||
return Promise.reject( | ||
new YError( | ||
E_BAD_SERVICE_PROMISE, | ||
servicesDeclarations[index] | ||
) | ||
); | ||
} | ||
return serviceDescriptor.servicePromise.then(service => service); | ||
@@ -525,3 +808,6 @@ } | ||
.then(services => services.reduce((hash, service, index) => { | ||
const serviceName = _pickServiceNameFromDeclaration(servicesDeclarations[index]); | ||
const serviceName = _pickServiceNameFromDeclaration( | ||
servicesDeclarations[index] | ||
); | ||
hash[serviceName] = service; | ||
@@ -534,12 +820,41 @@ return hash; | ||
function _pickServiceNameFromDeclaration(serviceDeclaration) { | ||
const [serviceName] = serviceDeclaration.split(DECLARATION_SEPARATOR); | ||
function _pickServiceNameFromDeclaration(dependencyDeclaration) { | ||
const { serviceName } = _parseDependencyDeclaration(dependencyDeclaration); | ||
return serviceName; | ||
} | ||
function _pickMappedNameFromDeclaration(serviceDeclaration) { | ||
const [serviceName, mappedName] = serviceDeclaration.split(DECLARATION_SEPARATOR); | ||
function _pickMappedNameFromDeclaration(dependencyDeclaration) { | ||
const { | ||
serviceName, mappedName, | ||
} = _parseDependencyDeclaration(dependencyDeclaration); | ||
return mappedName || serviceName; | ||
} | ||
/* Architecture Note #1.3.1: Dependencies declaration syntax | ||
The dependencies syntax is of the following form: | ||
`?serviceName:mappedName` | ||
The `?` flag indicates an optionnal dependencies. | ||
`:mappedName` is optional and says to the container to | ||
inject `serviceName` but to rename it to `mappedName`. | ||
It allows to write generic services with fixed | ||
dependencies and remap their name at injection time. | ||
*/ | ||
function _parseDependencyDeclaration(dependencyDeclaration) { | ||
const optional = dependencyDeclaration.startsWith(OPTIONAL_FLAG); | ||
const [serviceName, mappedName] = ( | ||
optional ? | ||
dependencyDeclaration.slice(1) : | ||
dependencyDeclaration | ||
).split(DECLARATION_SEPARATOR); | ||
return { | ||
serviceName, | ||
mappedName: mappedName || serviceName, | ||
optional, | ||
}; | ||
} | ||
function _applyShapes(shapes, serviceName) { | ||
@@ -546,0 +861,0 @@ return shapes.reduce((shapedService, shape) => { |
@@ -0,1 +1,3 @@ | ||
/* eslint max-nested-callbacks:0 */ | ||
import assert from 'assert'; | ||
@@ -49,2 +51,8 @@ import sinon from 'sinon'; | ||
it('should fail with dependencies since it makes no sense', () => { | ||
assert.throws(() => { | ||
$.constant('time', $.depends(['hash3'], time)); | ||
}, 'E_CONSTANT_INJECTION'); | ||
}); | ||
}); | ||
@@ -63,7 +71,27 @@ | ||
it('should register provider', () => { | ||
$.service('hash', hashProvider); | ||
$.provider('hash', hashProvider); | ||
}); | ||
it('should fail with direct circular dependencies', () => { | ||
assert.throws(() => { | ||
$.provider('hash', $.depends(['hash'], hashProvider)); | ||
}, (err) => { | ||
assert.deepEqual(err.code, 'E_CIRCULAR_DEPENDENCY'); | ||
assert.deepEqual(err.params, ['hash']); | ||
return true; | ||
}); | ||
}); | ||
it('should fail with direct circular dependencies on mapped services', () => { | ||
assert.throws(() => { | ||
$.provider('hash', $.depends(['hash:lol'], hashProvider)); | ||
}, (err) => { | ||
assert.deepEqual(err.code, 'E_CIRCULAR_DEPENDENCY'); | ||
assert.deepEqual(err.params, ['hash']); | ||
return true; | ||
}); | ||
}); | ||
it('should fail with circular dependencies', () => { | ||
try { | ||
assert.throws(() => { | ||
$.provider('hash', $.depends(['hash3'], hashProvider)); | ||
@@ -73,10 +101,24 @@ $.provider('hash1', $.depends(['hash'], hashProvider)); | ||
$.provider('hash3', $.depends(['hash'], hashProvider)); | ||
} catch (err) { | ||
}, (err) => { | ||
assert.deepEqual(err.code, 'E_CIRCULAR_DEPENDENCY'); | ||
assert.deepEqual(err.params, ['hash', 'hash3']); | ||
} | ||
assert.deepEqual(err.params, ['hash3', 'hash', 'hash3']); | ||
return true; | ||
}); | ||
}); | ||
it('should fail with deeper circular dependencies', () => { | ||
assert.throws(() => { | ||
$.provider('hash', $.depends(['hash1'], hashProvider)); | ||
$.provider('hash1', $.depends(['hash2'], hashProvider)); | ||
$.provider('hash2', $.depends(['hash3'], hashProvider)); | ||
$.provider('hash3', $.depends(['hash'], hashProvider)); | ||
}, (err) => { | ||
assert.deepEqual(err.code, 'E_CIRCULAR_DEPENDENCY'); | ||
assert.deepEqual(err.params, ['hash3', 'hash', 'hash1', 'hash2', 'hash3']); | ||
return true; | ||
}); | ||
}); | ||
it('should fail with circular dependencies on mapped services', () => { | ||
try { | ||
assert.throws(() => { | ||
$.provider('aHash', $.depends(['hash3:aHash3'], hashProvider)); | ||
@@ -86,6 +128,7 @@ $.provider('aHash1', $.depends(['hash:aHash'], hashProvider)); | ||
$.provider('aHash3', $.depends(['hash:aHash'], hashProvider)); | ||
} catch (err) { | ||
}, (err) => { | ||
assert.deepEqual(err.code, 'E_CIRCULAR_DEPENDENCY'); | ||
assert.deepEqual(err.params, ['hash', 'hash3']); | ||
} | ||
assert.deepEqual(err.params, ['aHash3', 'hash:aHash', 'hash3:aHash3']); | ||
return true; | ||
}); | ||
}); | ||
@@ -113,4 +156,4 @@ | ||
assert.deepEqual(dependencies, {}); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -130,4 +173,4 @@ }); | ||
}); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -148,4 +191,4 @@ }); | ||
}); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -166,7 +209,42 @@ }); | ||
}); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should work with given optional dependencies', (done) => { | ||
$.constant('ENV', ENV); | ||
$.constant('DEBUG', {}); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV', '?DEBUG'], hashProvider)); | ||
$.run(['time', 'hash']) | ||
.then((dependencies) => { | ||
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']); | ||
assert.deepEqual(dependencies, { | ||
hash: { ENV, DEBUG: {} }, | ||
time, | ||
}); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should work with lacking optional dependencies', (done) => { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV', '?DEBUG'], hashProvider)); | ||
$.run(['time', 'hash']) | ||
.then((dependencies) => { | ||
assert.deepEqual(Object.keys(dependencies), ['time', 'hash']); | ||
assert.deepEqual(dependencies, { | ||
hash: { ENV, DEBUG: {}.undef }, | ||
time, | ||
}); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should work with deeper dependencies', (done) => { | ||
@@ -185,4 +263,4 @@ $.constant('ENV', ENV); | ||
assert.deepEqual(Object.keys(dependencies), ['hash5', 'time']); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -204,4 +282,4 @@ }); | ||
assert.deepEqual(timeServiceStub.args, [[{}]]); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -223,4 +301,4 @@ }); | ||
assert.deepEqual(timeServiceStub.args, [[{}]]); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -238,4 +316,4 @@ }); | ||
assert.deepEqual(err.params, ['lol']); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -253,4 +331,4 @@ }); | ||
assert.deepEqual(err.params, ['lol']); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -268,4 +346,4 @@ }); | ||
assert.deepEqual(err.params, ['lol']); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -282,4 +360,4 @@ }); | ||
assert.deepEqual(err.params, ['lol']); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -301,4 +379,4 @@ }); | ||
assert.deepEqual(err.params, ['hash', 'hash2', 'lol']); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -349,4 +427,4 @@ }); | ||
assert.deepEqual(err.message, 'E_DB_ERROR'); | ||
done(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -374,6 +452,5 @@ db.reject(new Error('E_DB_ERROR')); | ||
assert.deepEqual(injectDependencies, {}); | ||
done(); | ||
}); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -398,6 +475,5 @@ | ||
}); | ||
done(); | ||
}); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -418,13 +494,157 @@ | ||
assert.equal(err.code, 'E_BAD_INJECTION'); | ||
done(); | ||
}); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should create dependencies when not declared as singletons', (done) => { | ||
$.constant('ENV', ENV); | ||
$.provider('hash', $.depends(['ENV'], hashProvider)); | ||
Promise.all([ | ||
$.run(['hash']), | ||
$.run(['hash']), | ||
]) | ||
.then(([{ hash }, { hash: sameHash }]) => { | ||
assert.notEqual(hash, sameHash); | ||
return $.run(['hash']) | ||
.then(({ hash: yaSameHash }) => { | ||
assert.notEqual(hash, yaSameHash); | ||
}); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should reuse dependencies when declared as singletons', (done) => { | ||
$.constant('ENV', ENV); | ||
$.provider('hash', $.depends(['ENV'], hashProvider), { | ||
singleton: true, | ||
}); | ||
Promise.all([ | ||
$.run(['hash']), | ||
$.run(['hash']), | ||
]) | ||
.then(([{ hash }, { hash: sameHash }]) => { | ||
assert.equal(hash, sameHash); | ||
return $.run(['hash']) | ||
.then(({ hash: yaSameHash }) => { | ||
assert.equal(hash, yaSameHash); | ||
}); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
}); | ||
describe('shutdown', () => { | ||
describe('$shutdownAll', () => { | ||
it('should work even with one silo and no dependencies', (done) => { | ||
$.run(['$shutdownAll']) | ||
.then((dependencies) => { | ||
assert.equal(typeof dependencies.$shutdownAll, 'function'); | ||
return dependencies.$shutdownAll(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should work with several silos and dependencies', (done) => { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider), { singleton: true }); | ||
$.provider('hash1', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash2', $.depends(['ENV'], hashProvider)); | ||
Promise.all([ | ||
$.run(['$shutdownAll']), | ||
$.run(['ENV', 'hash', 'hash1', 'time']), | ||
$.run(['ENV', 'hash', 'hash2']), | ||
]) | ||
.then(([dependencies]) => { | ||
assert.equal(typeof dependencies.$shutdownAll, 'function'); | ||
return dependencies.$shutdownAll(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should work when trigered from several silos simultaneously', (done) => { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash1', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash2', $.depends(['ENV'], hashProvider)); | ||
Promise.all([ | ||
$.run(['$shutdownAll']), | ||
$.run(['$shutdownAll', 'ENV', 'hash', 'hash1', 'time']), | ||
$.run(['$shutdownAll', 'ENV', 'hash', 'hash2']), | ||
]) | ||
.then( | ||
dependenciesBuckets => | ||
Promise.all(dependenciesBuckets.map( | ||
dependencies => dependencies.$shutdownAll() | ||
)) | ||
) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should work when a silo shutdown is in progress', (done) => { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash1', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash2', $.depends(['ENV'], hashProvider)); | ||
Promise.all([ | ||
$.run(['$shutdownAll']), | ||
$.run(['$shutdown', 'ENV', 'hash', 'hash1', 'time']), | ||
$.run(['ENV', 'hash', 'hash2']), | ||
]) | ||
.then(([dependencies1, dependencies2]) => | ||
Promise.all([ | ||
dependencies2.$shutdown(), | ||
dependencies1.$shutdownAll(), | ||
]) | ||
) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should disallow new runs', (done) => { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider)); | ||
$.provider('hash1', $.depends(['ENV'], hashProvider)); | ||
$.run(['$shutdownAll']) | ||
.then((dependencies) => { | ||
assert.equal(typeof dependencies.$shutdownAll, 'function'); | ||
return dependencies.$shutdownAll(); | ||
}) | ||
.then(() => { | ||
assert.throws( | ||
() => $.run(['ENV', 'hash', 'hash1']), | ||
(err) => { | ||
assert.equal(err.code, 'E_INSTANCE_SHUTDOWN'); | ||
return true; | ||
} | ||
); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
}); | ||
describe('$shutdown', () => { | ||
it('should work with no dependencies', (done) => { | ||
@@ -435,6 +655,5 @@ $.run(['$shutdown']) | ||
dependencies.$shutdown() | ||
.then(done) | ||
.catch(done); | ||
return dependencies.$shutdown(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -451,6 +670,5 @@ }); | ||
dependencies.$shutdown() | ||
.then(done) | ||
.catch(done); | ||
return dependencies.$shutdown(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -468,6 +686,5 @@ }); | ||
dependencies.$shutdown() | ||
.then(done) | ||
.catch(done); | ||
return dependencies.$shutdown(); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -562,3 +779,3 @@ }); | ||
}) | ||
.then(done) | ||
.then(() => done()) | ||
.catch(done); | ||
@@ -599,6 +816,58 @@ }); | ||
}) | ||
.then(done) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should not shutdown singleton dependencies if used elsewhere', (done) => { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider), { | ||
singleton: true, | ||
}); | ||
$.run(['time', 'hash']) | ||
.then((dependencies) => { | ||
const { hash } = dependencies; | ||
return $.run(['time', 'hash', '$shutdown']) | ||
.then((dependencies) => { | ||
assert.equal(dependencies.hash, hash); | ||
return dependencies.$shutdown() | ||
.then( | ||
() => | ||
$.run(['time', 'hash']) | ||
.then((dependencies) => { | ||
assert.equal(dependencies.hash, hash); | ||
}) | ||
); | ||
}); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
it('should shutdown singleton dependencies if not used elsewhere', (done) => { | ||
$.constant('ENV', ENV); | ||
$.constant('time', time); | ||
$.provider('hash', $.depends(['ENV'], hashProvider), { | ||
singleton: true, | ||
}); | ||
$.run(['time', 'hash', '$shutdown']) | ||
.then((dependencies) => { | ||
const { hash } = dependencies; | ||
return dependencies.$shutdown() | ||
.then( | ||
() => | ||
$.run(['time', 'hash']) | ||
.then((dependencies) => { | ||
assert.notEqual(dependencies.hash, hash); | ||
}) | ||
); | ||
}) | ||
.then(() => done()) | ||
.catch(done); | ||
}); | ||
}); | ||
@@ -605,0 +874,0 @@ |
@@ -1,3 +0,13 @@ | ||
// Needing several Lifecycle instances is a rare usecase so we are providing | ||
// a singleton to simplify developpers usage | ||
/* Architecture Note #1.2: One instance to rule them all | ||
We almost never need to use several Knifecycle instances. | ||
This is why we are providing the `knifecycle/instance` | ||
module that give a direct access to a lazy instanciated | ||
`Knifecycle` instance. | ||
At the same time, I prefer choosing when instantiating a | ||
singleton this is why I decided to not do it on the behalf | ||
of the developers by instead providing an opt-in interface | ||
to this singleton. | ||
*/ | ||
import Knifecycle from './index'; | ||
@@ -4,0 +14,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
155539
3142
589
22