knifecycle
Advanced tools
Comparing version 16.0.1 to 17.0.0
@@ -0,1 +1,6 @@ | ||
# [17.0.0](https://github.com/nfroidure/knifecycle/compare/v16.0.1...v17.0.0) (2023-08-20) | ||
Enabling the build to use fatal error promises and to gracefully | ||
exit. | ||
## [16.0.1](https://github.com/nfroidure/knifecycle/compare/v16.0.0...v16.0.1) (2023-08-16) | ||
@@ -2,0 +7,0 @@ |
@@ -0,3 +1,4 @@ | ||
import type { Autoloader } from './index.js'; | ||
import type { DependencyDeclaration, Initializer } from './util.js'; | ||
import type { Autoloader } from './index.js'; | ||
export declare const MANAGED_SERVICES: string[]; | ||
export type BuildInitializer = (dependencies: DependencyDeclaration[]) => Promise<string>; | ||
@@ -4,0 +5,0 @@ declare const _default: import("./util.js").ServiceInitializer<{ |
import { SPECIAL_PROPS, parseDependencyDeclaration, initializer, } from './util.js'; | ||
import { buildInitializationSequence } from './sequence.js'; | ||
import { FATAL_ERROR } from './fatalError.js'; | ||
import { DISPOSE } from './dispose.js'; | ||
export const MANAGED_SERVICES = [FATAL_ERROR, DISPOSE, '$instance']; | ||
/* Architecture Note #2: Build | ||
@@ -66,6 +69,27 @@ | ||
batches.pop(); | ||
return `${batches | ||
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 (MANAGED_SERVICES.includes(name)) { | ||
return ''; | ||
} | ||
if ('constant' === | ||
@@ -82,8 +106,12 @@ dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE]) { | ||
export async function initialize(services = {}) {${batches | ||
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 ('constant' === dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE]) { | ||
if (MANAGED_SERVICES.includes(name) || | ||
'constant' === dependenciesHash[name].__initializer[SPECIAL_PROPS.TYPE]) { | ||
return ` | ||
@@ -101,3 +129,11 @@ ${name}: Promise.resolve(${name}),`; | ||
})${'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; | ||
})` | ||
: ''},`; | ||
@@ -104,0 +140,0 @@ }) |
/* 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'; | ||
@@ -14,2 +13,6 @@ import initInitializerBuilder from './build.js'; | ||
const mockedDepsHash = { | ||
$fatalError: constant('$fatalError', undefined), | ||
$dispose: constant('$dispose', undefined), | ||
$instance: constant('$instance', undefined), | ||
$siloContext: constant('$siloContext', undefined), | ||
NODE_ENV: constant('NODE_ENV', 'development'), | ||
@@ -54,3 +57,22 @@ dep1: initializer({ | ||
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 | ||
@@ -67,3 +89,6 @@ import initDep1 from './services/dep1'; | ||
export async function initialize(services = {}) { | ||
const $fatalError = await initFatalError(); | ||
// Initialization batch #0 | ||
batchsDisposers[0] = []; | ||
const batch0 = { | ||
@@ -84,2 +109,3 @@ dep1: initDep1({ | ||
// Initialization batch #1 | ||
batchsDisposers[1] = []; | ||
const batch1 = { | ||
@@ -89,3 +115,11 @@ dep2: initDep2({ | ||
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; | ||
}), | ||
}; | ||
@@ -101,2 +135,3 @@ | ||
// Initialization batch #2 | ||
batchsDisposers[2] = []; | ||
const batch2 = { | ||
@@ -122,6 +157,6 @@ dep3: initDep3({ | ||
} | ||
" | ||
`); | ||
}); | ||
// 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(); | ||
@@ -132,2 +167,3 @@ $.register(constant('PWD', '~/my-project')); | ||
$.register(constant('$fatalError', {})); | ||
$.register(constant('$instance', {})); | ||
const { buildInitializer } = await $.run(['buildInitializer']); | ||
@@ -139,8 +175,29 @@ const content = await buildInitializer([ | ||
'$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; | ||
@@ -154,3 +211,6 @@ // Definition batch #1 | ||
export async function initialize(services = {}) { | ||
const $fatalError = await initFatalError(); | ||
// Initialization batch #0 | ||
batchsDisposers[0] = []; | ||
const batch0 = { | ||
@@ -160,2 +220,6 @@ dep1: initDep1({ | ||
NODE_ENV: Promise.resolve(NODE_ENV), | ||
$fatalError: Promise.resolve($fatalError), | ||
$dispose: Promise.resolve($dispose), | ||
$instance: Promise.resolve($instance), | ||
$siloContext: Promise.resolve($siloContext), | ||
}; | ||
@@ -170,4 +234,9 @@ | ||
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 = { | ||
@@ -177,3 +246,11 @@ dep2: initDep2({ | ||
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; | ||
}), | ||
}; | ||
@@ -189,2 +266,3 @@ | ||
// Initialization batch #2 | ||
batchsDisposers[2] = []; | ||
const batch2 = { | ||
@@ -208,4 +286,9 @@ dep3: initDep3({ | ||
finalMappedDep: services['dep3'], | ||
$fatalError: services['$fatalError'], | ||
$dispose: services['$dispose'], | ||
$instance: services['$instance'], | ||
$siloContext: services['$siloContext'], | ||
}; | ||
} | ||
" | ||
`); | ||
@@ -212,0 +295,0 @@ }); |
@@ -1,6 +0,10 @@ | ||
import { SPECIAL_PROPS, SPECIAL_PROPS_PREFIX, DECLARATION_SEPARATOR, OPTIONAL_FLAG, ALLOWED_INITIALIZER_TYPES, ALLOWED_SPECIAL_PROPS, parseInjections, readFunctionName, reuseSpecialProps, parseName, name, autoName, inject, useInject, mergeInject, autoInject, alsoInject, type, extra, singleton, initializer, constant, service, autoService, provider, autoProvider, wrapInitializer, handler, autoHandler, parseDependencyDeclaration, stringifyDependencyDeclaration, unwrapInitializerProperties } from './util.js'; | ||
import { NO_PROVIDER, SPECIAL_PROPS, SPECIAL_PROPS_PREFIX, DECLARATION_SEPARATOR, OPTIONAL_FLAG, ALLOWED_INITIALIZER_TYPES, ALLOWED_SPECIAL_PROPS, parseInjections, readFunctionName, reuseSpecialProps, parseName, name, autoName, inject, useInject, mergeInject, autoInject, alsoInject, type, extra, singleton, initializer, constant, service, autoService, provider, autoProvider, wrapInitializer, handler, autoHandler, parseDependencyDeclaration, stringifyDependencyDeclaration, unwrapInitializerProperties } from './util.js'; | ||
import initFatalError, { FATAL_ERROR } from './fatalError.js'; | ||
import initDispose, { DISPOSE } from './dispose.js'; | ||
import initInitializerBuilder from './build.js'; | ||
import type { ServiceName, Service, Disposer, FatalErrorPromise, Provider, Dependencies, DependencyDeclaration, ExtraInformations, ParsedDependencyDeclaration, ConstantProperties, ConstantInitializer, ProviderInitializerBuilder, ProviderProperties, ProviderInitializer, ProviderInputProperties, ServiceInitializerBuilder, ServiceProperties, ServiceInitializer, ServiceInputProperties, AsyncInitializerBuilder, AsyncInitializer, PartialAsyncInitializer, Initializer, ServiceInitializerWrapper, ProviderInitializerWrapper, HandlerFunction, Parameters } from './util.js'; | ||
import type { BuildInitializer } from './build.js'; | ||
export type { ServiceName, Service, Disposer, FatalErrorPromise, Provider, Dependencies, DependencyDeclaration, ExtraInformations, ParsedDependencyDeclaration, ConstantProperties, ConstantInitializer, ProviderInitializerBuilder, ProviderProperties, ProviderInitializer, ProviderInputProperties, ServiceInitializerBuilder, ServiceProperties, ServiceInitializer, ServiceInputProperties, AsyncInitializerBuilder, AsyncInitializer, PartialAsyncInitializer, Initializer, ServiceInitializerWrapper, ProviderInitializerWrapper, HandlerFunction, Parameters, BuildInitializer, }; | ||
import type { FatalErrorService } from './fatalError.js'; | ||
export { initFatalError, initDispose }; | ||
export type { ServiceName, Service, Disposer, FatalErrorPromise, Provider, Dependencies, DependencyDeclaration, ExtraInformations, ParsedDependencyDeclaration, ConstantProperties, ConstantInitializer, ProviderInitializerBuilder, ProviderProperties, ProviderInitializer, ProviderInputProperties, ServiceInitializerBuilder, ServiceProperties, ServiceInitializer, ServiceInputProperties, AsyncInitializerBuilder, AsyncInitializer, PartialAsyncInitializer, Initializer, ServiceInitializerWrapper, ProviderInitializerWrapper, HandlerFunction, Parameters, BuildInitializer, FatalErrorService, }; | ||
export declare const RUN_DEPENDENT_NAME = "__run__"; | ||
@@ -10,3 +14,3 @@ export declare const SYSTEM_DEPENDENT_NAME = "__system__"; | ||
export declare const INJECTOR_DEPENDENT_NAME = "__injector__"; | ||
export declare const NO_PROVIDER: unique symbol; | ||
export { NO_PROVIDER }; | ||
export type KnifecycleOptions = { | ||
@@ -57,9 +61,4 @@ sequential?: boolean; | ||
loadingSequences: ServiceName[][]; | ||
errorsPromises: Promise<void>[]; | ||
_shutdownPromise?: Promise<void>; | ||
throwFatalError?: (err: Error) => void; | ||
} | ||
export type FatalErrorService = { | ||
promise: Promise<void>; | ||
}; | ||
export type InternalDependencies = { | ||
@@ -73,7 +72,13 @@ $dispose: Disposer; | ||
}; | ||
export { DISPOSE, FATAL_ERROR }; | ||
export declare const AUTOLOAD = "$autoload"; | ||
export declare const INJECTOR = "$injector"; | ||
export declare const INSTANCE = "$instance"; | ||
export declare const SILO_CONTEXT = "$siloContext"; | ||
export declare const UNBUILDABLE_SERVICES: string[]; | ||
declare class Knifecycle { | ||
private _options; | ||
private _silosCounter; | ||
private _silosContexts; | ||
private _initializersStates; | ||
_silosContexts: Record<SiloIndex, SiloContext>; | ||
_initializersStates: Record<string, InitializerStateDescriptor<unknown, Dependencies>>; | ||
private _shutdownPromise?; | ||
@@ -80,0 +85,0 @@ /** |
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
/* eslint max-len: ["warn", { "ignoreComments": true }] @typescript-eslint/no-this-alias: "warn" */ | ||
import { SPECIAL_PROPS, SPECIAL_PROPS_PREFIX, DECLARATION_SEPARATOR, OPTIONAL_FLAG, ALLOWED_INITIALIZER_TYPES, ALLOWED_SPECIAL_PROPS, parseInjections, readFunctionName, reuseSpecialProps, parseName, name, autoName, inject, useInject, mergeInject, autoInject, alsoInject, type, extra, singleton, initializer, constant, service, autoService, provider, autoProvider, wrapInitializer, handler, autoHandler, parseDependencyDeclaration, stringifyDependencyDeclaration, unwrapInitializerProperties, } from './util.js'; | ||
import { NO_PROVIDER, SPECIAL_PROPS, SPECIAL_PROPS_PREFIX, DECLARATION_SEPARATOR, OPTIONAL_FLAG, ALLOWED_INITIALIZER_TYPES, ALLOWED_SPECIAL_PROPS, parseInjections, readFunctionName, reuseSpecialProps, parseName, name, autoName, inject, useInject, mergeInject, autoInject, alsoInject, type, extra, singleton, initializer, constant, service, autoService, provider, autoProvider, wrapInitializer, handler, autoHandler, parseDependencyDeclaration, 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'; | ||
export { initFatalError, initDispose }; | ||
export const RUN_DEPENDENT_NAME = '__run__'; | ||
@@ -11,10 +14,15 @@ export const SYSTEM_DEPENDENT_NAME = '__system__'; | ||
export const INJECTOR_DEPENDENT_NAME = '__injector__'; | ||
export const NO_PROVIDER = Symbol('NO_PROVIDER'); | ||
export { NO_PROVIDER }; | ||
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 | ||
@@ -76,8 +84,5 @@ | ||
[FATAL_ERROR]: { | ||
initializer: service(async () => { | ||
throw new YError('E_UNEXPECTED_INIT', FATAL_ERROR); | ||
}, FATAL_ERROR), | ||
initializer: initFatalError, | ||
autoloaded: false, | ||
dependents: [], | ||
silosInstances: {}, | ||
}, | ||
@@ -93,5 +98,3 @@ [SILO_CONTEXT]: { | ||
[DISPOSE]: { | ||
initializer: service(async () => { | ||
throw new YError('E_UNEXPECTED_INIT', DISPOSE); | ||
}, DISPOSE), | ||
initializer: initDispose, | ||
autoloaded: false, | ||
@@ -192,3 +195,3 @@ dependents: [], | ||
.map((dependencyDeclaration) => { | ||
const serviceName = _pickServiceNameFromDeclaration(dependencyDeclaration); | ||
const { serviceName } = parseDependencyDeclaration(dependencyDeclaration); | ||
if ( | ||
@@ -198,3 +201,3 @@ // TEMPFIX: let's build | ||
// TEMPFIX: Those services are special... | ||
![FATAL_ERROR, INJECTOR, SILO_CONTEXT].includes(serviceName) && | ||
![INJECTOR, SILO_CONTEXT].includes(serviceName) && | ||
initializer[SPECIAL_PROPS.SINGLETON] && | ||
@@ -218,3 +221,2 @@ this._initializersStates[serviceName] && | ||
// TEMPFIX: Those services are special... | ||
FATAL_ERROR, | ||
INJECTOR, | ||
@@ -227,3 +229,3 @@ SILO_CONTEXT, | ||
(this._initializersStates[serviceName]?.initializer?.[SPECIAL_PROPS.INJECT] || []) | ||
.map(_pickServiceNameFromDeclaration) | ||
.map((declaration) => parseDependencyDeclaration(declaration).serviceName) | ||
.includes(initializer[SPECIAL_PROPS.NAME])) { | ||
@@ -243,3 +245,3 @@ debug(`Found an inconsistent dependent initializer: ${initializer[SPECIAL_PROPS.NAME]}`, serviceName, initializer); | ||
_lookupCircularDependencies(rootServiceName, dependencyDeclaration, declarationsStacks = []) { | ||
const serviceName = _pickServiceNameFromDeclaration(dependencyDeclaration); | ||
const serviceName = parseDependencyDeclaration(dependencyDeclaration).serviceName; | ||
const initializersState = this._initializersStates[serviceName]; | ||
@@ -251,3 +253,3 @@ if (!initializersState || !initializersState.initializer) { | ||
(initializersState.initializer[SPECIAL_PROPS.INJECT] || []).forEach((childDependencyDeclaration) => { | ||
const childServiceName = _pickServiceNameFromDeclaration(childDependencyDeclaration); | ||
const childServiceName = parseDependencyDeclaration(childDependencyDeclaration).serviceName; | ||
if (rootServiceName === childServiceName) { | ||
@@ -307,3 +309,3 @@ throw new YError('E_CIRCULAR_DEPENDENCY', ...[rootServiceName] | ||
return links.concat(initializerState.initializer[SPECIAL_PROPS.INJECT].map((dependencyDeclaration) => { | ||
const dependedServiceName = _pickServiceNameFromDeclaration(dependencyDeclaration); | ||
const dependedServiceName = parseDependencyDeclaration(dependencyDeclaration).serviceName; | ||
return { serviceName, dependedServiceName }; | ||
@@ -357,3 +359,2 @@ })); | ||
loadingSequences: [], | ||
errorsPromises: [], | ||
}; | ||
@@ -363,15 +364,2 @@ if (this._shutdownPromise) { | ||
} | ||
// Create a provider for the special fatal error service | ||
this._initializersStates[FATAL_ERROR].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 | ||
@@ -381,93 +369,4 @@ this._initializersStates[SILO_CONTEXT].silosInstances[siloIndex] = { | ||
}; | ||
// Create a provider for the shutdown special dependency | ||
this._initializersStates[DISPOSE].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) { | ||
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]); | ||
// TODO: recreate error promise when autoloaded/injected things? | ||
debug('Handling fatal errors:', siloContext.errorsPromises); | ||
Promise.all(siloContext.errorsPromises).catch(siloContext.throwFatalError); | ||
const services = await this._loadInitializerDependencies(siloContext, [RUN_DEPENDENT_NAME], dependenciesDeclarations, [DISPOSE, FATAL_ERROR]); | ||
debug('All dependencies now loaded:', siloContext.loadingSequences); | ||
@@ -607,4 +506,7 @@ return services; | ||
if (provider.fatalErrorPromise) { | ||
const fatalErrorInitializerState = (await this._initializersStates[FATAL_ERROR]); | ||
await fatalErrorInitializerState.singletonProviderLoadPromise; | ||
const fatalError = fatalErrorInitializerState.singletonProvider.service; | ||
debug('Registering service descriptor error promise:', serviceName); | ||
siloContext.errorsPromises.push(provider.fatalErrorPromise); | ||
fatalError.registerErrorPromise(provider.fatalErrorPromise); | ||
} | ||
@@ -714,3 +616,3 @@ if (initializerState.initializer[SPECIAL_PROPS.SINGLETON]) { | ||
} | ||
debug(`${[...parentsNames, serviceName].join('->')}: Could not autoload the initializer...`, err); | ||
debug(`${[...parentsNames, serviceName].join('->')}: Could not autoload the initializer...`, printStackTrace(err)); | ||
initializerState.initializer = undefined; | ||
@@ -800,6 +702,2 @@ resolveInitializer(undefined); | ||
export { SPECIAL_PROPS, SPECIAL_PROPS_PREFIX, DECLARATION_SEPARATOR, OPTIONAL_FLAG, ALLOWED_INITIALIZER_TYPES, ALLOWED_SPECIAL_PROPS, parseInjections, readFunctionName, parseName, Knifecycle, initializer, name, autoName, type, inject, useInject, mergeInject, autoInject, alsoInject, extra, singleton, reuseSpecialProps, wrapInitializer, constant, service, autoService, provider, autoProvider, handler, autoHandler, parseDependencyDeclaration, stringifyDependencyDeclaration, unwrapInitializerProperties, initInitializerBuilder, }; | ||
function _pickServiceNameFromDeclaration(dependencyDeclaration) { | ||
const { serviceName } = parseDependencyDeclaration(dependencyDeclaration); | ||
return serviceName; | ||
} | ||
function _applyShapes(shapes, serviceName) { | ||
@@ -806,0 +704,0 @@ return shapes.reduce((shapedService, shape) => { |
@@ -569,3 +569,3 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
service: { | ||
fatalErrorPromise: $fatalError.promise, | ||
fatalErrorPromise: $fatalError.errorPromise, | ||
}, | ||
@@ -572,0 +572,0 @@ }); |
@@ -0,1 +1,2 @@ | ||
export declare const NO_PROVIDER: unique symbol; | ||
export declare const DECLARATION_SEPARATOR = ">"; | ||
@@ -2,0 +3,0 @@ export declare const OPTIONAL_FLAG = "?"; |
@@ -6,2 +6,3 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
const debug = initDebug('knifecycle'); | ||
export const NO_PROVIDER = Symbol('NO_PROVIDER'); | ||
/* Architecture Note #1.2: Creating initializers | ||
@@ -8,0 +9,0 @@ |
@@ -28,3 +28,3 @@ { | ||
"name": "knifecycle", | ||
"version": "16.0.1", | ||
"version": "17.0.0", | ||
"description": "Manage your NodeJS processes's lifecycle automatically with an unobtrusive dependency injection implementation.", | ||
@@ -74,3 +74,4 @@ "main": "dist/index.js", | ||
"dependencies", | ||
"service" | ||
"service", | ||
"knifecycle" | ||
], | ||
@@ -98,6 +99,6 @@ "author": { | ||
"jest": "^29.6.2", | ||
"jsarch": "^6.0.1", | ||
"jsarch": "^6.0.3", | ||
"jsdoc-to-markdown": "^8.0.0", | ||
"metapak": "^5.1.6", | ||
"metapak-nfroidure": "^15.0.2", | ||
"metapak": "^5.1.7", | ||
"metapak-nfroidure": "^15.0.3", | ||
"prettier": "^3.0.2", | ||
@@ -104,0 +105,0 @@ "rimraf": "^5.0.1", |
@@ -16,3 +16,2 @@ [//]: # ( ) | ||
[![Browser Support Matrix](https://saucelabs.com/open_sauce/build_matrix/nfroidure.svg)](https://saucelabs.com/u/nfroidure) | ||
@@ -29,2 +28,4 @@ Most (maybe all) applications rely on two kinds of dependencies. | ||
![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 | ||
@@ -35,2 +36,7 @@ provide code but access to global states (time, filesystem, db). It also have an | ||
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 | ||
@@ -41,4 +47,5 @@ handle the reasonning behind `knifecycle` and its implementation. | ||
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. | ||
@@ -50,10 +57,10 @@ | ||
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 | ||
@@ -63,7 +70,11 @@ problem within purely functional libraries (require allows it), it may be | ||
`$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 | ||
@@ -81,3 +92,9 @@ | ||
import { YError } from 'YError'; | ||
import Knifecycle, { initializer, constant, inject, name } from 'knifecycle'; | ||
import { | ||
Knifecycle, | ||
initializer, | ||
constant, | ||
inject, | ||
name | ||
} from 'knifecycle'; | ||
@@ -102,5 +119,9 @@ // First of all we create a new Knifecycle instance | ||
// 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) { | ||
@@ -115,4 +136,4 @@ reject(err); | ||
} | ||
}); | ||
}); | ||
}, | ||
); | ||
} | ||
@@ -148,3 +169,4 @@ | ||
// 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( | ||
@@ -231,19 +253,20 @@ { | ||
}, | ||
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]}!`); | ||
} | ||
}, | ||
), | ||
@@ -405,3 +428,3 @@ ); | ||
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 😉. | ||
@@ -408,0 +431,0 @@ [//]: # (::contents:end) |
/* 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'; | ||
@@ -15,2 +14,6 @@ import initInitializerBuilder from './build.js'; | ||
const mockedDepsHash = { | ||
$fatalError: constant('$fatalError', undefined), | ||
$dispose: constant('$dispose', undefined), | ||
$instance: constant('$instance', undefined), | ||
$siloContext: constant('$siloContext', undefined), | ||
NODE_ENV: constant('NODE_ENV', 'development'), | ||
@@ -71,5 +74,23 @@ dep1: initializer( | ||
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 | ||
@@ -86,3 +107,6 @@ import initDep1 from './services/dep1'; | ||
export async function initialize(services = {}) { | ||
const $fatalError = await initFatalError(); | ||
// Initialization batch #0 | ||
batchsDisposers[0] = []; | ||
const batch0 = { | ||
@@ -103,2 +127,3 @@ dep1: initDep1({ | ||
// Initialization batch #1 | ||
batchsDisposers[1] = []; | ||
const batch1 = { | ||
@@ -108,3 +133,11 @@ dep2: initDep2({ | ||
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; | ||
}), | ||
}; | ||
@@ -120,2 +153,3 @@ | ||
// Initialization batch #2 | ||
batchsDisposers[2] = []; | ||
const batch2 = { | ||
@@ -141,8 +175,7 @@ dep3: initDep3({ | ||
} | ||
`, | ||
); | ||
" | ||
`); | ||
}); | ||
// 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(); | ||
@@ -154,2 +187,3 @@ | ||
$.register(constant('$fatalError', {})); | ||
$.register(constant('$instance', {})); | ||
@@ -163,10 +197,29 @@ const { buildInitializer } = await $.run<any>(['buildInitializer']); | ||
'$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; | ||
@@ -180,3 +233,6 @@ // Definition batch #1 | ||
export async function initialize(services = {}) { | ||
const $fatalError = await initFatalError(); | ||
// Initialization batch #0 | ||
batchsDisposers[0] = []; | ||
const batch0 = { | ||
@@ -186,2 +242,6 @@ dep1: initDep1({ | ||
NODE_ENV: Promise.resolve(NODE_ENV), | ||
$fatalError: Promise.resolve($fatalError), | ||
$dispose: Promise.resolve($dispose), | ||
$instance: Promise.resolve($instance), | ||
$siloContext: Promise.resolve($siloContext), | ||
}; | ||
@@ -196,4 +256,9 @@ | ||
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 = { | ||
@@ -203,3 +268,11 @@ dep2: initDep2({ | ||
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; | ||
}), | ||
}; | ||
@@ -215,2 +288,3 @@ | ||
// Initialization batch #2 | ||
batchsDisposers[2] = []; | ||
const batch2 = { | ||
@@ -234,7 +308,11 @@ dep3: initDep3({ | ||
finalMappedDep: services['dep3'], | ||
$fatalError: services['$fatalError'], | ||
$dispose: services['$dispose'], | ||
$instance: services['$instance'], | ||
$siloContext: services['$siloContext'], | ||
}; | ||
} | ||
`, | ||
); | ||
" | ||
`); | ||
}); | ||
}); |
@@ -7,2 +7,5 @@ import { | ||
import { buildInitializationSequence } from './sequence.js'; | ||
import { FATAL_ERROR } from './fatalError.js'; | ||
import { DISPOSE } from './dispose.js'; | ||
import type { Autoloader } from './index.js'; | ||
import type { | ||
@@ -13,4 +16,5 @@ DependencyDeclaration, | ||
} from './util.js'; | ||
import type { Autoloader } from './index.js'; | ||
export const MANAGED_SERVICES = [FATAL_ERROR, DISPOSE, '$instance']; | ||
type DependencyTreeNode = { | ||
@@ -112,33 +116,58 @@ __name: string; | ||
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] | ||
@@ -164,3 +193,11 @@ ) { | ||
'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; | ||
})` | ||
: '' | ||
@@ -183,4 +220,4 @@ },`; | ||
`, | ||
) | ||
.join('')} | ||
) | ||
.join('')} | ||
return {${dependencies | ||
@@ -187,0 +224,0 @@ .map(parseDependencyDeclaration) |
@@ -795,3 +795,3 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
service: { | ||
fatalErrorPromise: $fatalError.promise, | ||
fatalErrorPromise: $fatalError.errorPromise, | ||
}, | ||
@@ -798,0 +798,0 @@ }); |
250
src/index.ts
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
/* eslint max-len: ["warn", { "ignoreComments": true }] @typescript-eslint/no-this-alias: "warn" */ | ||
import { | ||
NO_PROVIDER, | ||
SPECIAL_PROPS, | ||
@@ -37,4 +38,6 @@ SPECIAL_PROPS_PREFIX, | ||
} 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'; | ||
@@ -71,2 +74,4 @@ import type { | ||
import type { BuildInitializer } from './build.js'; | ||
import type { FatalErrorService } from './fatalError.js'; | ||
export { initFatalError, initDispose }; | ||
export type { | ||
@@ -101,2 +106,3 @@ ServiceName, | ||
BuildInitializer, | ||
FatalErrorService, | ||
}; | ||
@@ -108,3 +114,3 @@ | ||
export const INJECTOR_DEPENDENT_NAME = '__injector__'; | ||
export const NO_PROVIDER = Symbol('NO_PROVIDER'); | ||
export { NO_PROVIDER }; | ||
@@ -173,9 +179,4 @@ export type KnifecycleOptions = { | ||
loadingSequences: ServiceName[][]; | ||
errorsPromises: Promise<void>[]; | ||
_shutdownPromise?: Promise<void>; | ||
throwFatalError?: (err: Error) => void; | ||
} | ||
export type FatalErrorService = { | ||
promise: Promise<void>; | ||
}; | ||
@@ -193,8 +194,13 @@ export type InternalDependencies = { | ||
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, | ||
]; | ||
@@ -235,4 +241,4 @@ /* Architecture Note #1: Knifecycle | ||
private _silosCounter: number; | ||
private _silosContexts: Record<SiloIndex, SiloContext>; | ||
private _initializersStates: Record< | ||
_silosContexts: Record<SiloIndex, SiloContext>; | ||
_initializersStates: Record< | ||
string, | ||
@@ -263,8 +269,5 @@ InitializerStateDescriptor<unknown, Dependencies> | ||
[FATAL_ERROR]: { | ||
initializer: service(async () => { | ||
throw new YError('E_UNEXPECTED_INIT', FATAL_ERROR); | ||
}, FATAL_ERROR), | ||
initializer: initFatalError, | ||
autoloaded: false, | ||
dependents: [], | ||
silosInstances: {}, | ||
}, | ||
@@ -280,5 +283,3 @@ [SILO_CONTEXT]: { | ||
[DISPOSE]: { | ||
initializer: service(async () => { | ||
throw new YError('E_UNEXPECTED_INIT', DISPOSE); | ||
}, DISPOSE), | ||
initializer: initDispose as any, | ||
autoloaded: false, | ||
@@ -417,3 +418,3 @@ dependents: [], | ||
.map((dependencyDeclaration) => { | ||
const serviceName = _pickServiceNameFromDeclaration( | ||
const { serviceName } = parseDependencyDeclaration( | ||
dependencyDeclaration, | ||
@@ -426,3 +427,3 @@ ); | ||
// TEMPFIX: Those services are special... | ||
![FATAL_ERROR, INJECTOR, SILO_CONTEXT].includes(serviceName) && | ||
![INJECTOR, SILO_CONTEXT].includes(serviceName) && | ||
initializer[SPECIAL_PROPS.SINGLETON] && | ||
@@ -464,3 +465,2 @@ this._initializersStates[serviceName] && | ||
// TEMPFIX: Those services are special... | ||
FATAL_ERROR, | ||
INJECTOR, | ||
@@ -481,3 +481,6 @@ SILO_CONTEXT, | ||
) | ||
.map(_pickServiceNameFromDeclaration) | ||
.map( | ||
(declaration) => | ||
parseDependencyDeclaration(declaration).serviceName, | ||
) | ||
.includes(initializer[SPECIAL_PROPS.NAME]) | ||
@@ -520,3 +523,5 @@ ) { | ||
): void { | ||
const serviceName = _pickServiceNameFromDeclaration(dependencyDeclaration); | ||
const serviceName = parseDependencyDeclaration( | ||
dependencyDeclaration, | ||
).serviceName; | ||
const initializersState = this._initializersStates[serviceName]; | ||
@@ -531,5 +536,5 @@ | ||
(childDependencyDeclaration) => { | ||
const childServiceName = _pickServiceNameFromDeclaration( | ||
const childServiceName = parseDependencyDeclaration( | ||
childDependencyDeclaration, | ||
); | ||
).serviceName; | ||
@@ -616,5 +621,5 @@ if (rootServiceName === childServiceName) { | ||
(dependencyDeclaration) => { | ||
const dependedServiceName = _pickServiceNameFromDeclaration( | ||
const dependedServiceName = parseDependencyDeclaration( | ||
dependencyDeclaration, | ||
); | ||
).serviceName; | ||
@@ -693,3 +698,2 @@ return { serviceName, dependedServiceName }; | ||
loadingSequences: [], | ||
errorsPromises: [], | ||
}; | ||
@@ -701,21 +705,2 @@ | ||
// Create a provider for the special fatal error service | ||
( | ||
this._initializersStates[FATAL_ERROR] as SiloedInitializerStateDescriptor< | ||
FatalErrorService, | ||
Dependencies<unknown> | ||
> | ||
).silosInstances[siloIndex] = { | ||
provider: { | ||
service: { | ||
promise: new Promise<void>((_resolve, reject) => { | ||
siloContext.throwFatalError = (err) => { | ||
debug('Handled a fatal error', err); | ||
reject(err); | ||
}; | ||
}), | ||
}, | ||
}, | ||
}; | ||
// Make the siloContext available for internal injections | ||
@@ -729,138 +714,3 @@ ( | ||
}; | ||
// Create a provider for the shutdown special dependency | ||
( | ||
this._initializersStates[DISPOSE] as SiloedInitializerStateDescriptor< | ||
Disposer, | ||
Dependencies<unknown> | ||
> | ||
).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; | ||
@@ -872,9 +722,5 @@ | ||
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); | ||
@@ -1118,4 +964,14 @@ | ||
if (provider.fatalErrorPromise) { | ||
const fatalErrorInitializerState = (await this._initializersStates[ | ||
FATAL_ERROR | ||
]) as SingletonInitializerStateDescriptor<any, any>; | ||
await fatalErrorInitializerState.singletonProviderLoadPromise; | ||
const fatalError = ( | ||
fatalErrorInitializerState.singletonProvider as Provider<FatalErrorService> | ||
).service; | ||
debug('Registering service descriptor error promise:', serviceName); | ||
siloContext.errorsPromises.push(provider.fatalErrorPromise); | ||
fatalError.registerErrorPromise(provider.fatalErrorPromise); | ||
} | ||
@@ -1315,3 +1171,3 @@ | ||
)}: Could not autoload the initializer...`, | ||
err, | ||
printStackTrace(err as Error), | ||
); | ||
@@ -1479,10 +1335,2 @@ initializerState.initializer = undefined; | ||
function _pickServiceNameFromDeclaration( | ||
dependencyDeclaration: DependencyDeclaration, | ||
): ServiceName { | ||
const { serviceName } = parseDependencyDeclaration(dependencyDeclaration); | ||
return serviceName; | ||
} | ||
function _applyShapes(shapes, serviceName) { | ||
@@ -1489,0 +1337,0 @@ return shapes.reduce((shapedService, shape) => { |
@@ -9,2 +9,4 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
export const NO_PROVIDER = Symbol('NO_PROVIDER'); | ||
/* Architecture Note #1.2: Creating initializers | ||
@@ -11,0 +13,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
616285
44
11016
881