Manage your NodeJS processes's lifecycle automatically with an unobtrusive dependency injection implementation.
npm install knifecycle[//]: # ( )
[//]: # (This file is automatically generated by a metapak)
[//]: # (module. Do not change it except between the)
[//]: # (content:start/end flags, your changes would)
[//]: # (be overridden.)
[//]: # ( )

[//]: # (::contents:start)
Most (maybe all) applications rely on two kinds of dependencies.
!The lifecycle of most (all?) applications
The code dependencies are fully covered by JavaScript 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.
!The app lifecycle sequence graph
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.
Last but not least, you can build your code with Knifecycle so that once in
production, it do not have to resolve the dependency tree leading to better
performances and reduce the bundle size (especially for tools like AWS Lambda /
GCP Functions where each endpoint has its own zip).
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, Knifecycle permits this, most modules made usable by Knifecycle can
in fact be used without it (this is also why static build works). See
this blog post
for more context about this statement.
- 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
$injector service à la Angular to allow accessing existing services
references if you really need to,
- generate Mermaid graphs of the dependency tree,
- auto-detect injected services names,
- build raw initialization modules to avoid embedding Knifecycle in your builds,
- optionally autoload services dependencies with custom logic.
You can find all Knifecycle comptabile modules on NPM with the
knifecycle keyword.
Using knifecycle is all about declaring the services our application needs and
running your application over it.
Let's say we are building a CLI script. Here is how we would proceed with
Knifecycle:
``js
// bin.js
import fs from 'fs';
import { YError } from 'YError';
import {
Knifecycle,
initializer,
constant,
inject,
name
} from 'knifecycle';
// First of all we create a new Knifecycle instance
const $ = new Knifecycle();
// Some of our code with rely on the process environment
// let's inject it as a constant instead of directly
// pickking env vars in process.env to make our code
// easily testable
$.register(constant('ENV', process.env));
// Let's do so for CLI args with another constant
// in real world apps we would have created a service
// that would parse args in a complexer way
$.register(constant('ARGS', process.argv));
// We want our CLI tool to rely on some configuration
// Let's build an injectable service initializer that
// reads environment variables via an injected but
// optional ENV objectapplication-services
// In a real world app, you may use the
// module services instead.
async function initConfig({ ENV = { CONFIG_PATH: '.' } }) {
await fs.promises.readFile(
ENV.CONFIG_PATH,
'utf-8',
(err, data) => {
if (err) {
reject(err);
return;
}
try {
resolve(JSON.parse(data));
} catch (err) {
reject(err);
}
},
);
}
// We are using the initializer decorator toENV
// declare our service initializer specificities
// and register it with our Knifecycle instance
$.register(
initializer(
{
// we have to give our final service a name
// for further use in other services injections
name: 'CONFIG',
// we will need an variable in the initializer?
// so adding it in the injected dependencies. The service
// sign tells Knifecycle that the ENV dependency
// is optional
inject: ['?ENV'],
// our initializer is simple so we use the
// type for the initializer which just indicate that
// the initializer will return a promise of the actual
// service
type: 'service',
// We don't want to read the config file everytime we
// inject it so declaring it as a singleton
singleton: true,
},
initConfig,
),
);
// Our CLI also uses a database so let's write an
// initializer for it (in a real world app you
// can use postgresql-service instead):CONFIG
const initDB = initializer(
{
name: 'db',
// Here we are injecting the previous serviceservice
// as required so that our DB cannot be connected without
// having a proper config.
inject: ['CONFIG', 'DB_URI', '?log'],
// The initializer type is slightly different. Indeed,
// we need to manage the database connection errors
// and wait for it to flush before shutting down the
// process.
// A service provider returns a promise of a provider
// descriptor exposing:
// - a mandatory property containing thedispose
// actual service;
// - an optional function allowing tofatalErrorPromise
// gracefully close the service;
// - an optional property to
// handle the service unrecoverable failure.
type: 'provider',
singleton: true,
},
async ({ CONFIG, DB_URI, log }) => {
const db = await MongoClient.connect(DB_URI, CONFIG.databaseOptions);
let fatalErrorPromise = new Promise((resolve, reject) => {
db.once('error', reject);
});
// Logging only if the log service is defined
log && log('info', 'db service initialized!');
return {
service: db,
dispose: db.close.bind(db, true),
fatalErrorPromise,
};
},
);
// Here we are registering our initializer apart to
// be able to reuse it, we also declare the required
// DB_URI constant it needs
$.register(constant('DB_URI', 'posgresql://xxxx'));
$.register(initDB);
// Say we need to use two different DB server
// We can reuse our initializer by tweaking
// some of its properties
$.register(constant('DB_URI2', 'posgresql://yyyy'));
$.register(
// First we remap the injected dependencies. It will
// take the DB_URI2 constant and inject it asDB_URI
//
inject(
['CONFIG', 'DB_URI2>DB_URI', '?log'],
// Then we override its name to make it
// available as a different service
name('db2', initDB),
),
);
// A lot of NodeJS functions have some side effects
// declaring them as constants allows you to easily
// mock/monitor/patch it. The common-services NPM
// module contains a few useful ones
$.register(constant('now', Date.now.bind(Date)))
.register(constant('log', console.log.bind(console)))
.register(constant('exit', process.exit.bind(process)));
// Finally, let's declare an $autoload serviceCannot load ${serviceName}: ${ARGS[2]}!
// to allow us to load only the initializers needed
// to run the given commands
$.register(
initializer(
{
name: '$autoload',
type: 'service',
inject: ['CONFIG', 'ARGS'],
// Note that the auto loader must be a singleton
singleton: true,
},
async ({ CONFIG, ARGS }) =>
async (serviceName) => {
if ('command' !== serviceName) {
// Allows to signal that the dependency is not found
// so that optional dependencies doesn't impeach the
// injector to resolve the dependency tree
throw new YError('E_UNMATCHED_DEPENDENCY', serviceName);
}
try {
const path = CONFIG.commands + '/' + ARGS[2];
return {
path,
initializer: require(path).default,
};
} catch (err) {
throw new Error();
}
},
),
);
// At this point, nothing is running. To instanciate the
// services, we have to create an execution silo using
// them. Note that we required the $instance serviceknifecycle
// implicitly created by $autoload
$.run(['command', '$instance', 'exit', 'log'])
// Here, command contains the initializer eventually
// found by automatically loading a NodeJS module
// in the above service. The db connection
// will only be instanciated if that command needs it
.then(async ({ command, $instance, exit, log }) => {
try {
command();
log('It worked!');
} catch (err) {
log('It failed!', err);
} finally {
// Here we ensure every db connections are closed
// properly. We could have use $.destroy() the same`
// way but this is to illustrate that the Knifecycle
// instance can be injected in services contexts
// (rarely done but good to know it exists)
await $instance.destroy().catch((err) => {
console.error('Could not exit gracefully:', err);
exit(1);
});
}
})
.catch((err) => {
console.error('Could not launch the app:', err);
process.exit(1);
});
Running the following should make the magic happen:
`sh`
cat "{ commands: './commands'}" > config.json
DEBUG=knifecycle CONFIG_PATH=./config.json node -r @babel/register bin.js mycommand test
// Prints: Could not launch the app: Error: Cannot load command: mycommand!
// (...stack trace)
Or at least, we still have to create commands, let's create the mycommand one:
`js
// commands/mycommand.js
import { initializer } from './dist';
// A simple command that prints the given args
export default initializer(
{
name: 'command',
type: 'service',
// Here we could have injected whatever we declared
// in the previous file: db, now, exit...
inject: ['ARGS', 'log'],
},
async ({ ARGS, log }) => {
return () => log('Command args:', ARGS.slice(2));
},
);
`
So now, it works:
`sh`
DEBUG=knifecycle CONFIG_PATH=./config.json node -r @babel/register bin.js mycommand test
// Prints: Command args: [ 'mycommand', 'test' ]
// It worked!
This is a very simple example but you can find a complexer CLI usage with
(metapak)[https://github.com/nfroidure/metapak/blob/master/bin/metapak.js].
Knifecycle also provide some utility function to automatically assign the
initializer property declarations, the following 3 ways to declare the getUser
service are equivalent:
`js
import noop from 'noop';
import { autoInject, inject, initializer, autoService } from 'knifecycle';
initializer({
name: 'getUser',
inject: ['db', '?log'],
type: 'service',
}, getUser);
service('getUser', autoInject(getUser)));
autoService(getUser);
async function getUser({ db, log = noop}) {}
`
That said, if you need to build your code with webpack/babel you may have to
convert auto-detections to raw declarations with the
babel-plugin-knifecycle
plugin. You can also do this only for the performance improvements it brings.
Also, keep in mind that the auto-detection is based on a simple regular
expression so you should care to keep initializer signatures simple to avoid
having a E_AUTO_INJECTION_FAILURE error. As a rule of thumb, avoid setting
complex default values.
`js
// Won't work
autoInject(async ({ log = () => {} }) => {});
// Will work
function noop() {}
autoInject(async ({ log = noop }) => {});
`
Simply use the DEBUG environment variable by setting it to 'knifecycle':
`sh`
DEBUG=knifecycle npm t
The output is very verbose but lead to a deep understanding of mechanisms that
take place under the hood.
The scope of this library won't change. However the plan is:
- improve performances;
- track bugs ;).
I'll also share most of my own initializers and their stubs/mocks in order to
let you reuse it through your projects easily. Here are the current projects
that use this DI lib:
- common-services: contains the
services I use the most in my apps,
- memory-kv-store: a simple in
memory key-value store,
- whook: a framework to build REST web
services.
- postgresql-service: a
simple wrapper around the pg module,jwt
- jwt-service: a simple wrapper
around the module to simplify its use,
- ftp-service: a FTP client with
clean defaults.
Notice that those modules remains usable without using Knifecycle at all which
is maybe the best feature of this library 😉.
[//]: # (::contents:end)
Promise.<function()>Instantiate the initializer builder service
Allow to dispose the services of an
initialized silo content.
functionDecorator that creates an initializer for a constant value
functionDecorator that creates an initializer from a service builder
functionDecorator that creates an initializer from a service
builder by automatically detecting its name
and dependencies
functionDecorator that creates an initializer for a provider
builder
functionDecorator that creates an initializer from a provider
builder by automatically detecting its name
and dependencies
ObjectExplode a dependency declaration an returns its parts.
StringStringify a dependency declaration from its parts.
* Knifecycle
* new Knifecycle(options)
* .register(initializer) ⇒ Knifecycle
* .toMermaidGraph(options) ⇒ String
* .run(dependenciesDeclarations) ⇒ Promise
* .destroy() ⇒ Promise
Returns: Knifecycle - The Knifecycle instance
| Param | Type | Description |
| --- | --- | --- |
| options | Object | An object with options |
| options.sequential | boolean | Allows to load dependencies sequentially (usefull for debugging) |
Example
`js
import Knifecycle from 'knifecycle'
Kind: instance method of Knifecycle
Returns: Knifecycle - The Knifecycle instance (for chaining)
| Param | Type | Description |
| --- | --- | --- |
| initializer | function | An initializer |
Kind: instance method of Knifecycle
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
`js
import Knifecycle, { inject, constant, service } from 'knifecycle';
import appInitializer from './app';
const $ = new Knifecycle();
$.register(constant('ENV', process.env));
$.register(constant('OS', require('os')));
$.register(service('app', inject(['ENV', 'OS'], appInitializer)));
$.toMermaidGraph();
// returns
graph TD
app-->ENV
app-->OS
`
Kind: instance method of Knifecycle
Returns: Promise - Service descriptor promise
| Param | Type | Description |
| --- | --- | --- |
| dependenciesDeclarations | Array.<String> | Service name. |
Example
`js
import Knifecycle, { constant } from 'knifecycle'
const $ = new Knifecycle();
$.register(constant('ENV', process.env));
$.run(['ENV'])
.then(({ ENV }) => {
// Here goes your code
})
`
Kind: instance method of Knifecycle
Returns: Promise - Full destruction promise
Example
`js
import Knifecycle, { constant } from 'knifecycle'
const $ = new Knifecycle();
$.register(constant('ENV', process.env));
$.run(['ENV'])
.then(({ ENV }) => {
// Here goes your code
// Finally destroy the instance
$.destroy()
})
`
Promise.<function()>Kind: global function
Returns: Promise.<function()> - A promise of the buildInitializer function
| Param | Type | Description |
| --- | --- | --- |
| services | Object | The services to inject |
| services.$autoload | Object | The dependencies autoloader |
Example
`js
import initInitializerBuilder from 'knifecycle/dist/build';
const buildInitializer = await initInitializerBuilder({
$autoload: async () => {},
});
`
Kind: inner method of initInitializerBuilder
Returns: Promise.<String> - The JavaScript module content
| Param | Type | Description |
| --- | --- | --- |
| dependencies | Array.<String> | The main dependencies |
Example
`js
import initInitializerBuilder from 'knifecycle/dist/build';
const buildInitializer = await initInitializerBuilder({
$autoload: async () => {},
});
const content = await buildInitializer(['entryPoint']);
`
functionKind: global function
Returns: function - Returns a new constant initializer
| Param | Type | Description |
| --- | --- | --- |
| name | String | The constant's name. |
| value | any | The constant's value |
Example
`js
import Knifecycle, { constant, service } from 'knifecycle';
const { printAnswer } = new Knifecycle()
.register(constant('THE_NUMBER', value))
.register(constant('log', console.log.bind(console)))
.register(service(
async ({ THE_NUMBER, log }) => () => log(THE_NUMBER),
'printAnswer',
['THE_NUMBER', 'log'],
))
.run(['printAnswer']);
functionKind: global function
Returns: function - Returns a new initializer
| Param | Type | Description |
| --- | --- | --- |
| serviceBuilder | function | An async function to build the service |
| [name] | String | The service's name |
| [dependencies] | Array.<String> | The service's injected dependencies |
| [singleton] | Boolean | Whether the service is a singleton or not |
| [extra] | any | Eventual extra information |
Example
`js
import Knifecycle, { constant, service } from 'knifecycle';
const { printAnswer } = new Knifecycle()
.register(constant('THE_NUMBER', value))
.register(constant('log', console.log.bind(console)))
.register(service(
async ({ THE_NUMBER, log }) => () => log(THE_NUMBER),
'printAnswer',
['THE_NUMBER', 'log'],
true
))
.run(['printAnswer']);
functionKind: global function
Returns: function - Returns a new initializer
| Param | Type | Description |
| --- | --- | --- |
| serviceBuilder | function | An async function to build the service |
functionKind: global function
Returns: function - Returns a new provider initializer
| Param | Type | Description |
| --- | --- | --- |
| providerBuilder | function | An async function to build the service provider |
| [name] | String | The service's name |
| [dependencies] | Array.<String> | The service's dependencies |
| [singleton] | Boolean | Whether the service is a singleton or not |
| [extra] | any | Eventual extra information |
Example
`js
import Knifecycle, { provider } from 'knifecycle'
import fs from 'fs';
const $ = new Knifecycle();
$.register(provider(configProvider, 'config'));
async function configProvider() {
return new Promise((resolve, reject) {
fs.readFile('config.js', function(err, data) {
let config;
if(err) {
reject(err);
return;
}
try {
config = JSON.parse(data.toString);
} catch (err) {
reject(err);
return;
}
resolve({
service: config,
});
});
});
}
`
functionKind: global function
Returns: function - Returns a new provider initializer
| Param | Type | Description |
| --- | --- | --- |
| providerBuilder | function | An async function to build the service provider |
ObjectKind: global function
Returns: Object - The various parts of it
| Param | Type | Description |
| --- | --- | --- |
| dependencyDeclaration | String | A dependency declaration string |
Example
`js`
parseDependencyDeclaration('pgsql>db');
// Returns
{
serviceName: 'pgsql',
mappedName: 'db',
optional: false,
}
StringKind: global function
Returns: String - The various parts of it
| Param | Type | Description |
| --- | --- | --- |
| dependencyDeclarationParts | Object | A dependency declaration string |
Example
`js
stringifyDependencyDeclaration({
serviceName: 'pgsql',
mappedName: 'db',
optional: false,
});
// Returns
'pgsql>db'
``