Security News
PyPI Introduces Digital Attestations to Strengthen Python Package Security
PyPI now supports digital attestations, enhancing security and trust by allowing package maintainers to verify the authenticity of Python packages.
knifecycle
Advanced tools
Manage your NodeJS processes's lifecycle automatically with an unobtrusive dependency injection implementation.
Manage your NodeJS processes's lifecycle automatically with an unobtrusive dependency injection implementation.
Most (maybe all) applications rely on two kinds of dependencies.
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.
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 StackOverflow answer for more context about this statement.
knifecycle
impeach that while providing an
$injector
service à la Angular to allow accessing existing services
references if you really need to;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:
// 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` object
async function initConfig({ ENV = { CONFIG_PATH: '.' } }) {
return new Promise((resolve, reject) => {
fs.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 to
// 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 `ENV` variable in the initializer
// so adding it in the injected dependencies. The `?`
// sign tells Knifecycle that the ENV dependency
// is optional
inject: ['?ENV'],
// our initializer is simple so we use the `service`
// 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:
const initDB = initializer(
{
name: 'db',
// Here we are injecting the previous `CONFIG` service
// 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 `service` property containing the
// actual service;
// - an optional `dispose` function allowing to
// gracefully close the service;
// - an optional `fatalErrorPromise` 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 as
// `DB_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` service
// 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(`Cannot load ${serviceName}: ${ARGS[2]}!`);
}
},
),
);
// 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` service
// implicitly created by `knifecycle`
$.run(['command', '$instance', 'exit', 'log'])
// Here, command contains the initializer eventually
// found by automatically loading a NodeJS module
// in the above `$autoload` 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:
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:
// 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:
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:
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.
// 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':
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:
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:
pg
module,jwt
module to simplify its use,Notice that those modules remains usable without using Knifecycle at all which is maybe the best feature of this library ;).
Promise.<function()>
Instantiate the initializer builder service
function
Decorator that creates an initializer for a constant value
function
Decorator that creates an initializer from a service builder
function
Decorator that creates an initializer from a service builder by automatically detecting its name and dependencies
function
Decorator that creates an initializer for a provider builder
function
Decorator that creates an initializer from a provider builder by automatically detecting its name and dependencies
function
Shortcut to create an initializer with a simple handler
function
Allows to create an initializer with a simple handler automagically
Object
Explode a dependency declaration an returns its parts.
String
Stringify a dependency declaration from its parts.
Kind: global class
Knifecycle
String
Promise
Promise
Promise
Promise
Promise
Create a new Knifecycle instance
Returns: Knifecycle
- The Knifecycle instance
Example
import Knifecycle from 'knifecycle'
const $ = new Knifecycle();
Knifecycle
Register an initializer
Kind: instance method of Knifecycle
Returns: Knifecycle
- The Knifecycle instance (for chaining)
Param | Type | Description |
---|---|---|
initializer | function | An initializer |
String
Outputs a Mermaid compatible dependency graph of the declared services. See Mermaid docs
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
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
Promise
Creates a new execution silo
Kind: instance method of Knifecycle
Returns: Promise
- Service descriptor promise
Param | Type | Description |
---|---|---|
dependenciesDeclarations | Array.<String> | Service name. |
Example
import Knifecycle, { constant } from 'knifecycle'
const $ = new Knifecycle();
$.register(constant('ENV', process.env));
$.run(['ENV'])
.then(({ ENV }) => {
// Here goes your code
})
Promise
Destroy the Knifecycle instance
Kind: instance method of Knifecycle
Returns: Promise
- Full destruction promise
Example
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
Initialize or return a service descriptor
Kind: instance method of Knifecycle
Returns: Promise
- Service descriptor promise.
Param | Type | Description |
---|---|---|
siloContext | Object | Current execution silo context |
serviceName | String | Service name. |
options | Object | Options for service retrieval |
options.injectorContext | Boolean | Flag indicating the injection were initiated by the $injector |
options.autoloading | Boolean | Flag to indicating $autoload dependencies on the fly loading |
serviceProvider | String | Service provider. |
Promise
Initialize a service descriptor
Kind: instance method of Knifecycle
Returns: Promise
- Service dependencies hash promise.
Param | Type | Description |
---|---|---|
siloContext | Object | Current execution silo context |
serviceName | String | Service name. |
options | Object | Options for service retrieval |
options.injectorContext | Boolean | Flag indicating the injection were initiated by the $injector |
options.autoloading | Boolean | Flag to indicating $autoload dependendencies on the fly loading. |
Promise
Initialize a service dependencies
Kind: instance method of Knifecycle
Returns: Promise
- Service dependencies hash promise.
Param | Type | Description |
---|---|---|
siloContext | Object | Current execution silo siloContext |
serviceName | String | Service name. |
servicesDeclarations | String | Dependencies declarations. |
options | Object | Options for service retrieval |
options.injectorContext | Boolean | Flag indicating the injection were initiated by the $injector |
options.autoloading | Boolean | Flag to indicating $autoload dependendencies on the fly loading. |
Promise.<function()>
Instantiate the initializer builder service
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
import initInitializerBuilder from 'knifecycle/dist/build';
const buildInitializer = await initInitializerBuilder({
$autoload: async () => {},
});
Promise.<String>
Create a JavaScript module that initialize a set of dependencies with hardcoded import/awaits.
Kind: inner method of initInitializerBuilder
Returns: Promise.<String>
- The JavaScript module content
Param | Type | Description |
---|---|---|
dependencies | Array.<String> | The main dependencies |
Example
import initInitializerBuilder from 'knifecycle/dist/build';
const buildInitializer = await initInitializerBuilder({
$autoload: async () => {},
});
const content = await buildInitializer(['entryPoint']);
function
Decorator that creates an initializer for a constant value
Kind: 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
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']);
printAnswer(); // 42
function
Decorator that creates an initializer from a service builder
Kind: 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 informations |
Example
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']);
printAnswer(); // 42
function
Decorator that creates an initializer from a service builder by automatically detecting its name and dependencies
Kind: global function
Returns: function
- Returns a new initializer
Param | Type | Description |
---|---|---|
serviceBuilder | function | An async function to build the service |
function
Decorator that creates an initializer for a provider builder
Kind: 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 informations |
Example
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,
});
});
});
}
function
Decorator that creates an initializer from a provider builder by automatically detecting its name and dependencies
Kind: global function
Returns: function
- Returns a new provider initializer
Param | Type | Description |
---|---|---|
providerBuilder | function | An async function to build the service provider |
function
Shortcut to create an initializer with a simple handler
Kind: global function
Returns: function
- Returns a new initializer
Param | Type | Default | Description |
---|---|---|---|
handlerFunction | function | The handler function | |
[name] | String | The name of the handler. Default to the DI prop if exists | |
[dependencies] | Array.<String> | [] | The dependencies to inject in it |
[options] | Object | Options attached to the built initializer |
Example
import Knifecycle, { handler } from 'knifecycle';
new Knifecycle()
.register(handler(getUser, 'getUser', ['db', '?log']));
const QUERY = `SELECT * FROM users WHERE id=$1`
async function getUser({ db }, userId) {
const [row] = await db.query(QUERY, userId);
return row;
}
function
Allows to create an initializer with a simple handler automagically
Kind: global function
Returns: function
- Returns a new initializer
Param | Type | Description |
---|---|---|
handlerFunction | function | The handler function |
Example
import Knifecycle, { autoHandler } from 'knifecycle';
new Knifecycle()
.register(autoHandler(getUser));
const QUERY = `SELECT * FROM users WHERE id=$1`
async function getUser({ db }, userId) {
const [row] = await db.query(QUERY, userId);
return row;
}
Object
Explode a dependency declaration an returns its parts.
Kind: global function
Returns: Object
- The various parts of it
Param | Type | Description |
---|---|---|
dependencyDeclaration | String | A dependency declaration string |
Example
parseDependencyDeclaration('pgsql>db');
// Returns
{
serviceName: 'pgsql',
mappedName: 'db',
optional: false,
}
String
Stringify a dependency declaration from its parts.
Kind: global function
Returns: String
- The various parts of it
Param | Type | Description |
---|---|---|
dependencyDeclarationParts | Object | A dependency declaration string |
Example
stringifyDependencyDeclaration({
serviceName: 'pgsql',
mappedName: 'db',
optional: false,
});
// Returns
'pgsql>db'
FAQs
Manage your NodeJS processes's lifecycle automatically with an unobtrusive dependency injection implementation.
The npm package knifecycle receives a total of 1,490 weekly downloads. As such, knifecycle popularity was classified as popular.
We found that knifecycle demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
PyPI now supports digital attestations, enhancing security and trust by allowing package maintainers to verify the authenticity of Python packages.
Security News
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.