Security News
GitHub Removes Malicious Pull Requests Targeting Open Source Repositories
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
knifecycle
Advanced tools
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.
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 web service. First, we need to
handle a configuration file so we are creating an
initializer to instanciate our CONFIG
service:
// services/config.js
import fs from 'fs';
import { initializer } from 'knifecycle';
// We are using the `initializer` decorator to
// declare our service initializer specificities
// Note that the initializer` decorator is pure
// so it just adds static informations and do not
// register the initializer to the provider yet.
export const initConfig = 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.
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
options: { singleton: true },
// Here is the actual initializer implementation, you
// can notice that it expect the `ENV` dependency to
// be set as a property of an object in first argument.
}, ({ ENV }) => {
return new Promise((resolve, reject) {
fs.readFile(ENV.CONFIG_PATH, function(err, data) {
if(err) {
return reject(err);
}
try {
resolve(JSON.parse(data));
} catch (err) {
reject(err);
}
}, 'utf-8');
});
Our service also uses a database so let's write an initializer for it:
// services/db.js
import { initializer } from 'knifecycle';
const initDB = initializer({
name: 'db',
// Here we are injecting the previous `CONFIG` service
// plus an optional one. If it does not exist then it
// will silently fail and the service will be undefined.
inject: ['CONFIG', '?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',,
options: { singleton: true },
}, ({ CONFIG, log }) {
return MongoClient.connect(CONFIG.DB_URI)
.then(function(db) {
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,
};
});
}
We need a last initializer for the HTTP server itself:
// services/server.js
import { initializer } from 'knifecycle';
import express from 'express';
const initDB = initializer({
name: 'server',
inject: ['ENV', 'CONFIG', '?log'],
options: { singleton: true },
}, ({ ENV, CONFIG, log }) => {
const app = express();
return new Promise((resolve, reject) => {
const port = ENV.PORT || CONFIG.PORT;
const server = app.listen(port, () => {
log && log('info', `server listening on port ${port}!`);
resolve(server);
});
}).then(function(server) {
let fatalErrorPromise = new Promise((resolve, reject) {
app.once('error', reject);
server.once('error', reject);
});
function dispose() {
return new Promise((resolve, reject) => {
server.close((err) => {
if(err) {
reject(err);
return;
}
resolve();
})
});
}
return {
service: app,
dispose,
fatalErrorPromise,
};
});
});
Great! We are ready to make it work altogether:
import { getInstance } from 'knifecycle';
import initConfig from 'services/config';
import initDB from 'services/db';
import initServer from 'services/server';
// We need only one Knifecycle instance so using
// a the singleton API
getInstance()
// Registering our initializers
.register(initConfig)
.register(initServer)
.register(initDB)
// Let's say we need to have another `db`
// service pointing to another db server.
.register(
// First we remap the injected dependencies. It will
// take the `DB2_CONFIG` service and inject it as
// `CONFIG`
inject(['DB2_CONFIG>CONFIG', '?log'],
// Then we override its name
name('db2', initDB)
)
)
// Finally, we have to create the `DB2_CONFIG` service
// on which the `db2` service now depends on
.register(name('DB2_CONFIG', inject(['CONFIG'], ({ CONFIG }) => {
// Let's just pick up the `db2` uri in the `CONFIG`
// service
return Promise.resolve({
DB_URI: CONFIG.DB2_URI,
});
})))
// Add the process environment as a simple constant
.constant('ENV', process.env)
// Add a function providing the current timestamp
.constant('now', Date.now.bind(Date))
// Add a delay function
.constant('delay', Promise.delay.bind(Promise))
// Add process lifetime utils
.constant('waitSignal', function waitSignal(signal) {
return new Promise((resolve, reject) => {
process.once(signal, resolve.bind(null, signal));
});
})
.constant('exit', process.exit.bind(exit))
// Setting a route to serve the current timestamp.
.register(name('timeRoute',
inject(
['server', 'now', '?log'],
({ server: app, now, log }) {
return Promise.resolve()
.then(() => {
app.get('/time', (req, res, next) => {
const curTime = now();
log && log('info', 'Sending the current time:', curTime);
res.status(200).send(curTime);
});
});
}
)
))
// At this point, nothing is running. To instanciate
// services, we have to create an execution silo using
// them. Note that we required the `$destroy` service
// implicitly created by `knifecycle`
.run(['server', 'timeRoute', 'waitSignal', 'exit', '$destroy'])
// Note that despite we injected them, we do not take
// back the `server` and `timeRoute` services. We only
// need them to get up and running but do not need to
// operate on them
.then(({ waitSignal, exit, $destroy }) {
// We want to exit gracefully when a SIG_TERM/INT
// signal is received
Promise.any([
waitSignal('SIGINT'),
waitSignal('SIGTERM'),
])
// The `$destroy` service will disable all silos
// progressively and then the services they rely
// on to finally resolve the returned promise
// once done
.then($destroy)
.then(() => {
// graceful shutdown was successful let's exit
// in peace
exit(0);
})
.catch((err) => {
console.error('Could not exit gracefully:', err);
exit(1);
});
})
.catch((err) => {
console.error('Could not launch the app:', err);
process.exit(1);
});
Simply use the DEBUG environment variable by setting it to 'knifecycle':
DEBUG=knifecycle npm t
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.
function
Apply special props to the given function from another one
function
Decorator creating a new initializer with some dependencies declarations appended to it.
function
Decorator to amend an initializer options.
function
Decorator to set an initializer name.
function
Decorator to set an initializer type.
function
Decorator to set an initializer properties.
Object
Explode a dependency declaration an returns its parts.
Kind: global class
Knifecycle
Knifecycle
Knifecycle
String
Promise
Promise
Promise
Promise
Create a new Knifecycle instance
Returns: Knifecycle
- The Knifecycle instance
Example
import Knifecycle from 'knifecycle'
const $ = new Knifecycle();
Knifecycle
Register a constant service
Kind: instance method of Knifecycle
Returns: Knifecycle
- The Knifecycle instance (for chaining)
Param | Type | Description |
---|---|---|
constantName | String | The name of the service |
constantValue | any | The constant value |
Example
import Knifecycle from 'knifecycle'
const $ = new Knifecycle();
// Expose the process env
$.constant('ENV', process.env);
// Expose a time() function
$.constant('time', Date.now.bind(Date));
Knifecycle
Register a service initializer
Kind: instance method of Knifecycle
Returns: Knifecycle
- The Knifecycle instance (for chaining)
Param | Type | Description |
---|---|---|
serviceName | String | Service name |
initializer | function | An initializer returning the service promise |
options | Object | Options attached to the initializer |
Example
import Knifecycle from 'knifecycle'
import fs from 'fs';
const $ = new Knifecycle();
$.service('config', configServiceInitializer, {
singleton: true,
});
function configServiceInitializer({ CONFIG_PATH }) {
return new Promise((resolve, reject) {
fs.readFile(CONFIG_PATH, function(err, data) {
if(err) {
return reject(err);
}
try {
resolve(JSON.parse(data));
} catch (err) {
reject(err);
}
}, 'utf-8');
}
Knifecycle
Register a provider initializer
Kind: instance method of Knifecycle
Returns: Knifecycle
- The Knifecycle instance (for chaining)
Param | Type | Description |
---|---|---|
serviceName | String | Service name resolved by the provider |
initializer | function | An initializer returning the service promise |
options | Object | Options attached to the initializer |
Example
import Knifecycle from 'knifecycle'
import fs from 'fs';
const $ = new Knifecycle();
$.provider('config', function configProvider() {
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,
});
});
});
});
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 } from 'knifecycle';
import appInitializer from './app';
const $ = new Knifecycle();
$.constant('ENV', process.env);
$.constant('OS', require('os'));
$.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 from 'knifecycle'
const $ = new Knifecycle();
$.constant('ENV', process.env);
$.run(['ENV'])
.then(({ ENV }) => {
// Here goes your code
})
Promise
Initialize or return a service descriptor
Kind: instance method of Knifecycle
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. |
Promise
Initialize a service
Kind: instance method of Knifecycle
Returns: Promise
- Service dependencies hash promise.
Param | Type | Description |
---|---|---|
siloContext | Object | Current execution silo context |
serviceName | String | Service name. |
serviceProvider | String | Service provider. |
Promise
Initialize a service dependencies
Kind: instance method of Knifecycle
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 |
Knifecycle
Returns a Knifecycle instance (always the same)
Kind: static method of Knifecycle
Returns: Knifecycle
- The created/saved instance
Example
import { getInstance } from 'knifecycle'
const $ = getInstance();
function
Apply special props to the given function from another one
Kind: global function
Returns: function
- The newly built function
Param | Type | Default | Description |
---|---|---|---|
from | function | The initialization function in which to pick the props | |
to | function | The initialization function from which to build the new one | |
[amend] | Object | {} | Some properties to override |
function
Decorator creating a new initializer with some dependencies declarations appended to it.
Kind: global function
Returns: function
- Returns a new initializer
Param | Type | Default | Description |
---|---|---|---|
dependenciesDeclarations | Array.<String> | List of dependencies declarations to declare which services the initializer needs to resolve its own service. | |
initializer | function | The initializer to tweak | |
[merge] | Boolean | false | Whether dependencies should be merged with existing ones or not |
Example
import { inject, getInstance } from 'knifecycle'
import myServiceInitializer from './service';
getInstance()
.service('myService',
inject(['ENV'], myServiceInitializer)
);
function
Decorator to amend an initializer options.
Kind: global function
Returns: function
- Returns a new initializer
Param | Type | Default | Description |
---|---|---|---|
options | Object | Options to set to the initializer | |
options.singleton | Object | Define the initializer service as a singleton (one instance for several runs) | |
initializer | function | The initializer to tweak | |
[merge] | function | true | Whether options should be merged or not |
Example
import { inject, options, getInstance } from 'knifecycle';
import myServiceInitializer from './service';
getInstance()
.service('myService',
inject(['ENV'],
options({ singleton: true}, myServiceInitializer)
)
);
function
Decorator to set an initializer name.
Kind: global function
Returns: function
- Returns a new initializer with that name set
Param | Type | Description |
---|---|---|
name | String | The name of the service the initializer resolves to. |
initializer | function | The initializer to tweak |
Example
import { name, getInstance } from 'knifecycle';
import myServiceInitializer from './service';
getInstance()
.register(name('myService', myServiceInitializer));
function
Decorator to set an initializer type.
Kind: global function
Returns: function
- Returns a new initializer
Param | Type | Description |
---|---|---|
type | String | The type to set to the initializer. |
initializer | function | The initializer to tweak |
Example
import { name, type, getInstance } from 'knifecycle';
import myServiceInitializer from './service';
getInstance()
.register(
type('service',
name('myService',
myServiceInitializer
)
)
);
function
Decorator to set an initializer properties.
Kind: global function
Returns: function
- Returns a new initializer
Param | Type | Description |
---|---|---|
properties | Object | Properties to set to the service. |
initializer | function | The initializer to tweak |
Example
import { initializer, getInstance } from 'knifecycle';
import myServiceInitializer from './service';
getInstance()
.register(initializer({
name: 'myService',
type: 'service',
inject: ['ENV'],
options: { singleton: true }
}, myServiceInitializer));
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,
}
2.0.0 (2017-05-28)
:
to >
. (9744aeb), closes #26index.js
exports were broken.<a name="1.4.0"></a>
FAQs
Manage your NodeJS processes's lifecycle automatically with an unobtrusive dependency injection implementation.
The npm package knifecycle receives a total of 483 weekly downloads. As such, knifecycle popularity was classified as not 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
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.
Security News
Node.js will be enforcing stricter semver-major PR policies a month before major releases to enhance stability and ensure reliable release candidates.