From 0cf2fd5c8177de59778d521fea65fce12b9f8611 Mon Sep 17 00:00:00 2001 From: Nicolas Froidure Date: Mon, 2 Dec 2024 14:44:24 +0100 Subject: [PATCH] feat(core): add a $ready service It allows a service to know when its silo context is fully loaded. fix #135 --- ARCHITECTURE.md | 16 ++++++++-------- src/build.test.ts | 36 +++++++++++++++++++++++++++--------- src/build.ts | 21 ++++++++++++++++++++- src/index.test.ts | 6 ++++++ src/index.ts | 36 +++++++++++++++++++++++++++++++++++- src/util.ts | 1 + 6 files changed, 97 insertions(+), 19 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f8ef704..26eaa20 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#L204-L223) +[See in context](./src/index.ts#L213-L232) @@ -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#L225-L234) +[See in context](./src/index.ts#L234-L243) @@ -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#L14-L35) +[See in context](./src/util.ts#L15-L36) @@ -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#L1371-L1380) +[See in context](./src/util.ts#L1372-L1381) @@ -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#L315-L339) +[See in context](./src/index.ts#L332-L356) @@ -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#L671-L681) +[See in context](./src/index.ts#L688-L698) @@ -157,7 +157,7 @@ For the build to work, we need: - the dependencies list you want to initialize -[See in context](./src/build.ts#L36-L51) +[See in context](./src/build.ts#L37-L52) @@ -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#L1441-L1452) +[See in context](./src/util.ts#L1442-L1453) diff --git a/src/build.test.ts b/src/build.test.ts index e83c269..1616bb8 100644 --- a/src/build.test.ts +++ b/src/build.test.ts @@ -50,7 +50,7 @@ describe('buildInitializer', () => { ), dep5: initializer( { - inject: [], + inject: ['$ready'], type: 'service', name: 'dep5', }, @@ -111,6 +111,10 @@ async function $dispose() { } } +let resolveReady; +const $ready = new Promise((resolve) => { + resolveReady = resolve; +}); const $instance = { destroy: $dispose, }; @@ -118,10 +122,10 @@ const $instance = { // Definition batch #0 import initDep1 from './services/dep1'; -import initDep5 from './services/dep5'; const NODE_ENV = "development"; // Definition batch #1 +import initDep5 from './services/dep5'; import initDep2 from './services/dep2'; // Definition batch #2 @@ -135,8 +139,7 @@ export async function initialize(services = {}) { const batch0 = { dep1: initDep1({ }), - dep5: initDep5({ - }), + $ready: Promise.resolve($ready), NODE_ENV: Promise.resolve(NODE_ENV), }; @@ -146,12 +149,15 @@ export async function initialize(services = {}) { ); services['dep1'] = await batch0['dep1']; - services['dep5'] = await batch0['dep5']; + services['$ready'] = await batch0['$ready']; services['NODE_ENV'] = await batch0['NODE_ENV']; // Initialization batch #1 batchsDisposers[1] = []; const batch1 = { + dep5: initDep5({ + $ready: services['$ready'], + }), dep2: initDep2({ dep1: services['dep1'], NODE_ENV: services['NODE_ENV'], @@ -171,6 +177,7 @@ export async function initialize(services = {}) { .map(key => batch1[key]) ); + services['dep5'] = await batch1['dep5']; services['dep2'] = await batch1['dep2']; // Initialization batch #2 @@ -235,6 +242,10 @@ async function $dispose() { } } +let resolveReady; +const $ready = new Promise((resolve) => { + resolveReady = resolve; +}); const $instance = { destroy: $dispose, }; @@ -357,6 +368,10 @@ async function $dispose() { } } +let resolveReady; +const $ready = new Promise((resolve) => { + resolveReady = resolve; +}); const $instance = { destroy: $dispose, }; @@ -364,11 +379,11 @@ const $instance = { // Definition batch #0 import initDep1 from './services/dep1'; -import initDep5 from './services/dep5'; const NODE_ENV = "development"; const $siloContext = undefined; // Definition batch #1 +import initDep5 from './services/dep5'; import initDep2 from './services/dep2'; // Definition batch #2 @@ -382,8 +397,7 @@ export async function initialize(services = {}) { const batch0 = { dep1: initDep1({ }), - dep5: initDep5({ - }), + $ready: Promise.resolve($ready), NODE_ENV: Promise.resolve(NODE_ENV), $fatalError: Promise.resolve($fatalError), $dispose: Promise.resolve($dispose), @@ -397,7 +411,7 @@ export async function initialize(services = {}) { ); services['dep1'] = await batch0['dep1']; - services['dep5'] = await batch0['dep5']; + services['$ready'] = await batch0['$ready']; services['NODE_ENV'] = await batch0['NODE_ENV']; services['$fatalError'] = await batch0['$fatalError']; services['$dispose'] = await batch0['$dispose']; @@ -407,6 +421,9 @@ export async function initialize(services = {}) { // Initialization batch #1 batchsDisposers[1] = []; const batch1 = { + dep5: initDep5({ + $ready: services['$ready'], + }), dep2: initDep2({ dep1: services['dep1'], NODE_ENV: services['NODE_ENV'], @@ -426,6 +443,7 @@ export async function initialize(services = {}) { .map(key => batch1[key]) ); + services['dep5'] = await batch1['dep5']; services['dep2'] = await batch1['dep2']; // Initialization batch #2 diff --git a/src/build.ts b/src/build.ts index 2498744..87be4cc 100644 --- a/src/build.ts +++ b/src/build.ts @@ -4,6 +4,7 @@ import { AUTOLOAD, parseDependencyDeclaration, initializer, + READY, } from './util.js'; import { buildInitializationSequence } from './sequence.js'; import { FATAL_ERROR } from './fatalError.js'; @@ -16,7 +17,7 @@ import type { } from './util.js'; import { OVERRIDES, pickOverridenName } from './overrides.js'; -export const MANAGED_SERVICES = [FATAL_ERROR, DISPOSE, INSTANCE]; +export const MANAGED_SERVICES = [FATAL_ERROR, DISPOSE, INSTANCE, READY]; type DependencyTreeNode = { __name: string; @@ -132,6 +133,10 @@ async function $dispose() { } } +let resolveReady; +const $ready = new Promise((resolve) => { + resolveReady = resolve; +}); const $instance = { destroy: $dispose, }; @@ -260,6 +265,20 @@ async function buildDependencyTree( mappedName, ]); + if(MANAGED_SERVICES.includes(finalName)) { + return { + + __name: finalName, + __initializer: async() => {}, + __inject: [], + __type: 'constant', + __initializerName: 'init' + upperCaseFirst(finalName.slice(1)), + __path: `internal://managed/${finalName}`, + __childNodes: [], + __parentsNames: [...parentsNames, finalName], + }; + } + try { const { path, initializer } = await $autoload(finalName); const node: DependencyTreeNode = { diff --git a/src/index.test.ts b/src/index.test.ts index fe4d4a6..025ee28 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -104,6 +104,12 @@ describe('Knifecycle', () => { }); }); + test('should work with the $ready service', async () => { + const $ready = await $.run<{ $ready: Promise }>(['$ready']); + + await $ready; + }); + test('should fail when overriding an initialized constant', async () => { $.register(constant('TEST', 1)); expect(await $.run(['TEST'])).toEqual({ diff --git a/src/index.ts b/src/index.ts index 246c161..9fd56ae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import { NO_PROVIDER, INSTANCE, SILO_CONTEXT, + READY, AUTOLOAD, SPECIAL_PROPS, SPECIAL_PROPS_PREFIX, @@ -192,7 +193,15 @@ export type InternalDependencies = { const debug = initDebug('knifecycle'); -export { DISPOSE, FATAL_ERROR, INSTANCE, SILO_CONTEXT, AUTOLOAD, OVERRIDES }; +export { + DISPOSE, + FATAL_ERROR, + INSTANCE, + SILO_CONTEXT, + READY, + AUTOLOAD, + OVERRIDES, +}; export const INJECTOR = '$injector'; export const UNBUILDABLE_SERVICES = [ AUTOLOAD, @@ -272,6 +281,14 @@ class Knifecycle { dependents: [], silosInstances: {}, }, + [READY]: { + initializer: service(async () => { + throw new YError('E_UNEXPECTED_INIT', READY); + }, READY), + autoloaded: false, + dependents: [], + silosInstances: {}, + }, [DISPOSE]: { initializer: initDispose as any, autoloaded: false, @@ -708,6 +725,11 @@ class Knifecycle { loadingSequences: [], }; + let resolveReady; + const ready = new Promise((resolve) => { + resolveReady = resolve; + }); + if (this._shutdownPromise) { throw new YError('E_INSTANCE_DESTROYED'); } @@ -721,6 +743,16 @@ class Knifecycle { provider: { service: siloContext }, }; + // Make the ready service available for internal injections + ( + this._initializersStates[READY] as SiloedInitializerStateDescriptor< + Promise, + Dependencies + > + ).silosInstances[siloIndex] = { + provider: { service: ready }, + }; + this._silosContexts[siloContext.index] = siloContext; const services = await this._loadInitializerDependencies( @@ -732,6 +764,8 @@ class Knifecycle { debug('All dependencies now loaded:', siloContext.loadingSequences); + resolveReady(); + return services as ID; } diff --git a/src/util.ts b/src/util.ts index 58a336a..ac4b3f6 100644 --- a/src/util.ts +++ b/src/util.ts @@ -9,6 +9,7 @@ const debug = initDebug('knifecycle'); export const NO_PROVIDER = Symbol('NO_PROVIDER'); export const INSTANCE = '$instance'; export const SILO_CONTEXT = '$siloContext'; +export const READY = '$ready'; export const AUTOLOAD = '$autoload'; /* Architecture Note #1.2: Creating initializers