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 show its limits. This is where knifecycle
enters the game.
It is largely inspired from the Angular service system except is should not
provide code but access to global stuffs (time, filesystem, dbs). It also
have an important additional feature to shutdown processes which is less
useful for front-end applications and doesn't exists in Angular.
Features
- services management: start services taking their dependencies in count and
shut them down the same way to gracefully exit.
- easy end to end testing: just replace your services per your own mocks and
stubs.
- isolation: isolate processing in a clean manner, per concerns.
Usage
First we create a Knifecycle instance:
import Knifecycle from 'knifecycle';
const $ = Knifecycle.getInstance();
export default $;
Then we create the services our application need. Some of them are simple
constants:
import { constant } from './knifecycle';
import Winston from 'winston';
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';
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 } from './knifecycle';
import MongoClient from 'mongodb';
provider('db',
depends(['ENV', 'logger'],
function dbProvider({ ENV, logger }) {
return MongoClient.connect(ENV.DB_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,
};
});
})
);
Adding an express server
import { depends, constant, provider, service } from './knifecycle';
import express from 'express';
constant('app', express());
service('routes/time',
depends('app', 'now', 'logger',
function timeRoutesProvider() {
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 './services/knifecycle';
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);
});
}