Skip to content

Latest commit

 

History

History
177 lines (126 loc) · 5.75 KB

ARCHITECTURE.md

File metadata and controls

177 lines (126 loc) · 5.75 KB

Architecture Notes

Summary

  1. Knifecycle
    1. OOP
    2. Creating initializers
      1. Dependencies declaration syntax
    3. Registering initializers
    4. Execution silos
  2. Build
  3. TypeScript tweaks

1. Knifecycle

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.

See in context

1.1. OOP

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.

See in context

1.2. Creating initializers

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.

See in context

1.2.1. Dependencies declaration syntax

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.

See in context

1.3. Registering initializers

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 optional dispose property exposing a method to properly stop the service and a fatalErrorPromise 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).

See in context

1.4. Execution silos

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.

See in context

2. Build

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

See in context

3. TypeScript tweaks

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

See in context