Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

knifecycle

Package Overview
Dependencies
Maintainers
1
Versions
101
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

knifecycle - npm Package Compare versions

Comparing version 1.3.1 to 1.4.0

20

CHANGELOG.md

@@ -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)

337

dist/index.js

@@ -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 = $;

12

package.json
{
"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 @@ ]

@@ -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> &#124; <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 @@

@@ -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 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc