This package makes your Node.js app evergreen by becoming Continuous Deployment capable.
It uses a very simple API and has only few limitations for your code. The main goal of writing this package was to avoid the demanding micro-services stuff in early stages of application life cycle, while still having Continuous Deployment on from the day one.
Unlike some other similar packages, greenium
does not modify any of Node.js
original behavior.
Usage
Of course, it should be installed:
yarn add greenium # npm i -S greenium
...before it can be used in your code:
const express = require('express')
const app = express()
// Commented out: const shopAPI = require('./shop')
const verdant = require('greenium')(['./shop'])
const shopAPI = verdant.attach(app.locals)
process.on('SIGPIPE', () => verdant.reload()) // Admin pressing the "red button".
// The rest of the application code remains unchanged.
app.use('/shop/hello', shopAPI.hello)
...
In the above example, a new namespace for an e-shop API is created and every time
the app receives the SIGPIPE
, it re-loads its (hopefully fixed) source code again - w/o breaking anything 😎!
The bad news is, that instead of having just line #3, you ended up adding two extra lines 😖.
NOTE: the greenium
package does not depend on express.js or on any particular
protocol, except the
Node.js module API
.
API
NOTE: The asynchronous attach / reload will probably not supported in next releases. The real-world experience has shown that those fancy features create more problems than they can solve.
Package exports
Verdant
: the object class / constructor - useful for derived classes;verdant
: the factory function, also the default export.
Verdant class
Verdant
( [ options : object | string[] ] )
constructor.
The valid options
object keys are:
async
: boolean
- if set, forces either fully async or fully sync operation;attacher
: string
- attach hook name, def:'attach'
;detacher
: string
- detach hook name, def:'detach'
;dirPath
: string
- def:__dirpath
of the parent module;paths
: string[]
- paths for initial loadables;strict
: boolean
- enables strict error checking (true
in production mode).
If the argument is an array of strings, then it is assumed to present the paths
option.
add
(path: string) : Verdant
instance method.
Loads the module or package by the path. It resolves the path the same way as require()
does, except that it uses the dirPath
option value instead of Node.js __dirpath
.
An alternative way is to use the paths
option.
api
: Proxy<Object>
r/o instance property.
Exposes the (aggregated) API from modules loaded by this instance. This value
remains the same.
attach
([context : object]) : Proxy | Promise<Proxy>
instance method.
Attaches (initializes) all loaded loadables, not initialized yet and
returns the API proxy. Returns a promise, if at least one detach hook was asynchronous,
of if the asyncAttach
option is on.
The returned / resolved value is always the api instance property value and does not change.
Repeated calls with the same context and w/o anything new being loaded in between, have no effect. ❓
reload
([filter : string|RegExp]) : Promise<Verdant>
instance method.
Reloads all matching loadables, detaching their old instances and repeating the recently
applied attach sequence to each of them.
The optional filter argument applies to loadables paths and may be:
- a string: must be an exact match or if it contains meta symbols, then it will be turned to a RegExp instance;
- a RegExp: only the loadables with matching paths will be processed.
If there were no matches, an error will be thrown, if strict
option is on.
Repeated calls while reloading in progress, will have no effect.
reloadSync
([filter : string|RegExp]) : Verdant
instance method.
The same as above, but will throw an exception, if any of attach hooks returns a promise
or if the asyncAttach
option is on.
revoke
()
instance method.
This is analogous to Javascript Proxy.prototype.revoke()
, except that it calls
detach hooks of all loaded modules, before rendering the Verdant instance
and its exposed API inactive. Call this method before exiting the application.
Calling this method will interrupt any reloading or attaching in progress.
Factory function
verdant
( dirPath : string [, options : object] ) : Verdant
is just a wrapper around the constructor call. This is also the default export
of the package.
Concept
Here: life cycle of a loadable module and
what verdant
does.
An application may contain a set of modules (loadables), each providing its own sub-API. Often, a sub-API needs a special initialization with a proper context (e.g. services or shared namespaces), in order to become functional.
A Verdant
instance represents an aggregated API from its loadables,
sharing the same initialization context. For example, common utility functions
may need no context at all, or may depend on some command-line options;
whereas business logic might need ready-to-use services running, and lower-level
API-s ready to be used.
A Verdant
instance can fetch a loadable at any time, but it would be unwise
to do this from HTTP request handler, for example. Because of this, initial loading
of loadables, their initialization and possible further re-loading are separate operations.
Life cycle of a loadable
Attaching
If loadable exports a function, it is called with the context as its argument and its return value is used as a sub-API. Otherwise, the exported object is a sub-API. Next, if the sub-API contains a specific attach hook function, it is called with the context. The optional attach hook can be synchronous or asynchronous function with meaningless return value.
Operation
Leaving few restrictions aside, a loadable can do anything it needs to.
Detaching
On reloading a loadable, the old instance may need to be detached first. This being a case, its sub-API must have a specific detach hook function. This optional function is called w/o arguments, and it must be synchronous. Typically, this function removes event listeners or object references from the outside world, so the instance can be garbage-collected. Do not trash any resources needed for servicing a possibly pending asynchronous request, however!
After all the requests targeting this instance, are gone from Node.js event queue, the garbage collector should kick in automatically.
What greenium does
After loading a source code module, Verdant
instance exposes the module API
via its api
instance property, which is actually a proxy. This way, the rest of
application code will not see actual data references - so when the module
needs to be reloaded, its old instance can be garbage collected.
The loadable code may run asynchronous operations, though - but this will not spoil the party here. The old instance will be kept in memory, until all its asynchronous ops are finished. After this, it will be likely garbage collected. Even if this fails (for some sort of memory leak bug), the old instance will still be un-accessible from the moment the new one gets loaded.
Beyond the API proxying and require.cache
manipulations, there is not much
left for a Verdant
instance to take care of.
Restrictions
The loadable modules code must be stateless - everything, except database connections, event handlers and similar (being set up by attach hook and released by detach hook), must live in database records, Node.js event loop or HTTP request/response instances. This is how the most of the server code should be written anyway.
The loadable main module should not expose a class (constructor) via its default export. The default export may be a synchronous wrapper function only.
All parts of loadable modules exported API will be read-only and mutating any part of it would likely result in an exception thrown. For altering any options, use the wrapper- or attach hook function instead.
Names in loadable API-s should not overlap - if any part of API collides with an item from some other module, an exception will be thrown.
Compatibility
This package requires Node.js v10.15.1 or higher.
The package may fail, if Node.js require
subsystem is modified in any way.
For this particular reason, good 'ol mocha, not the magnificent jest,
was used for testing here.