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.
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;
- 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 may depend on higher level ones. By example a
logger.
import { depends, service } from 'knifecycle/instance';
import Logger from 'logger';
service('logger',
depends(['ENV'],
function logService({ ENV }) {
let logger = new Logger({
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) {
db.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(() => {
process.exit(0);
})
.catch((err) => {
console.error('Could not exit gracefully:', err);
process.exit(1);
});
}
Debugging
Simply use the DEBUG env var by setting it to 'knifecycle':
DEBUG=knifecycle npm t
Plans
This library is already used by the microservices i am working on at 7Digital
but I plan to use it with the
Trip Story 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
- evolve with Node. You will never have to transpile this library to use it with Node.
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) ⇒
function
Register a service
- provider(serviceName, serviceProvider) ⇒
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) ⇒ 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 |
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) ⇒ 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 |
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 names. |
injectOnly | Boolean | false | Flag indicating if existing services only should be used |
License
MIT