From 020dc1382e591a401b3134af4165d3247c036802 Mon Sep 17 00:00:00 2001 From: Nicolas Froidure Date: Fri, 18 Aug 2023 16:17:32 +0200 Subject: [PATCH] refactor(build): add special services to built Allows to handle fatal errors and gracefully shutdown in built code. fix #128 --- ARCHITECTURE.md | 16 +- README.md | 425 +++++++++++++++++++++++++++------------------- package.json | 3 +- src/build.test.ts | 110 ++++++++++-- src/build.ts | 89 +++++++--- src/dispose.ts | 148 ++++++++++++++++ src/fatalError.ts | 56 ++++++ src/index.test.ts | 2 +- src/index.ts | 252 ++++++--------------------- src/util.ts | 2 + 10 files changed, 674 insertions(+), 429 deletions(-) create mode 100644 src/dispose.ts create mode 100644 src/fatalError.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 79b7c35..cdbda08 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -37,7 +37,7 @@ It is designed to have a low footprint on services code. 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#L195-L214) +[See in context](./src/index.ts#L201-L220) @@ -52,7 +52,7 @@ A service provider is full of state since its concern is [encapsulate](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)) your application global states. -[See in context](./src/index.ts#L216-L225) +[See in context](./src/index.ts#L222-L231) @@ -78,7 +78,7 @@ A service provider is full of state since its concern is `Knifecycle` provides a set of decorators that allows you to simply create new initializers. -[See in context](./src/util.ts#L9-L30) +[See in context](./src/util.ts#L11-L32) @@ -92,7 +92,7 @@ The `?` flag indicates an optional dependency. It allows to write generic services with fixed dependencies and remap their name at injection time. -[See in context](./src/util.ts#L1302-L1311) +[See in context](./src/util.ts#L1304-L1313) @@ -121,7 +121,7 @@ Initializers can be of three types: instanciated once for all for each executions silos using them (we will cover this topic later on). -[See in context](./src/index.ts#L310-L334) +[See in context](./src/index.ts#L311-L335) @@ -137,7 +137,7 @@ 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#L639-L649) +[See in context](./src/index.ts#L644-L654) @@ -157,7 +157,7 @@ For the build to work, we need: - the dependencies list you want to initialize -[See in context](./src/build.ts#L28-L43) +[See in context](./src/build.ts#L32-L47) @@ -173,5 +173,5 @@ Sadly TypeScript does not allow to add generic types For more details, see: https://stackoverflow.com/questions/64948037/generics-type-loss-while-infering/64950184#64950184 -[See in context](./src/util.ts#L1372-L1383) +[See in context](./src/util.ts#L1374-L1385) diff --git a/README.md b/README.md index 4055dd0..f00a688 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ -[//]: # ( ) -[//]: # (This file is automatically generated by a `metapak`) -[//]: # (module. Do not change it except between the) -[//]: # (`content:start/end` flags, your changes would) -[//]: # (be overridden.) -[//]: # ( ) +[//]: # ' ' +[//]: # 'This file is automatically generated by a `metapak`' +[//]: # 'module. Do not change it except between the' +[//]: # '`content:start/end` flags, your changes would' +[//]: # 'be overridden.' +[//]: # ' ' + # knifecycle -> Manage your NodeJS processes's lifecycle automatically with an unobtrusive dependency injection implementation. + +> Manage your NodeJS processes's lifecycle automatically with an unobtrusive +> dependency injection implementation. [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/nfroidure/knifecycle/blob/main/LICENSE) [![Coverage Status](https://coveralls.io/repos/github/nfroidure/knifecycle/badge.svg?branch=main)](https://coveralls.io/github/nfroidure/knifecycle?branch=main) +[//]: # '::contents:start' -[//]: # (::contents:start) - -[![Browser Support Matrix](https://saucelabs.com/open_sauce/build_matrix/nfroidure.svg)](https://saucelabs.com/u/nfroidure) Most (maybe all) applications rely on two kinds of dependencies. @@ -25,41 +26,53 @@ 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. +![The app lifecycle sequence graph](https://insertafter.com/images/dependencies-graph-sequences.svg) + 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. +Last but not least, you can build your code with Knifecycle so that once in +production, it do not have to resolve the dependency tree leading to better +performances and reduce the bundle size (especially for tools like AWS Lambda / +GCP Functions where each endpoint has its own zip). + You may want to look at the [architecture notes](./ARCHITECTURE.md) 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 StackOverflow answer](http://stackoverflow.com/questions/9250851/do-i-need-dependency-injection-in-nodejs-or-how-to-deal-with/44084729#44084729) +approaches, Knifecycle permits this, most modules made usable by Knifecycle can +in fact be used without it (this is also why static build works). See +[this blog post](https://insertafter.com/en/blog/unobstrusive_dependency_injection_with_knifecycle.html) for more context about this statement. ## Features - services management: start services taking their dependencies in count and shut them down the same way for graceful exits (namely dependency injection - with inverted control); -- singleton: maintain singleton services across several running execution silos. + with inverted control), +- singleton: maintain singleton services across several running execution silos, - easy end to end testing: just replace your services per your own mocks and stubs while ensuring your application integrity between testing and - production; + production, - isolation: isolate processing in a clean manner, per concerns; - functional programming ready: encapsulate global states allowing the rest of - your application to be purely functional; + your application to be purely functional, - no circular dependencies for services: while circular dependencies are not a problem within purely functional libraries (require allows it), it may be harmful for your services, `knifecycle` impeach that while providing an `$injector` service à la Angular to allow accessing existing services - references if you really need to; -- generate Mermaid graphs of the dependency tree; -- build raw initialization modules to avoid embedding Knifecycle in your builds; + references if you really need to, +- generate Mermaid graphs of the dependency tree, +- auto-detect injected services names, +- build raw initialization modules to avoid embedding Knifecycle in your builds, - optionally autoload services dependencies with custom logic. +You can find all Knifecycle comptabile modules on NPM with the +[knifecycle keyword](https://www.npmjs.com/search?q=keywords:knifecycle). + ## Usage Using `knifecycle` is all about declaring the services our application needs and @@ -72,7 +85,13 @@ Knifecycle: // bin.js import fs from 'fs'; import { YError } from 'YError'; -import Knifecycle, { initializer, constant, inject, name } from 'knifecycle'; +import { + Knifecycle, + initializer, + constant, + inject, + name +} from 'knifecycle'; // First of all we create a new Knifecycle instance const $ = new Knifecycle(); @@ -92,9 +111,13 @@ $.register(constant('ARGS', process.argv)); // Let's build an injectable service initializer that // reads environment variables via an injected but // optional `ENV` object +// In a real world app, you may use the +// `application-services` module services instead. async function initConfig({ ENV = { CONFIG_PATH: '.' } }) { - return new Promise((resolve, reject) => { - fs.readFile(ENV.CONFIG_PATH, 'utf-8', (err, data) => { + await fs.promises.readFile( + ENV.CONFIG_PATH, + 'utf-8', + (err, data) => { if (err) { reject(err); return; @@ -104,8 +127,8 @@ async function initConfig({ ENV = { CONFIG_PATH: '.' } }) { } catch (err) { reject(err); } - }); - }); + }, + ); } // We are using the `initializer` decorator to @@ -136,7 +159,8 @@ $.register( ); // Our CLI also uses a database so let's write an -// initializer for it: +// initializer for it (in a real world app you +// can use `postgresql-service` instead): const initDB = initializer( { name: 'db', @@ -218,23 +242,24 @@ $.register( // Note that the auto loader must be a singleton singleton: true, }, - async ({ CONFIG, ARGS }) => async (serviceName) => { - if ('command' !== serviceName) { - // Allows to signal that the dependency is not found - // so that optional dependencies doesn't impeach the - // injector to resolve the dependency tree - throw new YError('E_UNMATCHED_DEPENDENCY', serviceName); - } - try { - const path = CONFIG.commands + '/' + ARGS[2]; - return { - path, - initializer: require(path).default, - }; - } catch (err) { - throw new Error(`Cannot load ${serviceName}: ${ARGS[2]}!`); - } - }, + async ({ CONFIG, ARGS }) => + async (serviceName) => { + if ('command' !== serviceName) { + // Allows to signal that the dependency is not found + // so that optional dependencies doesn't impeach the + // injector to resolve the dependency tree + throw new YError('E_UNMATCHED_DEPENDENCY', serviceName); + } + try { + const path = CONFIG.commands + '/' + ARGS[2]; + return { + path, + initializer: require(path).default, + }; + } catch (err) { + throw new Error(`Cannot load ${serviceName}: ${ARGS[2]}!`); + } + }, ), ); @@ -391,11 +416,12 @@ that use this DI lib: clean defaults. Notice that those modules remains usable without using Knifecycle at all which -is maybe the best feature of this library ;). +is maybe the best feature of this library 😉. -[//]: # (::contents:end) +[//]: # '::contents:end' # API + ## Classes
@@ -446,62 +472,72 @@ is maybe the best feature of this library ;). ## Knifecycle -**Kind**: global class -* [Knifecycle](#Knifecycle) - * [new Knifecycle(options)](#new_Knifecycle_new) - * [.register(initializer)](#Knifecycle+register) ⇒ [Knifecycle](#Knifecycle) - * [.toMermaidGraph(options)](#Knifecycle+toMermaidGraph) ⇒ String - * [.run(dependenciesDeclarations)](#Knifecycle+run) ⇒ Promise - * [.destroy()](#Knifecycle+destroy) ⇒ Promise +**Kind**: global class + +- [Knifecycle](#Knifecycle) + - [new Knifecycle(options)](#new_Knifecycle_new) + - [.register(initializer)](#Knifecycle+register) ⇒ + [Knifecycle](#Knifecycle) + - [.toMermaidGraph(options)](#Knifecycle+toMermaidGraph) ⇒ String + - [.run(dependenciesDeclarations)](#Knifecycle+run) ⇒ Promise + - [.destroy()](#Knifecycle+destroy) ⇒ Promise ### new Knifecycle(options) + Create a new Knifecycle instance -**Returns**: [Knifecycle](#Knifecycle) - The Knifecycle instance +**Returns**: [Knifecycle](#Knifecycle) - The Knifecycle instance -| Param | Type | Description | -| --- | --- | --- | -| options | Object | An object with options | +| Param | Type | Description | +| ------------------ | -------------------- | ---------------------------------------------------------------- | +| options | Object | An object with options | | options.sequential | boolean | Allows to load dependencies sequentially (usefull for debugging) | -**Example** +**Example** + ```js -import Knifecycle from 'knifecycle' +import Knifecycle from 'knifecycle'; const $ = new Knifecycle(); ``` + ### knifecycle.register(initializer) ⇒ [Knifecycle](#Knifecycle) + Register an initializer **Kind**: instance method of [Knifecycle](#Knifecycle) -**Returns**: [Knifecycle](#Knifecycle) - The Knifecycle instance (for chaining) +**Returns**: [Knifecycle](#Knifecycle) - The Knifecycle instance +(for chaining) -| Param | Type | Description | -| --- | --- | --- | +| Param | Type | Description | +| ----------- | --------------------- | -------------- | | initializer | function | An initializer | ### knifecycle.toMermaidGraph(options) ⇒ String -Outputs a Mermaid compatible dependency graph of the declared services. -See [Mermaid docs](https://github.com/knsv/mermaid) + +Outputs a Mermaid compatible dependency graph of the declared services. See +[Mermaid docs](https://github.com/knsv/mermaid) **Kind**: instance method of [Knifecycle](#Knifecycle) -**Returns**: String - Returns a string containing the Mermaid dependency graph +**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 | +| 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** -**Example** ```js import Knifecycle, { inject, constant, service } from 'knifecycle'; import appInitializer from './app'; @@ -518,66 +554,74 @@ graph TD app-->ENV app-->OS ``` + ### knifecycle.run(dependenciesDeclarations) ⇒ Promise + Creates a new execution silo **Kind**: instance method of [Knifecycle](#Knifecycle) -**Returns**: Promise - Service descriptor promise +**Returns**: Promise - Service descriptor promise -| Param | Type | Description | -| --- | --- | --- | +| Param | Type | Description | +| ------------------------ | --------------------------------- | ------------- | | dependenciesDeclarations | Array.<String> | Service name. | -**Example** +**Example** + ```js -import Knifecycle, { constant } from 'knifecycle' +import Knifecycle, { constant } from 'knifecycle'; const $ = new Knifecycle(); $.register(constant('ENV', process.env)); -$.run(['ENV']) -.then(({ ENV }) => { - // Here goes your code -}) +$.run(['ENV']).then(({ ENV }) => { + // Here goes your code +}); ``` + ### knifecycle.destroy() ⇒ Promise + Destroy the Knifecycle instance **Kind**: instance method of [Knifecycle](#Knifecycle) **Returns**: Promise - Full destruction promise -**Example** +**Example** + ```js -import Knifecycle, { constant } from 'knifecycle' +import Knifecycle, { constant } from 'knifecycle'; const $ = new Knifecycle(); $.register(constant('ENV', process.env)); -$.run(['ENV']) -.then(({ ENV }) => { - // Here goes your code +$.run(['ENV']).then(({ ENV }) => { + // Here goes your code - // Finally destroy the instance - $.destroy() -}) + // Finally destroy the instance + $.destroy(); +}); ``` + ## initInitializerBuilder(services) ⇒ Promise.<function()> + Instantiate the initializer builder service **Kind**: global function -**Returns**: Promise.<function()> - A promise of the buildInitializer function +**Returns**: Promise.<function()> - A promise of the +buildInitializer function -| Param | Type | Description | -| --- | --- | --- | -| services | Object | The services to inject | +| Param | Type | Description | +| ------------------ | ------------------- | --------------------------- | +| services | Object | The services to inject | | services.$autoload | Object | The dependencies autoloader | -**Example** +**Example** + ```js import initInitializerBuilder from 'knifecycle/dist/build'; @@ -585,21 +629,24 @@ const buildInitializer = await initInitializerBuilder({ $autoload: async () => {}, }); ``` + ### initInitializerBuilder~buildInitializer(dependencies) ⇒ Promise.<String> -Create a JavaScript module that initialize -a set of dependencies with hardcoded + +Create a JavaScript module that initialize a set of dependencies with hardcoded import/awaits. -**Kind**: inner method of [initInitializerBuilder](#initInitializerBuilder) -**Returns**: Promise.<String> - The JavaScript module content +**Kind**: inner method of +[initInitializerBuilder](#initInitializerBuilder) +**Returns**: Promise.<String> - The JavaScript module content -| Param | Type | Description | -| --- | --- | --- | +| Param | Type | Description | +| ------------ | --------------------------------- | --------------------- | | dependencies | Array.<String> | The main dependencies | -**Example** +**Example** + ```js import initInitializerBuilder from 'knifecycle/dist/build'; @@ -609,100 +656,116 @@ const buildInitializer = await initInitializerBuilder({ const content = await buildInitializer(['entryPoint']); ``` + ## constant(name, value) ⇒ function + Decorator that creates an initializer for a constant value **Kind**: global function -**Returns**: function - Returns a new constant initializer +**Returns**: function - Returns a new constant initializer + +| Param | Type | Description | +| ----- | ------------------- | -------------------- | +| name | String | The constant's name. | +| value | any | The constant's value | -| Param | Type | Description | -| --- | --- | --- | -| name | String | The constant's name. | -| value | any | The constant's value | +**Example** -**Example** ```js import Knifecycle, { constant, service } from 'knifecycle'; const { printAnswer } = new Knifecycle() .register(constant('THE_NUMBER', value)) .register(constant('log', console.log.bind(console))) - .register(service( - async ({ THE_NUMBER, log }) => () => log(THE_NUMBER), - 'printAnswer', - ['THE_NUMBER', 'log'], - )) + .register( + service( + async ({ THE_NUMBER, log }) => + () => + log(THE_NUMBER), + 'printAnswer', + ['THE_NUMBER', 'log'], + ), + ) .run(['printAnswer']); printAnswer(); // 42 ``` + ## service(serviceBuilder, [name], [dependencies], [singleton], [extra]) ⇒ function + Decorator that creates an initializer from a service builder **Kind**: global function -**Returns**: function - Returns a new initializer +**Returns**: function - Returns a new initializer -| Param | Type | Description | -| --- | --- | --- | -| serviceBuilder | function | An async function to build the service | -| [name] | String | The service's name | -| [dependencies] | Array.<String> | The service's injected dependencies | -| [singleton] | Boolean | Whether the service is a singleton or not | -| [extra] | any | Eventual extra informations | +| Param | Type | Description | +| -------------- | --------------------------------- | ----------------------------------------- | +| serviceBuilder | function | An async function to build the service | +| [name] | String | The service's name | +| [dependencies] | Array.<String> | The service's injected dependencies | +| [singleton] | Boolean | Whether the service is a singleton or not | +| [extra] | any | Eventual extra informations | + +**Example** -**Example** ```js import Knifecycle, { constant, service } from 'knifecycle'; const { printAnswer } = new Knifecycle() .register(constant('THE_NUMBER', value)) .register(constant('log', console.log.bind(console))) - .register(service( - async ({ THE_NUMBER, log }) => () => log(THE_NUMBER), - 'printAnswer', - ['THE_NUMBER', 'log'], - true - )) + .register( + service( + async ({ THE_NUMBER, log }) => + () => + log(THE_NUMBER), + 'printAnswer', + ['THE_NUMBER', 'log'], + true, + ), + ) .run(['printAnswer']); printAnswer(); // 42 ``` + ## autoService(serviceBuilder) ⇒ function -Decorator that creates an initializer from a service - builder by automatically detecting its name - and dependencies + +Decorator that creates an initializer from a service builder by automatically +detecting its name and dependencies **Kind**: global function -**Returns**: function - Returns a new initializer +**Returns**: function - Returns a new initializer -| Param | Type | Description | -| --- | --- | --- | +| Param | Type | Description | +| -------------- | --------------------- | -------------------------------------- | | serviceBuilder | function | An async function to build the service | ## provider(providerBuilder, [name], [dependencies], [singleton], [extra]) ⇒ function -Decorator that creates an initializer for a provider - builder + +Decorator that creates an initializer for a provider builder **Kind**: global function -**Returns**: function - Returns a new provider initializer +**Returns**: function - Returns a new provider initializer -| Param | Type | Description | -| --- | --- | --- | -| providerBuilder | function | An async function to build the service provider | -| [name] | String | The service's name | -| [dependencies] | Array.<String> | The service's dependencies | -| [singleton] | Boolean | Whether the service is a singleton or not | -| [extra] | any | Eventual extra informations | +| Param | Type | Description | +| --------------- | --------------------------------- | ----------------------------------------------- | +| providerBuilder | function | An async function to build the service provider | +| [name] | String | The service's name | +| [dependencies] | Array.<String> | The service's dependencies | +| [singleton] | Boolean | Whether the service is a singleton or not | +| [extra] | any | Eventual extra informations | + +**Example** -**Example** ```js import Knifecycle, { provider } from 'knifecycle' import fs from 'fs'; @@ -735,88 +798,95 @@ async function configProvider() { }); } ``` + ## autoProvider(providerBuilder) ⇒ function -Decorator that creates an initializer from a provider - builder by automatically detecting its name - and dependencies + +Decorator that creates an initializer from a provider builder by automatically +detecting its name and dependencies **Kind**: global function -**Returns**: function - Returns a new provider initializer +**Returns**: function - Returns a new provider initializer -| Param | Type | Description | -| --- | --- | --- | +| Param | Type | Description | +| --------------- | --------------------- | ----------------------------------------------- | | providerBuilder | function | An async function to build the service provider | ## handler(handlerFunction, [name], [dependencies], [options]) ⇒ function + Shortcut to create an initializer with a simple handler **Kind**: global function -**Returns**: function - Returns a new initializer +**Returns**: function - Returns a new initializer -| Param | Type | Default | Description | -| --- | --- | --- | --- | -| handlerFunction | function | | The handler function | -| [name] | String | | The name of the handler. Default to the DI prop if exists | -| [dependencies] | Array.<String> | [] | The dependencies to inject in it | -| [options] | Object | | Options attached to the built initializer | +| Param | Type | Default | Description | +| --------------- | --------------------------------- | --------------- | --------------------------------------------------------- | +| handlerFunction | function | | The handler function | +| [name] | String | | The name of the handler. Default to the DI prop if exists | +| [dependencies] | Array.<String> | [] | The dependencies to inject in it | +| [options] | Object | | Options attached to the built initializer | + +**Example** -**Example** ```js import Knifecycle, { handler } from 'knifecycle'; -new Knifecycle() -.register(handler(getUser, 'getUser', ['db', '?log'])); +new Knifecycle().register(handler(getUser, 'getUser', ['db', '?log'])); -const QUERY = `SELECT * FROM users WHERE id=$1` +const QUERY = `SELECT * FROM users WHERE id=$1`; async function getUser({ db }, userId) { const [row] = await db.query(QUERY, userId); return row; } ``` + ## autoHandler(handlerFunction) ⇒ function + Allows to create an initializer with a simple handler automagically **Kind**: global function -**Returns**: function - Returns a new initializer +**Returns**: function - Returns a new initializer -| Param | Type | Description | -| --- | --- | --- | +| Param | Type | Description | +| --------------- | --------------------- | -------------------- | | handlerFunction | function | The handler function | -**Example** +**Example** + ```js import Knifecycle, { autoHandler } from 'knifecycle'; -new Knifecycle() -.register(autoHandler(getUser)); +new Knifecycle().register(autoHandler(getUser)); -const QUERY = `SELECT * FROM users WHERE id=$1` +const QUERY = `SELECT * FROM users WHERE id=$1`; async function getUser({ db }, userId) { const [row] = await db.query(QUERY, userId); return row; } ``` + ## parseDependencyDeclaration(dependencyDeclaration) ⇒ Object + Explode a dependency declaration an returns its parts. **Kind**: global function -**Returns**: Object - The various parts of it +**Returns**: Object - The various parts of it -| Param | Type | Description | -| --- | --- | --- | +| Param | Type | Description | +| --------------------- | ------------------- | ------------------------------- | | dependencyDeclaration | String | A dependency declaration string | -**Example** +**Example** + ```js parseDependencyDeclaration('pgsql>db'); // Returns @@ -826,19 +896,22 @@ parseDependencyDeclaration('pgsql>db'); optional: false, } ``` + ## stringifyDependencyDeclaration(dependencyDeclarationParts) ⇒ String + Stringify a dependency declaration from its parts. **Kind**: global function -**Returns**: String - The various parts of it +**Returns**: String - The various parts of it -| Param | Type | Description | -| --- | --- | --- | +| Param | Type | Description | +| -------------------------- | ------------------- | ------------------------------- | | dependencyDeclarationParts | Object | A dependency declaration string | -**Example** +**Example** + ```js stringifyDependencyDeclaration({ serviceName: 'pgsql', @@ -847,11 +920,13 @@ stringifyDependencyDeclaration({ }); // Returns -'pgsql>db' +('pgsql>db'); ``` # Authors + - [Nicolas Froidure](http://insertafter.com/en/index.html) # License + [MIT](https://github.com/nfroidure/knifecycle/blob/main/LICENSE) diff --git a/package.json b/package.json index 8b2a544..7035000 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,8 @@ "lifecycle", "shutdown", "dependencies", - "service" + "service", + "knifecycle" ], "author": { "name": "Nicolas Froidure", diff --git a/src/build.test.ts b/src/build.test.ts index 973fc07..8aac29f 100644 --- a/src/build.test.ts +++ b/src/build.test.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { describe, test } from '@jest/globals'; -import assert from 'assert'; +import { describe, test, expect } from '@jest/globals'; import { YError } from 'yerror'; import initInitializerBuilder from './build.js'; import { Knifecycle, initializer, constant } from './index.js'; @@ -12,6 +11,10 @@ describe('buildInitializer', () => { }; } const mockedDepsHash = { + $fatalError: constant('$fatalError', undefined), + $dispose: constant('$dispose', undefined), + $instance: constant('$instance', undefined), + $siloContext: constant('$siloContext', undefined), NODE_ENV: constant('NODE_ENV', 'development'), dep1: initializer( { @@ -67,9 +70,27 @@ describe('buildInitializer', () => { const { buildInitializer } = await $.run(['buildInitializer']); const content = await buildInitializer(['dep1', 'finalMappedDep>dep3']); - assert.equal( - content, - ` + + expect(content).toMatchInlineSnapshot(` +" +import { initFatalError } from 'knifecycle'; + +const batchsDisposers = []; + +async function $dispose() { + for(const batchDisposers of batchsDisposers.reverse()) { + await Promise.all( + batchDisposers + .map(batchDisposer => batchDisposer()) + ); + } +} + +const $instance = { + destroy: $dispose, +}; + + // Definition batch #0 import initDep1 from './services/dep1'; const NODE_ENV = "development"; @@ -81,7 +102,10 @@ import initDep2 from './services/dep2'; import initDep3 from './services/dep3'; export async function initialize(services = {}) { + const $fatalError = await initFatalError(); + // Initialization batch #0 + batchsDisposers[0] = []; const batch0 = { dep1: initDep1({ }), @@ -97,11 +121,20 @@ export async function initialize(services = {}) { services['NODE_ENV'] = await batch0['NODE_ENV']; // Initialization batch #1 + batchsDisposers[1] = []; const batch1 = { dep2: initDep2({ dep1: services['dep1'], NODE_ENV: services['NODE_ENV'], - }).then(provider => provider.service), + }).then(provider => { + if(provider.dispose) { + batchsDisposers[1].push(provider.dispose); + } + if(provider.fatalErrorPromise) { + $fatalError.registerErrorPromise(provider.fatalErrorPromise); + } + return provider.service; + }), }; await Promise.all( @@ -112,6 +145,7 @@ export async function initialize(services = {}) { services['dep2'] = await batch1['dep2']; // Initialization batch #2 + batchsDisposers[2] = []; const batch2 = { dep3: initDep3({ dep2: services['dep2'], @@ -132,18 +166,18 @@ export async function initialize(services = {}) { finalMappedDep: services['dep3'], }; } -`, - ); +" +`); }); - // TODO: allow building with internal dependencies - test.skip('should work with simple internal services dependencies', async () => { + test('should work with simple internal services dependencies', async () => { const $ = new Knifecycle(); $.register(constant('PWD', '~/my-project')); $.register(initAutoloader); $.register(initInitializerBuilder); $.register(constant('$fatalError', {})); + $.register(constant('$instance', {})); const { buildInitializer } = await $.run(['buildInitializer']); @@ -152,14 +186,33 @@ export async function initialize(services = {}) { 'finalMappedDep>dep3', '$fatalError', '$dispose', + '$instance', '$siloContext', ]); - assert.equal( - content, - ` + expect(content).toMatchInlineSnapshot(` +" +import { initFatalError } from 'knifecycle'; + +const batchsDisposers = []; + +async function $dispose() { + for(const batchDisposers of batchsDisposers.reverse()) { + await Promise.all( + batchDisposers + .map(batchDisposer => batchDisposer()) + ); + } +} + +const $instance = { + destroy: $dispose, +}; + + // Definition batch #0 import initDep1 from './services/dep1'; const NODE_ENV = "development"; +const $siloContext = undefined; // Definition batch #1 import initDep2 from './services/dep2'; @@ -168,11 +221,18 @@ import initDep2 from './services/dep2'; import initDep3 from './services/dep3'; export async function initialize(services = {}) { + const $fatalError = await initFatalError(); + // Initialization batch #0 + batchsDisposers[0] = []; const batch0 = { dep1: initDep1({ }), NODE_ENV: Promise.resolve(NODE_ENV), + $fatalError: Promise.resolve($fatalError), + $dispose: Promise.resolve($dispose), + $instance: Promise.resolve($instance), + $siloContext: Promise.resolve($siloContext), }; await Promise.all( @@ -182,13 +242,26 @@ export async function initialize(services = {}) { services['dep1'] = await batch0['dep1']; services['NODE_ENV'] = await batch0['NODE_ENV']; + services['$fatalError'] = await batch0['$fatalError']; + services['$dispose'] = await batch0['$dispose']; + services['$instance'] = await batch0['$instance']; + services['$siloContext'] = await batch0['$siloContext']; // Initialization batch #1 + batchsDisposers[1] = []; const batch1 = { dep2: initDep2({ dep1: services['dep1'], NODE_ENV: services['NODE_ENV'], - }).then(provider => provider.service), + }).then(provider => { + if(provider.dispose) { + batchsDisposers[1].push(provider.dispose); + } + if(provider.fatalErrorPromise) { + $fatalError.registerErrorPromise(provider.fatalErrorPromise); + } + return provider.service; + }), }; await Promise.all( @@ -199,6 +272,7 @@ export async function initialize(services = {}) { services['dep2'] = await batch1['dep2']; // Initialization batch #2 + batchsDisposers[2] = []; const batch2 = { dep3: initDep3({ dep2: services['dep2'], @@ -217,9 +291,13 @@ export async function initialize(services = {}) { return { dep1: services['dep1'], finalMappedDep: services['dep3'], + $fatalError: services['$fatalError'], + $dispose: services['$dispose'], + $instance: services['$instance'], + $siloContext: services['$siloContext'], }; } -`, - ); +" +`); }); }); diff --git a/src/build.ts b/src/build.ts index 0f3b7f0..2e8dafc 100644 --- a/src/build.ts +++ b/src/build.ts @@ -4,12 +4,16 @@ import { initializer, } from './util.js'; import { buildInitializationSequence } from './sequence.js'; +import { FATAL_ERROR } from './fatalError.js'; +import { DISPOSE } from './dispose.js'; +import type { Autoloader } from './index.js'; import type { DependencyDeclaration, Initializer, Dependencies, } from './util.js'; -import type { Autoloader } from './index.js'; + +export const MANAGED_SERVICES = [FATAL_ERROR, DISPOSE, '$instance']; type DependencyTreeNode = { __name: string; @@ -107,37 +111,62 @@ async function initInitializerBuilder({ }); batches.pop(); - return `${batches - .map( - (batch, index) => ` + return ` +import { initFatalError } from 'knifecycle'; + +const batchsDisposers = []; + +async function $dispose() { + for(const batchDisposers of batchsDisposers.reverse()) { + await Promise.all( + batchDisposers + .map(batchDisposer => batchDisposer()) + ); + } +} + +const $instance = { + destroy: $dispose, +}; + +${batches + .map( + (batch, index) => ` // Definition batch #${index}${batch - .map((name) => { - if ( - 'constant' === - dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE] - ) { - return ` + .map((name) => { + if (MANAGED_SERVICES.includes(name)) { + return ''; + } + if ( + 'constant' === + dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE] + ) { + return ` const ${name} = ${JSON.stringify( - dependenciesHash[name].__initializer[SPECIAL_PROPS.VALUE], - null, - 2, - )};`; - } + dependenciesHash[name].__initializer[SPECIAL_PROPS.VALUE], + null, + 2, + )};`; + } - return ` + return ` import ${dependenciesHash[name].__initializerName} from '${dependenciesHash[name].__path}';`; - }) - .join('')}`, - ) - .join('\n')} + }) + .join('')}`, + ) + .join('\n')} -export async function initialize(services = {}) {${batches - .map( - (batch, index) => ` +export async function initialize(services = {}) { + const $fatalError = await initFatalError(); +${batches + .map( + (batch, index) => ` // Initialization batch #${index} + batchsDisposers[${index}] = []; const batch${index} = {${batch .map((name) => { if ( + MANAGED_SERVICES.includes(name) || 'constant' === dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE] ) { return ` @@ -158,7 +187,15 @@ export async function initialize(services = {}) {${batches } })${ 'provider' === dependenciesHash[name].__type - ? '.then(provider => provider.service)' + ? `.then(provider => { + if(provider.dispose) { + batchsDisposers[${index}].push(provider.dispose); + } + if(provider.fatalErrorPromise) { + $fatalError.registerErrorPromise(provider.fatalErrorPromise); + } + return provider.service; + })` : '' },`; }) @@ -176,8 +213,8 @@ ${batch }) .join('')} `, - ) - .join('')} + ) + .join('')} return {${dependencies .map(parseDependencyDeclaration) .map( diff --git a/src/dispose.ts b/src/dispose.ts new file mode 100644 index 0000000..8c97d4e --- /dev/null +++ b/src/dispose.ts @@ -0,0 +1,148 @@ +import { + NO_PROVIDER, + SPECIAL_PROPS, + parseDependencyDeclaration, + service, +} from './util.js'; +import initDebug from 'debug'; +import type { Disposer, ServiceName } from './util.js'; +import type { Knifecycle, SiloContext } from './index.js'; + +const debug = initDebug('knifecycle'); + +export const DISPOSE = '$dispose'; + +async function initDispose({ + $instance, + $siloContext, +}: { + $instance: Knifecycle; + $siloContext: SiloContext; +}): Promise { + return async () => { + $siloContext._shutdownPromise = + $siloContext._shutdownPromise || + _shutdownNextServices($siloContext.loadingSequences.concat()); + await $siloContext._shutdownPromise; + delete $instance._silosContexts[$siloContext.index]; + + // Shutdown services in their instanciation order + async function _shutdownNextServices( + serviceLoadSequences: ServiceName[][], + ) { + if (0 === serviceLoadSequences.length) { + return; + } + const currentServiceLoadSequence = serviceLoadSequences.pop() || []; + + // First ensure to remove services that are depend on + // by another service loaded in the same batch (may + // happen depending on the load sequence) + const dependendedByAServiceInTheSameBatch = + currentServiceLoadSequence.filter((serviceName) => { + if ( + currentServiceLoadSequence + .filter( + (anotherServiceName) => anotherServiceName !== serviceName, + ) + .some((anotherServiceName) => + ( + $instance._initializersStates[anotherServiceName] + ?.initializer?.[SPECIAL_PROPS.INJECT] || [] + ) + .map( + (declaration) => + parseDependencyDeclaration(declaration).serviceName, + ) + .includes(serviceName), + ) + ) { + debug( + `Delaying service "${serviceName}" dependencies shutdown to a dedicated batch.'`, + ); + return true; + } + }); + + await Promise.all( + currentServiceLoadSequence + .filter( + (serviceName) => + !dependendedByAServiceInTheSameBatch.includes(serviceName), + ) + .map(async (serviceName) => { + const initializeState = $instance._initializersStates[serviceName]; + + if ('silosInstances' in initializeState) { + const provider = $instance._getServiceProvider( + $siloContext, + serviceName, + ); + + if ( + serviceLoadSequences.some((servicesLoadSequence) => + servicesLoadSequence.includes(serviceName), + ) + ) { + debug( + 'Delaying service shutdown to another batch:', + serviceName, + ); + return Promise.resolve(); + } + if ( + !initializeState.silosInstances[$siloContext.index] + .instanceDisposePromise + ) { + debug('Shutting down a service:', serviceName); + initializeState.silosInstances[ + $siloContext.index + ].instanceDisposePromise = + provider && + provider !== NO_PROVIDER && + 'dispose' in provider && + provider.dispose + ? provider.dispose() + : Promise.resolve(); + } else { + debug('Reusing a service shutdown promise:', serviceName); + } + await initializeState.silosInstances[$siloContext.index] + .instanceDisposePromise; + } else if ('singletonProvider' in initializeState) { + initializeState.dependents = initializeState.dependents.filter( + ({ silo }) => silo !== $siloContext.index, + ); + + if (initializeState.dependents.length) { + debug( + `Will not shut down the ${serviceName} singleton service (still used ${initializeState.dependents.length} times).`, + initializeState.dependents, + ); + } else { + const provider = $instance._getServiceProvider( + $siloContext, + serviceName, + ); + debug('Shutting down a singleton service:', serviceName); + delete initializeState.singletonProviderLoadPromise; + delete initializeState.singletonProvider; + return provider && + provider !== NO_PROVIDER && + 'dispose' in provider && + provider.dispose + ? provider.dispose() + : Promise.resolve(); + } + } + }), + ); + if (dependendedByAServiceInTheSameBatch.length) { + serviceLoadSequences.unshift(dependendedByAServiceInTheSameBatch); + } + await _shutdownNextServices(serviceLoadSequences); + } + }; +} + +export default service(initDispose, DISPOSE, ['$instance', '$siloContext']); diff --git a/src/fatalError.ts b/src/fatalError.ts new file mode 100644 index 0000000..3aaa04e --- /dev/null +++ b/src/fatalError.ts @@ -0,0 +1,56 @@ +import { printStackTrace } from 'yerror'; +import { service } from './util.js'; +import initDebug from 'debug'; + +const debug = initDebug('knifecycle'); + +export const FATAL_ERROR = '$fatalError'; + +export type FatalErrorService = { + errorPromise: Promise; + registerErrorPromise: (errorPromise: Promise) => void; + unregisterErrorPromise: (errorPromise: Promise) => void; + throwFatalError: (err: Error) => void; +}; + +async function initFatalError(): Promise { + const errorPromises: Promise[] = []; + let errorCatchStep = 0; + let rejectFatalError; + const errorPromise = new Promise((_resolve, reject) => { + rejectFatalError = reject; + }); + const throwFatalError = (err: Error) => { + debug('Handled a fatal error', printStackTrace(err)); + rejectFatalError(err); + }; + const handleErrorCatch = () => { + const currentStep = ++errorCatchStep; + + Promise.all(errorPromises).catch((err: Error) => { + if (currentStep === errorCatchStep) { + throwFatalError(err); + } else { + debug( + `Ignored a fatal error ${currentStep}/${errorCatchStep}:`, + printStackTrace(err), + ); + } + }); + }; + + return { + errorPromise, + registerErrorPromise: (errorPromise: Promise) => { + errorPromises.push(errorPromise); + handleErrorCatch(); + }, + unregisterErrorPromise: (errorPromise: Promise) => { + errorPromises.filter((anErrorPromise) => anErrorPromise !== errorPromise); + handleErrorCatch(); + }, + throwFatalError, + }; +} + +export default service(initFatalError, FATAL_ERROR, [], true); diff --git a/src/index.test.ts b/src/index.test.ts index bb0e86a..6fecf35 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -792,7 +792,7 @@ describe('Knifecycle', () => { }) { return Promise.resolve({ service: { - fatalErrorPromise: $fatalError.promise, + fatalErrorPromise: $fatalError.errorPromise, }, }); } diff --git a/src/index.ts b/src/index.ts index dffe15b..c6de224 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint max-len: ["warn", { "ignoreComments": true }] @typescript-eslint/no-this-alias: "warn" */ import { + NO_PROVIDER, SPECIAL_PROPS, SPECIAL_PROPS_PREFIX, DECLARATION_SEPARATOR, @@ -34,8 +35,10 @@ import { stringifyDependencyDeclaration, unwrapInitializerProperties, } from './util.js'; +import initFatalError, { FATAL_ERROR } from './fatalError.js'; +import initDispose, { DISPOSE } from './dispose.js'; import initInitializerBuilder from './build.js'; -import { YError } from 'yerror'; +import { YError, printStackTrace } from 'yerror'; import initDebug from 'debug'; import type { ServiceName, @@ -67,6 +70,8 @@ import type { Parameters, } from './util.js'; import type { BuildInitializer } from './build.js'; +import type { FatalErrorService } from './fatalError.js'; +export { initFatalError, initDispose }; export type { ServiceName, Service, @@ -96,13 +101,14 @@ export type { HandlerFunction, Parameters, BuildInitializer, + FatalErrorService, }; export const RUN_DEPENDENT_NAME = '__run__'; export const SYSTEM_DEPENDENT_NAME = '__system__'; export const AUTOLOAD_DEPENDENT_NAME = '__autoloader__'; export const INJECTOR_DEPENDENT_NAME = '__injector__'; -export const NO_PROVIDER = Symbol('NO_PROVIDER'); +export { NO_PROVIDER }; export type KnifecycleOptions = { sequential?: boolean; @@ -166,13 +172,8 @@ export interface SiloContext { index: SiloIndex; loadingServices: ServiceName[]; loadingSequences: ServiceName[][]; - errorsPromises: Promise[]; _shutdownPromise?: Promise; - throwFatalError?: (err: Error) => void; } -export type FatalErrorService = { - promise: Promise; -}; export type InternalDependencies = { $dispose: Disposer; @@ -185,12 +186,17 @@ export type InternalDependencies = { const debug = initDebug('knifecycle'); -const DISPOSE = '$dispose'; -const AUTOLOAD = '$autoload'; -const INJECTOR = '$injector'; -const INSTANCE = '$instance'; -const SILO_CONTEXT = '$siloContext'; -const FATAL_ERROR = '$fatalError'; +export { DISPOSE, FATAL_ERROR }; +export const AUTOLOAD = '$autoload'; +export const INJECTOR = '$injector'; +export const INSTANCE = '$instance'; +export const SILO_CONTEXT = '$siloContext'; +export const UNBUILDABLE_SERVICES = [ + AUTOLOAD, + INJECTOR, + INSTANCE, + SILO_CONTEXT, +]; /* Architecture Note #1: Knifecycle @@ -226,8 +232,8 @@ A service provider is full of state since its concern is class Knifecycle { private _options: KnifecycleOptions; private _silosCounter: number; - private _silosContexts: Record; - private _initializersStates: Record< + _silosContexts: Record; + _initializersStates: Record< string, InitializerStateDescriptor >; @@ -253,12 +259,9 @@ class Knifecycle { this._silosContexts = {}; this._initializersStates = { [FATAL_ERROR]: { - initializer: service(async () => { - throw new YError('E_UNEXPECTED_INIT', FATAL_ERROR); - }, FATAL_ERROR), + initializer: initFatalError, autoloaded: false, dependents: [], - silosInstances: {}, }, [SILO_CONTEXT]: { initializer: service(async () => { @@ -269,9 +272,7 @@ class Knifecycle { silosInstances: {}, }, [DISPOSE]: { - initializer: service(async () => { - throw new YError('E_UNEXPECTED_INIT', DISPOSE); - }, DISPOSE), + initializer: initDispose as any, autoloaded: false, dependents: [], silosInstances: {}, @@ -405,7 +406,7 @@ class Knifecycle { _checkInitializerDependencies(initializer: Initializer) { const initializerDependsOfItself = initializer[SPECIAL_PROPS.INJECT] .map((dependencyDeclaration) => { - const serviceName = _pickServiceNameFromDeclaration( + const { serviceName } = parseDependencyDeclaration( dependencyDeclaration, ); @@ -413,7 +414,7 @@ class Knifecycle { // TEMPFIX: let's build initializer[SPECIAL_PROPS.NAME] !== 'BUILD_CONSTANTS' && // TEMPFIX: Those services are special... - ![FATAL_ERROR, INJECTOR, SILO_CONTEXT].includes(serviceName) && + ![INJECTOR, SILO_CONTEXT].includes(serviceName) && initializer[SPECIAL_PROPS.SINGLETON] && this._initializersStates[serviceName] && 'initializer' in this._initializersStates[serviceName] && @@ -450,7 +451,6 @@ class Knifecycle { (serviceName) => ![ // TEMPFIX: Those services are special... - FATAL_ERROR, INJECTOR, SILO_CONTEXT, ].includes(serviceName), @@ -466,7 +466,10 @@ class Knifecycle { SPECIAL_PROPS.INJECT ] || [] ) - .map(_pickServiceNameFromDeclaration) + .map( + (declaration) => + parseDependencyDeclaration(declaration).serviceName, + ) .includes(initializer[SPECIAL_PROPS.NAME]) ) { debug( @@ -504,7 +507,9 @@ class Knifecycle { dependencyDeclaration: DependencyDeclaration, declarationsStacks: DependencyDeclaration[] = [], ): void { - const serviceName = _pickServiceNameFromDeclaration(dependencyDeclaration); + const serviceName = parseDependencyDeclaration( + dependencyDeclaration, + ).serviceName; const initializersState = this._initializersStates[serviceName]; if (!initializersState || !initializersState.initializer) { @@ -514,9 +519,9 @@ class Knifecycle { declarationsStacks = declarationsStacks.concat(dependencyDeclaration); (initializersState.initializer[SPECIAL_PROPS.INJECT] || []).forEach( (childDependencyDeclaration) => { - const childServiceName = _pickServiceNameFromDeclaration( + const childServiceName = parseDependencyDeclaration( childDependencyDeclaration, - ); + ).serviceName; if (rootServiceName === childServiceName) { throw new YError( @@ -598,9 +603,9 @@ class Knifecycle { return links.concat( initializerState.initializer[SPECIAL_PROPS.INJECT].map( (dependencyDeclaration) => { - const dependedServiceName = _pickServiceNameFromDeclaration( + const dependedServiceName = parseDependencyDeclaration( dependencyDeclaration, - ); + ).serviceName; return { serviceName, dependedServiceName }; }, @@ -674,32 +679,12 @@ class Knifecycle { index: siloIndex, loadingServices: [], loadingSequences: [], - errorsPromises: [], }; if (this._shutdownPromise) { throw new YError('E_INSTANCE_DESTROYED'); } - // Create a provider for the special fatal error service - ( - this._initializersStates[FATAL_ERROR] as SiloedInitializerStateDescriptor< - FatalErrorService, - Dependencies - > - ).silosInstances[siloIndex] = { - provider: { - service: { - promise: new Promise((_resolve, reject) => { - siloContext.throwFatalError = (err) => { - debug('Handled a fatal error', err); - reject(err); - }; - }), - }, - }, - }; - // Make the siloContext available for internal injections ( this._initializersStates[ @@ -708,155 +693,16 @@ class Knifecycle { ).silosInstances[siloIndex] = { provider: { service: siloContext }, }; - // Create a provider for the shutdown special dependency - ( - this._initializersStates[DISPOSE] as SiloedInitializerStateDescriptor< - Disposer, - Dependencies - > - ).silosInstances[siloIndex] = { - provider: { - service: async () => { - const _this = this; - siloContext._shutdownPromise = - siloContext._shutdownPromise || - _shutdownNextServices(siloContext.loadingSequences.concat()); - await siloContext._shutdownPromise; - delete this._silosContexts[siloContext.index]; - - // Shutdown services in their instanciation order - async function _shutdownNextServices( - serviceLoadSequences: ServiceName[][], - ) { - if (0 === serviceLoadSequences.length) { - return; - } - const currentServiceLoadSequence = serviceLoadSequences.pop() || []; - - // First ensure to remove services that are depend on - // by another service loaded in the same batch (may - // happen depending on the load sequence) - const dependendedByAServiceInTheSameBatch = - currentServiceLoadSequence.filter((serviceName) => { - if ( - currentServiceLoadSequence - .filter( - (anotherServiceName) => - anotherServiceName !== serviceName, - ) - .some((anotherServiceName) => - ( - _this._initializersStates[anotherServiceName] - ?.initializer?.[SPECIAL_PROPS.INJECT] || [] - ) - .map(_pickServiceNameFromDeclaration) - .includes(serviceName), - ) - ) { - debug( - `Delaying service "${serviceName}" dependencies shutdown to a dedicated batch.'`, - ); - return true; - } - }); - - await Promise.all( - currentServiceLoadSequence - .filter( - (serviceName) => - !dependendedByAServiceInTheSameBatch.includes(serviceName), - ) - .map(async (serviceName) => { - const initializeState = - _this._initializersStates[serviceName]; - - if ('silosInstances' in initializeState) { - const provider = _this._getServiceProvider( - siloContext, - serviceName, - ); - - if ( - serviceLoadSequences.some((servicesLoadSequence) => - servicesLoadSequence.includes(serviceName), - ) - ) { - debug( - 'Delaying service shutdown to another batch:', - serviceName, - ); - return Promise.resolve(); - } - if ( - !initializeState.silosInstances[siloContext.index] - .instanceDisposePromise - ) { - debug('Shutting down a service:', serviceName); - initializeState.silosInstances[ - siloContext.index - ].instanceDisposePromise = - provider && - provider !== NO_PROVIDER && - 'dispose' in provider && - provider.dispose - ? provider.dispose() - : Promise.resolve(); - } else { - debug('Reusing a service shutdown promise:', serviceName); - } - await initializeState.silosInstances[siloContext.index] - .instanceDisposePromise; - } else if ('singletonProvider' in initializeState) { - initializeState.dependents = - initializeState.dependents.filter( - ({ silo }) => silo !== siloContext.index, - ); - - if (initializeState.dependents.length) { - debug( - `Will not shut down the ${serviceName} singleton service (still used ${initializeState.dependents.length} times).`, - initializeState.dependents, - ); - } else { - const provider = _this._getServiceProvider( - siloContext, - serviceName, - ); - debug('Shutting down a singleton service:', serviceName); - delete initializeState.singletonProviderLoadPromise; - delete initializeState.singletonProvider; - return provider && - provider !== NO_PROVIDER && - 'dispose' in provider && - provider.dispose - ? provider.dispose() - : Promise.resolve(); - } - } - }), - ); - if (dependendedByAServiceInTheSameBatch.length) { - serviceLoadSequences.unshift(dependendedByAServiceInTheSameBatch); - } - await _shutdownNextServices(serviceLoadSequences); - } - }, - dispose: Promise.resolve.bind(Promise), - }, - }; + this._silosContexts[siloContext.index] = siloContext; const services = await this._loadInitializerDependencies( siloContext, [RUN_DEPENDENT_NAME], dependenciesDeclarations, - [DISPOSE], + [DISPOSE, FATAL_ERROR], ); - // TODO: recreate error promise when autoloaded/injected things? - debug('Handling fatal errors:', siloContext.errorsPromises); - Promise.all(siloContext.errorsPromises).catch(siloContext.throwFatalError); - debug('All dependencies now loaded:', siloContext.loadingSequences); return services as ID; @@ -1095,8 +941,18 @@ class Knifecycle { } if (provider.fatalErrorPromise) { + const fatalErrorInitializerState = (await this._initializersStates[ + FATAL_ERROR + ]) as SingletonInitializerStateDescriptor; + + await fatalErrorInitializerState.singletonProviderLoadPromise; + + const fatalError = ( + fatalErrorInitializerState.singletonProvider as Provider + ).service; + debug('Registering service descriptor error promise:', serviceName); - siloContext.errorsPromises.push(provider.fatalErrorPromise); + fatalError.registerErrorPromise(provider.fatalErrorPromise); } if (initializerState.initializer[SPECIAL_PROPS.SINGLETON]) { @@ -1291,7 +1147,7 @@ class Knifecycle { `${[...parentsNames, serviceName].join( '->', )}: Could not autoload the initializer...`, - err, + printStackTrace(err as Error), ); initializerState.initializer = undefined; resolveInitializer(undefined); @@ -1454,14 +1310,6 @@ export { initInitializerBuilder, }; -function _pickServiceNameFromDeclaration( - dependencyDeclaration: DependencyDeclaration, -): ServiceName { - const { serviceName } = parseDependencyDeclaration(dependencyDeclaration); - - return serviceName; -} - function _applyShapes(shapes, serviceName) { return shapes.reduce((shapedService, shape) => { if (shapedService) { diff --git a/src/util.ts b/src/util.ts index 27f156e..20b1653 100644 --- a/src/util.ts +++ b/src/util.ts @@ -6,6 +6,8 @@ import initDebug from 'debug'; const debug = initDebug('knifecycle'); +export const NO_PROVIDER = Symbol('NO_PROVIDER'); + /* Architecture Note #1.2: Creating initializers `knifecycle` uses initializers at its a core. An initializer is basically