knifecycle
Manage your NodeJS processes's lifecycle.
Most (maybe all) applications rely on two kinds of dependencies.
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.
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 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
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);
- 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; - generate Mermaid graphs of the dependency tree.
Usage
Using Knifecycle is all about declaring the services our
application needs. Some of them are simple constants:
import { constant } from 'knifecycle/instance';
constant('ENV', process.env);
constant('now', Date.now.bind(Date));
constant('delay', Promise.delay.bind(Promise));
constant('waitSignal', function waitSignal(signal) {
return new Promise((resolve, reject) => {
process.once(signal, resolve.bind(null, signal));
});
});
constant('exit', process.exit.bind(exit));
While others are services that are asynchronously built
or may depends on other services. By example a logger.
import { depends, service } from 'knifecycle/instance';
import Logger from 'logger';
service('logger',
depends(['?LOGGER_CONFIG', 'ENV'],
function logService({ LOGGER_CONFIG, ENV }) {
let logger = new Logger({
logFile: LOGGER_CONFIG && LOGGER_CONFIG.LOGFILE ?
LOGGER_CONFIG.LOGFILE :
ENV.LOGFILE : ,
});
logger.log('info', 'Log service initialized!');
return Promise.resolve(logger);
}
)
);
Let's add a db service too:
import { depends, provider, constant } from 'knifecycle/instance';
import MongoClient from 'mongodb';
constant('DB_CONFIG', { uri: 'mongo:xxxxx' });
provider('db',
depends(['DB_CONFIG', 'logger'],
dbProvider
)
);
function dbProvider({ DB_CONFIG, logger }) {
return MongoClient.connect(DB_CONFIG.uri)
.then(function(db) {
let fatalErrorPromise = new Promise((resolve, reject) {
db.once('error', reject);
});
logger.log('info', 'db service initialized!');
return {
servicePromise: db,
shutdownProvider: db.close.bind(db, true),
errorPromise: fatalErrorPromise,
};
});
}
constant('DB_CONFIG2', { uri: 'mongo:xxxxx' });
provider('db2',
depends(['DB_CONFIG2:DB_CONFIG', 'logger'],
dbProvider
);
Adding an Express server
import { depends, constant, provider, service } from 'knifecycle/instance';
import express from 'express';
constant('app', express());
service('routes/time',
depends('app', 'now', 'logger',
function timeRoutesProvider({ app, now, logger }) {
return Promise.resolve()
.then(() => {
app.get('/time', (req, res, next) => {
const curTime = now();
logger.log('info', 'Sending the current time:', curTime);
res.status(200).send(curTime);
});
});
})
);
provider('server',
depends(['app', 'routes/time', 'logger', 'ENV'],
function serverProvider({ app, logger, ENV }) {
return new Promise((resolve, reject) => {
app.listen(ENV.PORT, (server) => {
logger.log('info', 'server listening on port ' + ENV.PORT + '!');
resolve(server);
});
}).then(function(server) {
let fatalErrorPromise = new Promise((resolve, reject) {
app.once('error', reject);
});
function shutdownServer() {
return new Promise((resolve, reject) => {
server.close((err) => {
if(err) {
reject(err);
return;
}
resolve();
})
});
}
return {
servicePromise: Promise.resolve(server),
shutdownProvider: shutdownServer,
errorPromise: fatalErrorPromise,
};
});
})
);
Let's wire it altogether to bootstrap an express application:
import { run } from 'knifecycle/instance';
import * from './services/core';
import * from './services/log';
import * from './services/db';
import * from './services/server';
run(['server', 'waitSignal', 'exit', '$shutdown'])
function main({ waitSignal, exit, $shutdown }) {
Promise.any([
waitSignal('SIGINT'),
waitSignal('SIGTERM'),
])
.then($shutdown)
.then(() => {
exit(0);
})
.catch((err) => {
console.error('Could not exit gracefully:', err);
exit(1);
});
}
Debugging
Simply use the DEBUG environment variable by setting it to
'knifecycle':
DEBUG=knifecycle npm t
Plans
The scope of this library won't change. However the plan is:
- improve performances
- evolve with Node. I may not need to transpile this library at
some point.
depends
, constant
, service
, provider
may become decorators;- track bugs ;).
I'll also share most of my own services/providers and their stubs/mocks in order
to let you reuse it through your projects easily.
API
Functions
- getInstance() ⇒
Knifecycle
Returns a Knifecycle instance (always the same)
- constant(constantName, constantValue) ⇒
function
Register a constant service
- service(serviceName, service, options) ⇒
function
Register a service
- provider(serviceName, serviceProvider, options) ⇒
Promise
Register a service provider
- depends(dependenciesDeclarations, serviceProvider) ⇒
function
Decorator to claim that a service depends on others ones.
- toMermaidGraph(options) ⇒
String
Outputs a Mermaid compatible dependency graph of the declared services.
See Mermaid docs
- run(dependenciesDeclarations) ⇒
Promise
Creates a new execution silo
- _getServiceDescriptor(siloContext, injectOnly, serviceName, serviceProvider) ⇒
Promise
Initialize or return a service descriptor
- _initializeServiceDescriptor(siloContext, serviceName, serviceProvider) ⇒
Promise
Initialize a service
- _initializeDependencies(siloContext, serviceName, servicesDeclarations, injectOnly) ⇒
Promise
Initialize a service dependencies
getInstance() ⇒ Knifecycle
Returns a Knifecycle instance (always the same)
Kind: global function
Returns: Knifecycle
- The created/saved instance
Example
import Knifecycle from 'knifecycle'
const $ = Knifecycle.getInstance();
constant(constantName, constantValue) ⇒ function
Register a constant service
Kind: global function
Returns: function
- The created service provider
Param | Type | Description |
---|
constantName | String | The name of the service |
constantValue | any | The constant value |
Example
import Knifecycle from 'knifecycle'
const $ = new Knifecycle();
$.constant('ENV', process.env);
$.constant('time', Date.now.bind(Date));
service(serviceName, service, options) ⇒ function
Register a service
Kind: global function
Returns: function
- The created service provider
Param | Type | Description |
---|
serviceName | String | Service name |
service | function | Promise | The service promise or a function returning it |
options | Object | Options passed to the provider method |
Example
import Knifecycle from 'knifecycle'
import fs from 'fs';
const $ = new Knifecycle();
$.service('config', function config() {
return new Promise((resolve, reject) {
fs.readFile('config.js', function(err, data) {
let config;
if(err) {
return reject(err);
}
try {
config = JSON.parse(data.toString);
} catch (err) {
return reject(err);
}
resolve({
service: config,
});
});
});
provider(serviceName, serviceProvider, options) ⇒ Promise
Register a service provider
Kind: global function
Returns: Promise
- The actual service descriptor promise
Param | Type | Description |
---|
serviceName | String | Service name |
serviceProvider | function | Service provider or a service provider promise |
options | Object | Options for the provider |
options.singleton | Object | Define the provider as a singleton (one instance for several runs) |
Example
import Knifecycle from 'knifecycle'
import fs from 'fs';
const $ = new Knifecycle();
$.provider('config', function configProvider() {
return Promise.resolve({
servicePromise: new Promise((resolve, reject) {
fs.readFile('config.js', function(err, data) {
let config;
if(err) {
return reject(err);
}
try {
config = JSON.parse(data.toString);
} catch (err) {
return reject(err);
}
resolve({
service: config,
});
});
});
});
});
depends(dependenciesDeclarations, serviceProvider) ⇒ function
Decorator to claim that a service depends on others ones.
Kind: global function
Returns: function
- Returns the decorator function
Param | Type | Description |
---|
dependenciesDeclarations | Array.<String> | Dependencies the decorated service provider depends on. |
serviceProvider | function | Service provider or a service provider promise |
Example
import Knifecycle from 'knifecycle'
import fs from 'fs';
const $ = new Knifecycle();
$.service('config', $.depends(['ENV'], function configProvider({ ENV }) {
return new Promise((resolve, reject) {
fs.readFile(ENV.CONFIG_FILE, function(err, data) {
let config;
if(err) {
return reject(err);
}
try {
config = JSON.parse(data.toString);
} catch (err) {
return reject(err);
}
resolve({
service: config,
});
});
});
}));
toMermaidGraph(options) ⇒ String
Outputs a Mermaid compatible dependency graph of the declared services.
See Mermaid docs
Kind: global function
Returns: String
- Returns a string containing the Mermaid dependency graph
Param | Type | Description |
---|
options | Object | Options for generating the graph (destructured) |
options.shapes | Array.<Object> | Various shapes to apply |
options.styles | Array.<Object> | Various styles to apply |
options.classes | Object | A hash of various classes contents |
Example
import Knifecycle from 'knifecycle'
const $ = new Knifecycle();
$.constant('ENV', process.env);
$.constant('OS', require('os'));
$.service('app', $.depends(['ENV', 'OS'], () => Promise.resolve()));
$.toMermaidGraph();
graph TD
app-->ENV
app-->OS
run(dependenciesDeclarations) ⇒ Promise
Creates a new execution silo
Kind: global function
Returns: Promise
- Service descriptor promise
Param | Type | Description |
---|
dependenciesDeclarations | Array.<String> | Service name. |
Example
import Knifecycle from 'knifecycle'
const $ = new Knifecycle();
$.constant('ENV', process.env);
$.run(['ENV'])
.then(({ ENV }) => {
})
_getServiceDescriptor(siloContext, injectOnly, serviceName, serviceProvider) ⇒ Promise
Initialize or return a service descriptor
Kind: global function
Returns: Promise
- Service dependencies hash promise.
Param | Type | Description |
---|
siloContext | Object | Current execution silo context |
injectOnly | Boolean | Flag indicating if existing services only should be used |
serviceName | String | Service name. |
serviceProvider | String | Service provider. |
_initializeServiceDescriptor(siloContext, serviceName, serviceProvider) ⇒ Promise
Initialize a service
Kind: global function
Returns: Promise
- Service dependencies hash promise.
Param | Type | Description |
---|
siloContext | Object | Current execution silo context |
serviceName | String | Service name. |
serviceProvider | String | Service provider. |
_initializeDependencies(siloContext, serviceName, servicesDeclarations, injectOnly) ⇒ Promise
Initialize a service dependencies
Kind: global function
Returns: Promise
- Service dependencies hash promise.
Param | Type | Default | Description |
---|
siloContext | Object | | Current execution silo siloContext |
serviceName | String | | Service name. |
servicesDeclarations | String | | Dependencies declarations. |
injectOnly | Boolean | false | Flag indicating if existing services only should be used |
License
MIT