[//]: # ( ) [//]: # (This file is automatically generated by the `jsarch`) [//]: # (module. Do not change it elsewhere, changes would) [//]: # (be overridden.) [//]: # ( ) # Architecture Notes ## Summary 1. [Knifecycle](#1-knifecycle) 1. [OOP](#11-oop) 2. [Creating initializers](#12-creating-initializers) 1. [Dependencies declaration syntax](#121-dependencies-declaration-syntax) 3. [Registering initializers](#13-registering-initializers) 4. [Execution silos](#14-execution-silos) 2. [Build](#2-build) 3. [TypeScript tweaks](#3-typescript-tweaks) ## 1. Knifecycle The `knifecycle` project is intended to be a [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) with [inversion of control](https://en.wikipedia.org/wiki/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](./src/index.ts#L141-L160) ### 1.1. OOP The `knifecycle` use case is one of the rare use case where [OOP](https://en.wikipedia.org/wiki/Object-oriented_programming) principles are a good fit. A service provider is full of state since its concern is precisely to [encapsulate](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)) your application global states. [See in context](./src/index.ts#L162-L171) ### 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 according to those types as we'll see later on. - injected dependencies: an array of dependencies declarations that declares which initializer this initializer depends on. Constants logically cannot have dependencies. - options: various options like for example, 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](./src/util.ts#L11-L32) #### 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](./src/util.ts#L1344-L1353) ### 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 literal 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 instantiated once for all for each executions silos using them (we will cover this topic later on). [See in context](./src/index.ts#L266-L290) ### 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](./src/index.ts#L633-L643) ## 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](./src/build.ts#L52-L67) ## 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](./src/util.ts#L1414-L1425)