The knifecycle
project is intended to be a dependency
injection
with inversion of control
tool. It will always be tied to this goal since I prefer
composing software instead of using frameworks and DI/IC is
a major part to design strong software in my opinion.
It is designed to have a low footprint on services code.
There is nothing worse than having to write specific code for
a given tool. With knifecycle
, services can be either constants,
functions or objects created synchronously or asynchronously. They
can be reused elsewhere (even when not using DI) with no changes
at all since they are just simple functions with annotations
set as a property.
In fact, the Knifecycle API is aimed to allow to statically build its services load/unload code once in production.
The knifecycle
use case is one of the rare use case where
OOP
principles are a good fit.
A service provider is full of state since its concern is precisely to encapsulate your application global states.
knifecycle
uses initializers at its a core. An initializer is basically
an asynchronous function with some annotations:
- name: it uniquely identifies the initializer so that it can be referred to as another initializer dependency.
- type: an initializer can be of three types at the moment (constant, service or provider). The initializer annotations varies accordsing to those types as we'll see later on.
- injected dependencies: an array of dependencies declarations that declares which initializer htis initializer depends on. Constants logically cannot have dependencies.
- options: various options like for exemple, if the initializer implements the singleton pattern or not.
- value: only used for constant, this property allows to know the value the initializer resolves to without actually executing it.
- extra: an extra property for custom use that will be propagated by the various other decorators you'll find in this library.
Knifecycle
provides a set of decorators that allows you to simply
create new initializers.
The dependencies syntax is of the following form:
?serviceName>mappedName
The ?
flag indicates an optional dependency.
>mappedName
is optional and allows to inject
mappedName
as serviceName
.
It allows to write generic services with fixed
dependencies and remap their name at injection time.
The first step to use knifecycle
is to create a new
Knifecycle
instance and register the previously
created initializers.
Initializers can be of three types:
- constants: a
constant
initializer resolves to any constant value. - services: a
service
initializer directly resolve to the actual service it builds. It can be objects, functions or litteral values. - providers: they instead resolve to an object that
contains the service built into the
service
property but also an optionaldispose
property exposing a method to properly stop the service and afatalErrorPromise
that will be rejected if an unrecoverable error happens allowing Knifecycle to terminate.
Initializers can be declared as singletons (constants are of course only singletons). This means that they will be instanciated once for all for each executions silos using them (we will cover this topic later on).
Once every initializers are registered, we need a way to bring
them to life. Execution silos are where the magic happens.
For each call of the run
method with given dependencies,
a new silo is created and the required environment to
run the actual code is leveraged.
Depending on your application design, you could run it in only one execution silo or into several ones according to the isolation level your wish to reach.
Using Knifecycle only makes sense for monoliths. For some targets like serverless functions, a better approach is to simply build a raw initialization function.
For the build to work, we need:
- a hash of various constants that may be used.
- an autoloader that resolves dependencies names to its actual initializer
- the dependencies list you want to initialize
Sadly TypeScript does not allow to add generic types
in all cases. This is why (Service|Provider)Initializer
types do not embed the (Service|Provider)Properties
directly. Instead, we use this utility function to
reveal it to TypeScript and, by the way, check their
completeness at execution time.
For more details, see: https://stackoverflow.com/questions/64948037/generics-type-loss-while-infering/64950184#64950184